高级概念和 DOM API
本文将介绍更多可使用 Shadow DOM 实现的强大功能!本课程以 Shadow DOM 101 和 Shadow 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>
此示例中有一些有趣的现象:
- “Root 2 FTW”仍会在“Root 1 FTW”上方呈现。这是因为我们放置
<shadow>
插入点的位置。如果您想反向移动,请移动插入点:root2.innerHTML = '<shadow></shadow><div>Root 2 FTW</div>';
。 - 请注意,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
重新定义为 null:
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。主机的节点保持不变。插入点只是将节点从宿主重新投影到阴影树。这是呈现/渲染方面的问题:“将这些节点移到此处”“在此位置渲染这些节点”。
例如:
<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 可视化工具
了解 Shadow DOM 的黑魔法很难。我记得第一次尝试理解它时,脑子里一片混乱。
为了直观地展示 Shadow DOM 渲染的工作原理,我使用 d3.js 构建了一个工具。左侧的两个标记框均可修改。您可以随意粘贴自己的标记,并进行调试,了解其运作方式以及插入点如何将主机节点切换到阴影树。
欢迎试用,并告诉我您的想法!
事件模型
有些事件会跨越阴影边界,有些则不会。在事件跨越边界的情况下,系统会调整事件目标,以维持影子根的边界上限提供的封装。也就是说,事件的目标重新进行了设定,因此这些事件看起来像是来自宿主元素,而不是来自 Shadow DOM 的内部元素。
Play Action 1
- 这个问题很有趣。您应该会看到从主机元素 (
<div data-host>
) 到蓝色节点的mouseout
。虽然它是一个分布式节点,但仍位于宿主中,而不是 ShadowDOM 中。将鼠标进一步向下移动到黄色区域后,蓝色节点上会出现mouseout
。
Play 操作 2
- 主机上会显示一个
mouseout
(位于最后)。通常,您会看到系统针对所有黄色块触发mouseout
事件。不过,在本例中,这些元素是 shadow DOM 的内部元素,并且事件不会通过其上边界冒泡。
播放操作 3
- 请注意,当您点击输入时,
focusin
不会显示在输入上,而是显示在主机节点本身上。已重新定位!
始终停止的事件
以下事件绝不会跨越阴影边界:
- abort
- 错误
- 选择
- 更改
- 负荷
- 重置
- resize
- scroll
- selectstart
总结
希望您同意 Shadow DOM 非常强大。我们首次实现了适当的封装,而无需使用 <iframe>
或其他旧版技术。
Shadow DOM 无疑是一个复杂的巨兽,但它值得添加到 Web 平台中。 请花一些时间来研究。了解它。踊跃提问。
如需了解详情,请参阅 Dominic 的入门文章 Shadow DOM 101 和我的 Shadow DOM 201:CSS 和样式一文。