進階概念和 DOM API
本文將進一步探討 Shadow DOM 的各種精彩功能!本課程是以「Shadow DOM 101」和「Shadow DOM 201」中討論的概念為基礎。
使用多個影子根
舉辦派對時,如果所有人都擠在同一個房間,就會感到悶熱。您希望可以將參與者分組,並分配到多個討論室。代管 Shadow DOM 的元素也可以這麼做,也就是說,它們可以一次代管多個 Shadow Root。
如果嘗試將多個 Shadow Root 附加至主機,會發生什麼情況:
<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>
雖然我們已附加 Shadow Tree,但轉譯結果仍是「Root 2 FTW」。這是因為最後新增至主機的影子樹會勝出。就轉譯而言,這是一個 LIFO 堆疊。檢查開發人員工具即可驗證這項行為。
如果只有最後一個陰影會顯示,那為什麼要使用多個陰影呢?輸入陰影插入點。
影子插入點
「影子插入點」(<shadow>) 與一般插入點 (<content>) 類似,都是預留位置。不過,它們並非主機內容的預留位置,而是其他陰影樹狀結構的主機。這就是 Shadow DOM 的全面啟動!
如您所想,越深入探究,事情就越複雜。因此,規格明確說明瞭多個 <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>
這個範例有幾個有趣之處:
- 「Root 2 FTW」仍會顯示在「Root 1 FTW」上方。這是因為我們放置
<shadow>插入點的位置。如要反向移動,請移動插入點:root2.innerHTML = '<shadow></shadow><div>Root 2 FTW</div>';。 - 請注意,root1 中現在有
<content>插入點。這會使文字節點「Light DOM」一併顯示。
<shadow> 轉譯的內容為何?
有時,瞭解在 <shadow> 轉譯的舊 Shadow Tree 很有用。您可以透過 .olderShadowRoot 取得該樹狀結構的參照:
**root2.olderShadowRoot** === root1 //true
取得主機的影子根
如果元素代管 Shadow DOM,您可以使用 .shadowRoot 存取其最新 Shadow Root:
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,HTMLContentElement 和 HTMLShadowElement 都有提供相關介面。
<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>。
使用插入點
從主體元素中選取並「分布」到 Shadow 樹狀結構中的節點,稱為…(請擊鼓)…分布節點!當插入點邀請他們進入時,他們可以跨越陰影邊界。
插入點在概念上很奇怪,因為它們不會實際移動 DOM。主機的節點會保持不變。插入點只會將節點從主機重新投影到 Shadow Tree。這與呈現/轉譯有關:「將這些節點移到這裡」「在這個位置轉譯這些節點」。
例如:
<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
Shadow DOM 就像黑魔法一樣難以理解。我還記得第一次試著理解這項技術時,
為了協助您瞭解 Shadow DOM 轉譯的運作方式,我使用 d3.js 建構了一項工具。左側的兩個標記方塊皆可編輯。你可以貼上自己的標記,並試著瞭解運作方式,以及插入點如何將主機節點切換至 Shadow Tree。
歡迎試用,並與我們分享看法!
事件模型
有些事件會跨越 Shadow DOM 邊界,有些則不會。如果事件跨越邊界,系統會調整事件目標,以維持 Shadow Root 上邊界提供的封裝。也就是說,事件會重新指定目標,看起來像是來自代管元素,而不是 Shadow DOM 的內部元素。
播放動作 1
- 這點很有意思。您應該會看到從主機元素 (
<div data-host>) 到藍色節點的mouseout。雖然是分散式節點,但仍在主機中,而非 ShadowDOM。將滑鼠游標進一步向下移至黃色區域,會再次在藍色節點上觸發mouseout。
播放動作 2
- 主機上會顯示一個
mouseout(在最尾端)。一般來說,所有黃色區塊都會觸發mouseout事件。不過,在這種情況下,這些元素位於 Shadow DOM 內部,事件不會向上傳遞至其上邊界。
播放動作 3
- 請注意,當您點選輸入時,
focusin不會顯示在輸入上,而是顯示在主機節點本身。已重新指定目標!
一律停止的事件
下列事件一律不會跨越 Shadow DOM 邊界:
- abort
- 錯誤
- 選取
- 變更
- load
- 重設
- resize
- scroll
- selectstart
結論
希望您也同意 Shadow DOM 非常強大。我們首次採用適當的封裝技術,不需額外使用 <iframe> 或其他舊技術。
Shadow DOM 確實是複雜的野獸,但值得加入網路平台。花點時間熟悉這項工具。輕鬆掌握。提出問題。
如要瞭解詳情,請參閱 Dominic 的簡介文章「Shadow DOM 101」和我的「Shadow DOM 201:CSS 和樣式」一文。