陰影 DOM 301

進階概念和 DOM API

本文將探討 Shadow DOM 的眾多強大功能!以 Shadow DOM 101Shadow DOM 201 中的概念為基礎。

使用多個陰影根

如果你是舉辦派對,但所有人都一陣子沒在同一個房間裡,就是為了跳脫束縛。 您想選擇將多組使用者分散到多個房間。代管 Shadow DOM 的元素也可以執行這種做法,也就是說,一次可代管多個陰影根。

讓我們看看如果嘗試將多個陰影根連接至主機,會發生什麼情況:

<div id="example1">Light DOM</div>
<script>
  var container = document.querySelector('#example1');
  var root1 = container.createShadowRoot();
  var root2 = container.createShadowRoot();
  root1.innerHTML = '<div>Root 1 FTW</div>';
  root2.innerHTML = '<div>Root 2 FTW</div>';
</script>

雖然我們已附加陰影樹狀結構,但會顯示「Root 2 FTW」;這是因為最後加到主機的陰影樹才是贏家。這是 LIFO 堆疊,就像算繪一樣。檢查開發人員工具可以驗證這項行為。

那麼,如果只有最後一個受邀對象加入算繪方,使用多個陰影的地方是什麼呢?輸入陰影插入點。

陰影插入點

「陰影插入點」(<shadow>) 和一般的插入點 (<content>) 類似,因為它們是預留位置。不過,這些主機可能代管其他陰影樹狀結構,而不是預留位置的「內容」。活動是 Shadow DOM Inception!

您或許能夠想到,只要細查兔子洞,就能變得更加複雜。因此,規格非常清楚顯示多個 <shadow> 元素正在執行時會發生的情況:

回到原始範例,第一個陰影 root1 從邀請清單中遺漏。新增 <shadow> 插入點即可取回:

<div id="example2">Light DOM</div>
<script>
var container = document.querySelector('#example2');
var root1 = container.createShadowRoot();
var root2 = container.createShadowRoot();
root1.innerHTML = '<div>Root 1 FTW</div><content></content>';
**root2.innerHTML = '<div>Root 2 FTW</div><shadow></shadow>';**
</script>

這個範例有幾個有趣的地方:

  1. 「Root 2 FTW」仍會顯示在「Root 1 FTW」上方。這是因為我們把 <shadow> 插入點放在哪裡。如要反向移動,請移動插入點:root2.innerHTML = '<shadow></shadow><div>Root 2 FTW</div>';
  2. 請注意,Root1 中現在有 <content> 插入點。如此一來,文字節點「Light DOM」就會伴隨轉譯行程一併顯示。

<shadow> 會顯示什麼?

有時候,瞭解在 <shadow> 轉譯的舊版陰影樹狀結構會很有幫助。您可以透過 .olderShadowRoot 取得該樹狀結構的參照:

**root2.olderShadowRoot** === root1 //true

取得主機的影子根

如果元素代管了 Shadow DOM,您可以使用 .shadowRoot 存取其最小的陰影根

var root = host.createShadowRoot();
console.log(host.shadowRoot === root); // true
console.log(document.body.shadowRoot); // null

如果您擔心有人跨越到陰影,請將 .shadowRoot 重新定義為空值:

Object.defineProperty(host, 'shadowRoot', {
  get: function() { return null; },
  set: function(value) { }
});

只是個小技巧,卻有效。最後,請務必牢記,雖然 Shadow DOM 並不是專為安全防護設計,但功能強大。因此,請勿以此方式完成內容隔離。

在 JS 中建構 Shadow DOM

如果您偏好使用 JS 建構 DOM,HTMLContentElementHTMLShadowElement 有適合這個介面的介面。

<div id="example3">
  <span>Light DOM</span>
</div>
<script>
var container = document.querySelector('#example3');
var root1 = container.createShadowRoot();
var root2 = container.createShadowRoot();

var div = document.createElement('div');
div.textContent = 'Root 1 FTW';
root1.appendChild(div);

 // HTMLContentElement
var content = document.createElement('content');
content.select = 'span'; // selects any spans the host node contains
root1.appendChild(content);

var div = document.createElement('div');
div.textContent = 'Root 2 FTW';
root2.appendChild(div);

// HTMLShadowElement
var shadow = document.createElement('shadow');
root2.appendChild(shadow);
</script>

本例與上一節的範例幾乎相同。唯一的差別在於,我現在使用 select 來提取新增的 <span>

使用插入點

從主機元素中選取的節點,並「分散」到陰影樹狀結構中,稱為...分散式節點!插入點邀請他們加入時,它們可以跨越陰影邊界。

插入點的概念比較奇怪,那就是這些點不會實際移動 DOM。主機的節點則不受影響。插入點只是將主機的節點重新投影到陰影樹狀結構。這是展示/算繪的東西:「將這些節點移到這裡」「在這個位置算繪這些節點。」

例如:

<div><h2>Light DOM</h2></div>
<script>
var root = document.querySelector('div').createShadowRoot();
root.innerHTML = '<content select="h2"></content>';

var h2 = document.querySelector('h2');
console.log(root.querySelector('content[select="h2"] h2')); // null;
console.log(root.querySelector('content').contains(h2)); // false
</script>

你看看! h2 並非 shadow DOM 的子項。這會產生另一個原始物件:

Element.getDistributedNodes()

我們無法掃遍 <content>,但 .getDistributedNodes() API 可讓我們在插入點查詢分散式節點:

<div id="example4">
  <h2>Eric</h2>
  <h2>Bidelman</h2>
  <div>Digital Jedi</div>
  <h4>footer text</h4>
</div>

<template id="sdom">
  <header>
    <content select="h2"></content>
  </header>
  <section>
    <content select="div"></content>
  </section>
  <footer>
    <content select="h4:first-of-type"></content>
  </footer>
</template>

<script>
var container = document.querySelector('#example4');

var root = container.createShadowRoot();

var t = document.querySelector('#sdom');
var clone = document.importNode(t.content, true);
root.appendChild(clone);

var html = [];
[].forEach.call(root.querySelectorAll('content'), function(el) {
  html.push(el.outerHTML + ': ');
  var nodes = el.getDistributedNodes();
  [].forEach.call(nodes, function(node) {
    html.push(node.outerHTML);
  });
  html.push('\n');
});
</script>

Element.getDestinationInsertionPoints()

.getDistributedNodes() 類似,您可以呼叫節點的 .getDestinationInsertionPoints() 來檢查節點分配給哪個插入點:

<div id="host">
  <h2>Light DOM
</div>

<script>
  var container = document.querySelector('div');

  var root1 = container.createShadowRoot();
  var root2 = container.createShadowRoot();
  root1.innerHTML = '<content select="h2"></content>';
  root2.innerHTML = '<shadow></shadow>';

  var h2 = document.querySelector('#host h2');
  var insertionPoints = h2.getDestinationInsertionPoints();
  [].forEach.call(insertionPoints, function(contentEl) {
    console.log(contentEl);
  });
</script>

工具:Shadow DOM Visualizer

「影子 DOM」這個黑色魔法其實很難理解。我記得第一次嘗試繞著身體

為協助視覺化呈現 Shadow DOM 轉譯方式,我們使用 d3.js 建構了一項工具。左側的兩個標記方塊都可以編輯。您可以把自己的標記貼入標記,隨意嘗試看看吧,看看主機節點的運作方式和插入點是將主機節點旋轉至陰影樹狀結構中。

陰影 DOM 圖表
啟動 Shadow DOM Visualizer

歡迎試用看看,並與我們分享你的想法!

事件模型

有些事件會跨越陰影界線,有些則不會。如果事件跨越邊界,系統就會調整事件目標,以維持陰影根層級上邊界的封裝。也就是說,事件會重新指定目標,讓事件看起來是來自主要元素,而不是來自 Shadow DOM 的內部元素

Play 動作 1

  • 這個答案很有趣。您應該會看到從主機元素 (<div data-host>) 到藍色節點的 mouseout。雖然這是分散式節點,但仍存在於主機中,而不是 ShadowDOM。再次將滑鼠停在黃色部分,藍色節點上就會出現 mouseout

Play 動作 2

  • 主機上會顯示一個 mouseout (在結尾處)。通常,所有黃色區塊都會觸發 mouseout 事件。但在這種情況下,這些元素都是 Shadow DOM 內部,且事件不會穿過其上邊界。

Play 動作 3

  • 請注意,當您點選輸入時,focusin 不會顯示在輸入上,而是顯示在主機節點上。又是目標網站了!

一律停止的活動

下列事件絕不會跨越陰影邊界:

  • abort
  • 錯誤
  • 選取
  • 變更
  • 重設
  • resize
  • scroll
  • selectstart

結論

希望您也同意 Shadow DOM 的功能非常強大。這是我們第一次採用適當的封裝方式,不需要額外的包包 <iframe> 或其他較舊的技術。

Shadow DOM 的確相當複雜,但對網路平台來說是值得一試的完美工具。不妨花點時間看看。學以致用。提出問題,

如要瞭解詳情,請參閱 Dominic 的簡介文章「Shadow DOM 101」和「Shadow DOM 201: CSS & Styling」一文。