2020 年 12 月 6 日

影子 DOM

影子 DOM 用於封裝。它允許元件擁有自己的「影子」DOM 樹,無法從主文件意外存取,可能有本機樣式規則,以及更多功能。

內建影子 DOM

你是否曾想過複雜的瀏覽器控制項是如何建立和設定樣式的?

例如 <input type="range">

瀏覽器在內部使用 DOM/CSS 來繪製它們。該 DOM 結構通常對我們隱藏,但我們可以在開發人員工具中看到它。例如在 Chrome 中,我們需要在開發人員工具中啟用「顯示使用者代理影子 DOM」選項。

然後 <input type="range"> 會像這樣

你在 #shadow-root 下看到的是「影子 DOM」。

我們無法透過常規的 JavaScript 呼叫或選擇器取得內建影子 DOM 元素。這些不是常規的子項,而是一種強大的封裝技術。

在上面的範例中,我們可以看到一個有用的屬性 pseudo。它是非標準的,存在於歷史原因。我們可以使用它來使用 CSS 設定子元素樣式,如下所示

<style>
/* make the slider track red */
input::-webkit-slider-runnable-track {
  background: red;
}
</style>

<input type="range">

再次強調,pseudo 是一個非標準屬性。依時間順序來說,瀏覽器首先開始嘗試使用內部 DOM 結構來實作控制項,然後,隨著時間推移,Shadow DOM 被標準化,允許我們開發人員執行類似的事情。

接下來,我們將使用現代 Shadow DOM 標準,由 DOM 規範 和其他相關規範涵蓋。

Shadow tree

一個 DOM 元素可以有兩種 DOM 子樹

  1. Light tree – 一個常規 DOM 子樹,由 HTML 子項組成。我們在前面章節中看到的所有子樹都是「Light」。
  2. Shadow tree – 一個隱藏的 DOM 子樹,不會反映在 HTML 中,不會被窺探到。

如果一個元素同時擁有這兩種子樹,則瀏覽器只會渲染 Shadow tree。但是,我們也可以設定 Shadow tree 和 Light tree 之間的一種組合。我們將在章節 Shadow DOM slots, composition 中看到詳細資訊。

Shadow tree 可以用於自訂元素,以隱藏組件內部結構並套用組件本機樣式。

例如,這個 <show-hello> 元素會將其內部 DOM 隱藏在 Shadow tree 中

<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 開發人員工具中顯示的 DOM 結果,所有內容都位於「#shadow-root」之下

首先,呼叫 elem.attachShadow({mode: …}) 會建立一個 Shadow tree。

有兩個限制

  1. 我們只能為每個元素建立一個 Shadow root。
  2. elem 必須是自訂元素,或下列元素之一:「article」、「aside」、「blockquote」、「body」、「div」、「footer」、「h1…h6」、「header」、「main」、「nav」、「p」、「section」或「span」。其他元素,例如 <img>,無法承載 Shadow tree。

mode 選項設定封裝層級。它必須具有下列兩個值之一

  • "open" – Shadow root 可用於 elem.shadowRoot

    任何程式碼都可以存取 elem 的 Shadow tree。

  • "closed"elem.shadowRoot 永遠是 null

    我們只能透過 attachShadow 傳回的參照存取 Shadow DOM(而且可能隱藏在類別中)。瀏覽器原生 Shadow tree,例如 <input type="range">,是封閉的。無法存取它們。

attachShadow 所傳回的 影子根節點 就如同一個元素:我們可以使用 innerHTML 或 DOM 方法(例如 append)來填充它。

具有影子根節點的元素稱為「影子樹宿主」,並可作為影子根節點的 host 屬性取得

// assuming {mode: "open"}, otherwise elem.shadowRoot is null
alert(elem.shadowRoot.host === elem); // true

封裝

影子 DOM 與主文件有嚴格的區隔

  1. 影子 DOM 元素對於來自光 DOM 的 querySelector 是不可見的。特別是,影子 DOM 元素的 id 可能與光 DOM 中的 id 衝突。它們必須在影子樹中保持唯一性。
  2. 影子 DOM 有自己的樣式表。外部 DOM 的樣式規則不會套用。

例如

<style>
  /* document style won't apply to the shadow tree inside #elem (1) */
  p { color: red; }
</style>

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

<script>
  elem.attachShadow({mode: 'open'});
    // shadow tree has its own style (2)
  elem.shadowRoot.innerHTML = `
    <style> p { font-weight: bold; } </style>
    <p>Hello, John!</p>
  `;

  // <p> is only visible from queries inside the shadow tree (3)
  alert(document.querySelectorAll('p').length); // 0
  alert(elem.shadowRoot.querySelectorAll('p').length); // 1
</script>
  1. 文件中的樣式不會影響影子樹。
  2. …但內部的樣式會生效。
  3. 若要取得影子樹中的元素,我們必須從樹內部查詢。

參考資料

摘要

影子 DOM 是一種建立元件區域 DOM 的方法。

  1. shadowRoot = elem.attachShadow({mode: open|closed}) – 為 elem 建立影子 DOM。如果 mode="open",則可作為 elem.shadowRoot 屬性取得。
  2. 我們可以使用 innerHTML 或其他 DOM 方法來填充 shadowRoot

影子 DOM 元素

  • 有自己的 id 空間,
  • 對於主文件中的 JavaScript 選擇器(例如 querySelector)是不可見的,
  • 僅使用影子樹中的樣式,不使用主文件中的樣式。

如果存在影子 DOM,瀏覽器會渲染它,而不是所謂的「光 DOM」(一般子元素)。在 影子 DOM 插槽、組成 章節中,我們將了解如何組成它們。

教學課程地圖

留言

留言前請先閱讀…
  • 如果您有改進建議 - 請提交 GitHub 議題或發起 pull request,而非留言。
  • 如果您無法理解文章中的某個部分 – 請詳細說明。
  • 若要插入少數幾個字元的程式碼,請使用 <code> 標籤,若要插入多行程式碼,請將其包覆在 <pre> 標籤中,若要插入超過 10 行程式碼,請使用沙盒 (plnkrjsbincodepen…)