Javascript 影子 DOM(Shadow DOM)

2023-02-17 10:58 更新

Shadow DOM 為封裝而生。它可以讓一個(gè)組件擁有自己的「影子」DOM 樹(shù),這個(gè) DOM 樹(shù)不能在主文檔中被任意訪(fǎng)問(wèn),可能擁有局部樣式規(guī)則,還有其他特性。

內(nèi)建 shadow DOM

你是否曾經(jīng)思考過(guò)復(fù)雜的瀏覽器控件是如何被創(chuàng)建和添加樣式的?

瀏覽器在內(nèi)部使用 DOM/CSS 來(lái)繪制它們。這個(gè) DOM 結(jié)構(gòu)一般來(lái)說(shuō)對(duì)我們是隱藏的,但我們可以在開(kāi)發(fā)者工具里面看見(jiàn)它。比如,在 Chrome 里,我們需要打開(kāi)「Show user agent shadow DOM」選項(xiàng)。

然后 <input type="range"> 看起來(lái)會(huì)像這樣:


你在 #shadow-root 下看到的就是被稱(chēng)為「shadow DOM」的東西。

我們不能使用一般的 JavaScript 調(diào)用或者選擇器來(lái)獲取內(nèi)建 shadow DOM 元素。它們不是常規(guī)的子元素,而是一個(gè)強(qiáng)大的封裝手段。

在上面的例子中,我們可以看到一個(gè)有用的屬性 pseudo。這是一個(gè)因?yàn)闅v史原因而存在的屬性,并不在標(biāo)準(zhǔn)中。我們可以使用它來(lái)給子元素加上 CSS 樣式,像這樣:

<style>
/* 讓滑塊軌道變紅 */
input::-webkit-slider-runnable-track {
  background: red;
}
</style>

<input type="range">

重申一次,pseudo 是一個(gè)非標(biāo)準(zhǔn)的屬性。按照時(shí)間順序來(lái)說(shuō),瀏覽器首先實(shí)驗(yàn)了使用內(nèi)部 DOM 結(jié)構(gòu)來(lái)實(shí)現(xiàn)控件,然后,在一段時(shí)間之后,shadow DOM 才被標(biāo)準(zhǔn)化來(lái)讓我們,開(kāi)發(fā)者們,做類(lèi)似的事。

接下來(lái),我們將要使用現(xiàn)代 shadow DOM 標(biāo)準(zhǔn),它在 DOM spec 和其他相關(guān)標(biāo)準(zhǔn)中可以被找到。

Shadow tree

一個(gè) DOM 元素可以有以下兩類(lèi) DOM 子樹(shù):

  1. Light tree(光明樹(shù)) —— 一個(gè)常規(guī) DOM 子樹(shù),由 HTML 子元素組成。我們?cè)谥罢鹿?jié)看到的所有子樹(shù)都是「光明的」。
  2. Shadow tree(影子樹(shù)) —— 一個(gè)隱藏的 DOM 子樹(shù),不在 HTML 中反映,無(wú)法被察覺(jué)。

如果一個(gè)元素同時(shí)有以上兩種子樹(shù),那么瀏覽器只渲染 shadow tree。但是我們同樣可以設(shè)置兩種樹(shù)的組合。我們將會(huì)在后面的章節(jié) Shadow DOM 插槽,組成 中看到更多細(xì)節(jié)。

影子樹(shù)可以在自定義元素中被使用,其作用是隱藏組件內(nèi)部結(jié)構(gòu)和添加只在組件內(nèi)有效的樣式。

比如,這個(gè) <show-hello> 元素將它的內(nèi)部 DOM 隱藏在了影子里面:

<script>
customElements.define('show-hello', class extends HTMLElement {
  connectedCallback() {
    const shadow = this.attachShadow({mode: 'open'});
    shadow.innerHTML = `<p>
      Hello, ${this.getAttribute('name')}
    </p>`;
  }
});
</script>

<show-hello name="John"></show-hello>

這就是在 Chrome 開(kāi)發(fā)者工具中看到的最終樣子,所有的內(nèi)容都在「#shadow-root」下:


首先,調(diào)用 elem.attachShadow({mode: …}) 可以創(chuàng)建一個(gè) shadow tree。

這里有兩個(gè)限制:

  1. 在每個(gè)元素中,我們只能創(chuàng)建一個(gè) shadow root。
  2. ?elem? 必須是自定義元素,或者是以下元素的其中一個(gè):「article」、「aside」、「blockquote」、「body」、「div」、「footer」、「h1…h(huán)6」、「header」、「main」、「nav」、「p」、「section」或者「span」。其他元素,比如 ?<img>?,不能容納 shadow tree。

mode 選項(xiàng)可以設(shè)定封裝層級(jí)。他必須是以下兩個(gè)值之一:

  • ?「open」? —— shadow root 可以通過(guò) ?elem.shadowRoot? 訪(fǎng)問(wèn)。
  • 任何代碼都可以訪(fǎng)問(wèn) elem 的 shadow tree。

  • ?「closed」? —— ?elem.shadowRoot? 永遠(yuǎn)是 ?null?。
  • 我們只能通過(guò) attachShadow 返回的指針來(lái)訪(fǎng)問(wèn) shadow DOM(并且可能隱藏在一個(gè) class 中)。瀏覽器原生的 shadow tree,比如 <input type="range">,是封閉的。沒(méi)有任何方法可以訪(fǎng)問(wèn)它們。

attachShadow 返回的 shadow root,和任何元素一樣:我們可以使用 innerHTML 或者 DOM 方法,比如 append 來(lái)擴(kuò)展它。

我們稱(chēng)有 shadow root 的元素叫做「shadow tree host」,可以通過(guò) shadow root 的 host 屬性訪(fǎng)問(wèn):

// 假設(shè) {mode: "open"},否則 elem.shadowRoot 是 null
alert(elem.shadowRoot.host === elem); // true

封裝

Shadow DOM 被非常明顯地和主文檔分開(kāi):

  1. Shadow DOM 元素對(duì)于 light DOM 中的 ?querySelector? 不可見(jiàn)。實(shí)際上,Shadow DOM 中的元素可能與 light DOM 中某些元素的 id 沖突。這些元素必須在 shadow tree 中獨(dú)一無(wú)二。
  2. Shadow DOM 有自己的樣式。外部樣式規(guī)則在 shadow DOM 中不產(chǎn)生作用。

比如:

<style>
  /* 文檔樣式對(duì) #elem 內(nèi)的 shadow tree 無(wú)作用 (1) */
  p { color: red; }
</style>

<div id="elem"></div>

<script>
  elem.attachShadow({mode: 'open'});
    // shadow tree 有自己的樣式 (2)
  elem.shadowRoot.innerHTML = `
    <style> p { font-weight: bold; } </style>
    <p>Hello, John!</p>
  `;

  // <p> 只對(duì) shadow tree 里面的查詢(xún)可見(jiàn) (3)
  alert(document.querySelectorAll('p').length); // 0
  alert(elem.shadowRoot.querySelectorAll('p').length); // 1
</script>
  1. 文檔里面的樣式對(duì) shadow tree 沒(méi)有任何效果。
  2. ……但是內(nèi)部的樣式是有效的。
  3. 為了獲取 shadow tree 內(nèi)部的元素,我們可以從樹(shù)的內(nèi)部查詢(xún)。

參考

總結(jié)

Shadow DOM 是創(chuàng)建組件級(jí)別 DOM 的一種方法。

  1. ?shadowRoot = elem.attachShadow({mode: open|closed})? —— 為 ?elem? 創(chuàng)建 shadow DOM。如果 ?mode="open"?,那么它通過(guò) ?elem.shadowRoot? 屬性被訪(fǎng)問(wèn)。
  2. 我們可以使用 ?innerHTML? 或者其他 DOM 方法來(lái)擴(kuò)展 ?shadowRoot?。

Shadow DOM 元素:

  • 有自己的 id 空間。
  • 對(duì)主文檔的 JavaScript 選擇器隱身,比如 ?querySelector?。
  • 只使用 shadow tree 內(nèi)部的樣式,不使用主文檔的樣式。

Shadow DOM,如果存在的話(huà),會(huì)被瀏覽器渲染而不是所謂的 「light DOM」(普通子元素)。在 Shadow DOM 插槽,組成 章節(jié)中我們將會(huì)看到如何組織它們。


以上內(nèi)容是否對(duì)您有幫助:
在線(xiàn)筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)