Shadow DOM 101

Dominic Cooney
Dominic Cooney

簡介

網頁元件是一套尖端標準,可:

  1. 可以建構小工具
  2. ...而且可明確重複使用
  3. ...而且,下一個元件版本變更內部實作詳細資料時,也不會破壞頁面。

這表示您必須決定使用 HTML/JavaScript 的時機和時機嗎?不!HTML 和 JavaScript 都具有互動性的視覺內容小工具是互動式的視覺內容,開發小工具時,請妥善運用您的 HTML 和 JavaScript 技能。網頁元件標準旨在協助您完成這項作業。

但有一個根本問題會導致使用 HTML 和 JavaScript 建構的小工具難以使用:小工具中的 DOM 樹狀結構不會從網頁的其他部分封裝。缺少封裝代表文件樣式表可能會意外套用至小工具中的某些部分;JavaScript 可能會不小心修改小工具內的某些部分,而 ID 可能會與小工具內的 ID 重疊,以此類推。

網頁元件由三個部分組成:

  1. 範本
  2. Shadow DOM
  3. 自訂元素

Shadow DOM 可解決 DOM 樹狀結構封裝問題。網頁元件的四個部分可以互相搭配運作,但您也可以挑選要使用的網頁元件部分。本教學課程說明如何使用 Shadow DOM。

影子世界大家好

使用 Shadow DOM 時,元素可以取得與物件相關聯的新類型的節點。這種新的節點類型稱為「陰影根」。與陰影根相關聯的元素稱為「陰影主機」。系統不會轉譯陰影主機的內容,而是算繪陰影根的內容。

舉例來說,如果您有以下標記:

<button>Hello, world!</button>
<script>
var host = document.querySelector('button');
var root = host.createShadowRoot();
root.textContent = 'こんにちは、影の世界!';
</script>

而不是

<button id="ex1a">Hello, world!</button>
<script>
function remove(selector) {
  Array.prototype.forEach.call(
      document.querySelectorAll(selector),
      function (node) { node.parentNode.removeChild(node); });
}

if (!HTMLElement.prototype.createShadowRoot) {
  remove('#ex1a');
  document.write('<img src="SS1.png" alt="Screenshot of a button with \'Hello, world!\' on it.">');
}
</script>

你的網頁看起來

<button id="ex1b">Hello, world!</button>
<script>
(function () {
  if (!HTMLElement.prototype.createShadowRoot) {
    remove('#ex1b');
    document.write('<img src="SS2.png" alt="Screenshot of a button with \'Hello, shadow world!\' in Japanese on it.">');
    return;
  }
  var host = document.querySelector('#ex1b');
  var root = host.createShadowRoot();
  root.textContent = 'こんにちは、影の世界!';
})();
</script>

不僅如此,如果網頁上的 JavaScript 要求該按鈕的 textContent 是什麼,也不會得到「天氣真好啊,通俗的!」,而是「Hello, world!」,因為影子根下的 DOM 子樹狀結構已封裝完畢。

區隔簡報內容和簡報

我們現在要瞭解如何使用 Shadow DOM 區隔內容與呈現內容。假設我們有以下這個名稱標記:

<style>
.ex2a.outer {
  border: 2px solid brown;
  border-radius: 1em;
  background: red;
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
}
.ex2a .boilerplate {
  color: white;
  font-family: sans-serif;
  padding: 0.5em;
}
.ex2a .name {
  color: black;
  background: white;
  font-family: "Marker Felt", cursive;
  font-size: 45pt;
  padding-top: 0.2em;
}
</style>
<div class="ex2a outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div>

以下是標記。您現在撰寫的內容。而不是使用 Shadow DOM:

<style>
.outer {
  border: 2px solid brown;
  border-radius: 1em;
  background: red;
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
}
.boilerplate {
  color: white;
  font-family: sans-serif;
  padding: 0.5em;
}
.name {
  color: black;
  background: white;
  font-family: "Marker Felt", cursive;
  font-size: 45pt;
  padding-top: 0.2em;
}
</style>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div>

由於 DOM 樹狀結構缺少封裝,因此文件中會公開名稱標記的整個結構。如果該頁面上其他元素意外使用相同的類別名稱來設定樣式或指令碼,我們就不會發生問題。

我們不必浪費時間。

步驟 1:隱藏簡報詳細資料

理論上,我們可能只在意:

  • 是「名稱標記」,
  • 名稱為「Bob」。

首先,我們編寫的標記必須越接近我們要的實際語意:

<div id="nameTag">Bob</div>

接著,我們會將呈現用的所有樣式和 div 放入 <template> 元素中:

<div id="nameTag">Bob</div>
<template id="nameTagTemplate">
<span class="unchanged"><style>
.outer {
  border: 2px solid brown;

  … same as above …

</style>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div></span>
</template>

此時,「Bob」是唯一轉譯的內容。由於我們將呈現的 DOM 元素移至 <template> 元素內,因此不會轉譯,但可以透過 JavaScript 存取。現在會填入陰影根目錄:

<script>
var shadow = document.querySelector('#nameTag').createShadowRoot();
var template = document.querySelector('#nameTagTemplate');
var clone = document.importNode(template.content, true);
shadow.appendChild(clone);

陰影根目錄已經設定完成,名稱標記也會再次轉譯。以滑鼠右鍵按一下名稱,並檢查看到其是甜點的語意標記元素:

<div id="nameTag">Bob</div>

這表示透過使用 Shadow DOM,我們已在文件中隱藏名稱標記的顯示詳細資料。呈現詳細資料會封裝在 Shadow DOM 中。

步驟 2:將內容與簡報分開

我們的名稱標記現在會隱藏網頁上的簡報詳細資料,但實際上並未將呈現方式與內容分開,因為雖然內容 (名稱「Bob」) 位於頁面中,而顯示的名稱就是我們複製到陰影根目錄的名稱。如果我們想變更名稱標記的名稱,就必須在兩個地方執行變更,但兩者可能會無法同步。

HTML 元素是可組合的,例如,您可以將按鈕放在表格中。我們只需要組合是構成元素:名稱標記必須是紅色背景、「嗨!」文字,以及名稱標記上的內容。

身為元件的作者,您會使用名為 <content> 的新元素來定義組合如何搭配小工具運作。這樣做會在小工具的呈現中建立插入點,並在該時間點建立陰影主機的插入點 cherry-picks 內容。

如果我們將 Shadow DOM 中的標記變更為:

<span class="unchanged"><template id="nameTagTemplate">
<style>
  …
</style></span>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    <content></content>
  </div>
</div>
<span class="unchanged"></template></span>

在轉譯名稱標記時,陰影主機的內容會投影到 <content> 元素出現的位置。

現在文件的結構會更加簡單,因為名稱只會集中在同一處,也就是文件。如果您的頁面需要更新使用者名稱

document.querySelector('#nameTag').textContent = 'Shellie';

就這樣因為我們正在使用 <content> 投影名稱標記的內容,因此瀏覽器會自動更新名稱標記的呈現方式。

<div id="ex2b">

現在你已區隔內容和呈現方式。內容位於文件中;簡報位於 Shadow DOM。瀏覽器在轉譯內容時,會自動同步這些標記。

步驟 3:利潤

藉由將內容和呈現方式分隔開來,我們就可以簡化操控內容的程式碼;在名稱標記範例中,程式碼只需要處理一個包含一個 <div> 而非多個內容的簡單結構。

現在,當我們變更呈現方式時,也不需要變更任何程式碼!

舉例來說,假設我們想要本地化名稱標籤。這仍然是名稱標記,因此文件中的語意內容不會改變:

<div id="nameTag">Bob</div>

陰影根設定程式碼維持不變。只要在陰影根層級進行變更即可

<template id="nameTagTemplate">
<style>
.outer {
  border: 2px solid pink;
  border-radius: 1em;
  background: url(sakura.jpg);
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
  font-family: sans-serif;
  font-weight: bold;
}
.name {
  font-size: 45pt;
  font-weight: normal;
  margin-top: 0.8em;
  padding-top: 0.2em;
}
</style>
<div class="outer">
  <div class="name">
    <content></content>
  </div>
  と申します。
</div>
</template>

這對於目前的網路情況而言是一大進步,因為您的名稱更新程式碼可以依附簡單且一致的元件結構。姓名更新程式碼不需要知道用於轉譯的結構。如果我們考慮轉譯了什麼,則這個名稱會以英文撰寫 (位於「嗨!我叫」)。

其他課程內容:進階投影

在上述範例中,<content> 元素會挑選陰影主機的所有內容。使用 select 屬性即可控管內容元素專案。您還可以使用多個內容元素

舉例來說,如果您的文件含有以下內容:

<div id="nameTag">
  <div class="first">Bob</div>
  <div>B. Love</div>
  <div class="email">bob@</div>
</div>

和使用 CSS 選取器選取特定內容的陰影根目錄:

<div style="background: purple; padding: 1em;">
  <div style="color: red;">
    <content **select=".first"**></content>
  </div>
  <div style="color: yellow;">
    <content **select="div"**></content>
  </div>
  <div style="color: blue;">
    <content **select=".email">**</content>
  </div>
</div>

<content select="div"><content select=".email"> 元素會比對 <div class="email"> 元素。志明的電子郵件地址會以多少顏色顯示?

答案是,志明的電子郵件地址一次會顯示為黃色。

原因在於,駭入 Shadow DOM 的使用者知道,建構已實際轉譯畫面的樹狀結構就像是一個巨大的派對。內容元素是一種邀請,可將文件內容導入後台 Shadow DOM 轉譯方。這些邀請會依序傳送,接收邀請的對象則取決於收件者 (即 select 屬性)。內容邀請後,一律應接受邀請 (對方不會?!),否則送出邀請。如果後續邀請又寄到該地址,應該不會有人在家,也就不會回到您的派對。

在上述範例中,<div class="email"> 會同時比對 div 選取器和 .email 選取器,但由於含有 div 選取工具的內容元素較早出現在文件前,因此 <div class="email"> 會進入黃色方,沒有人可以來參加藍調派對。(這或許會原因可能是藍色,雖然悲傷的喜歡公司,所以你其實不知道。)

如果是受邀參加「無」方的提案,系統就完全不會將其算繪。這就是第一個示例中的「Hello, world」文字如果您想達到截然不同的轉譯結果,這種做法就很實用:您可以在文件中編寫語意模型 (頁面中的指令碼皆可存取),但為了算繪目的而隱藏該模型,並使用 JavaScript 將該模型連結到實際不同的 Shadow DOM 轉譯模型。

舉例來說,HTML 有合適的日期挑選器。寫完 <input type="date"> 後,系統就會顯示整潔的彈出式日曆。但如果您想讓使用者為「甜點」選擇一段日期範圍 (你知道的...有由紅維尼製成的吊床) 就沒問題。請按照下列方式設定文件:

<div class="dateRangePicker">
  <label for="start">Start:</label>
  <input type="date" name="startDate" id="start">
  <br>
  <label for="end">End:</label>
  <input type="date" name="endDate" id="end">
</div>

但可以建立 Shadow DOM,藉此使用資料表建立以圖表標示的日期範圍等資訊。當使用者點選日曆中的日期時,元件會更新 startDate 和 endDate 輸入內容中的狀態;當使用者提交表單時,系統會提交這些輸入元素的值。

為什麼我在文件中加入無法顯示的標籤?原因在於,如果使用者透過不支援 Shadow DOM 的瀏覽器查看表單,表單仍可使用,只是不再那麼美。使用者會看到以下內容:

<div class="dateRangePicker">
  <label for="start">Start:</label>
  <input type="date" name="startDate" id="start">
  <br>
  <label for="end">End:</label>
  <input type="date" name="endDate" id="end">
</div>

您通過 Shadow DOM 101

這些是 Shadow DOM 的基本概念,也就是您傳遞了 Shadow DOM 101!舉例來說,您可以使用 Shadow DOM 執行更多操作,例如在一個陰影主機中使用多個陰影,或針對封裝使用巢狀陰影,或使用模型導向的檢視畫面 (MDV) 和 Shadow DOM 建立網頁架構。網頁元件不只是 Shadow DOM。

我們會在後續文章中說明。