Javascript 影子 DOM(Shadow DOM)

2023-02-17 10:58 更新

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

內(nèi)建 shadow DOM

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

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

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


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

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

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

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

<input type="range">

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

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

Shadow tree

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

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

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

影子樹可以在自定義元素中被使用,其作用是隱藏組件內(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 開發(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 可以通過 ?elem.shadowRoot? 訪問。
  • 任何代碼都可以訪問 elem 的 shadow tree。

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

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

我們稱有 shadow root 的元素叫做「shadow tree host」,可以通過 shadow root 的 host 屬性訪問:

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

封裝

Shadow DOM 被非常明顯地和主文檔分開:

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

比如:

<style>
  /* 文檔樣式對(duì) #elem 內(nèi)的 shadow tree 無作用 (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 里面的查詢可見 (3)
  alert(document.querySelectorAll('p').length); // 0
  alert(elem.shadowRoot.querySelectorAll('p').length); // 1
</script>
  1. 文檔里面的樣式對(duì) shadow tree 沒有任何效果。
  2. ……但是內(nèi)部的樣式是有效的。
  3. 為了獲取 shadow tree 內(nèi)部的元素,我們可以從樹的內(nèi)部查詢。

參考

總結(jié)

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

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

Shadow DOM 元素:

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

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


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

掃描二維碼

下載編程獅App

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

編程獅公眾號(hào)