Shadow DOM 301:高级概念 和 DOM API
本文将讨论更多有关 Shadow DOM 应用的精彩内容!文中内容基于在 Shadow DOM 101 和 Shadow DOM 201 中讨论的概念。
在 Chrome 中,开启 about:flags 页面中的 Enable experimental Web Platform features 就可以体验本文介绍的所有内容。
使用多个 shadow root
假如你举办了一场聚会,要是把所有人都聚集在同一间屋子里会显得拥挤不堪。你希望能将人们按组分散到不同的房间内。托管 Shadow DOM 的元素也具有这样的能力,也就是说,宿主元素能够在同一时间内托管多个 shadow root。
让我们来试着将多个 shadow root 托管到同一个宿主元素里会发生什么:
<div>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>
Light DOM
在开发者工具中,开启 Show Shadow DOM 以便观察 ShadowRoots。
尽管我们已经为宿主元素附加上了一个 shadow 树,但最终显示的内容却是 "Root 2 FTW"。这是因为最后被加入到宿主元素中的 shadow 树获胜。就渲染而言,它就像后进先出(LIFO)的栈一样。可以检查开发者工具来验证这一行为。
添加进宿主元素中的 shadow 树按照它们的添加顺序而堆叠起来,从最先加入的 shadow 树开始。最终渲染的是最后加入的 shadow 树。
最近添加的树称为 younger tree。之前添加的树称为 older tree。在本例中,
root2
是 younger tree,root1
是 older tree。
如果只有最后加入的 shadow 树才能被渲染,那么使用多个 shadow 的意义何在?别着急,让我们来认识下 shadow 插入点(insertion points)。
Shadow 插入点
Shadow 插入点(<shadow>
)与普通 插入点(<content>
)均为占位符。不过,相比作为宿主内容的占位符,它们算得上是其他 shadow 树的宿主。它是 Shadow DOM 的基石!
正如你想象得到的,在兔子洞( rabbit hole)中陷的越深,事情变得越复杂。有鉴于此,规范对于同时存在多个 <shadow>
的行为作了明确的解释:
如果一个 shadow 树中存在多个 <shadow>
插入点,那么仅第一个被确认,其余的被忽略。
回过头来看看之前的例子,第一个 shadow root1
不在邀请名单中。增加一个 <shadow>
插入点来把它召回:
<div>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>
Light DOM
这个例子中有些有趣的地方:
- Root 2 FTW 依然渲染在 Root 1 FTW 上面。原因和我们放置
<shadow>
插入点的位置有关。如果你想颠倒顺序,那就移动插入点:root2.innerHTML = '<shadow></shadow><div>Root 2 FTW</div>';
。 - 注意此时在 root1 中存在一个
<content>
插入点。正因为如此,文本节点 "Light DOM" 也一并显示出来。
<shadow> 里究竟渲染了什么?
有些时候,了解一个 <shadow>
中渲染的旧的 shadow 树很有用。你可以通过 .olderShadowRoot
获取到那棵树的引用:
root2.olderShadowRoot === root1 //true
获取宿主元素的 shadow root
如果一个元素托管着 Shadow DOM,你可以使用 .shadowRoot
来访问它的 youngest shadow root:
var root = host.createShadowRoot(); console.log(host.shadowRoot === root); // true console.log(document.body.shadowRoot); // null
如果不想别人乱动你的 shadow,那就将 .shadowRoot
重定义为 null:
Object.defineProperty(host, 'shadowRoot', { get: function() { return null; }, set: function(value) { } });
有点取巧,但是很有效。最后要牢记的是,虽然 Shadow DOM 很棒,但它并没有被设计成一个安全特性。不要想着依赖它来实现完整的内容隔离。
在 JS 中构建 Shadow DOM
如果你偏好在 JS 中构建 DOM,尽可以使用 HTMLContentElement
和 HTMLShadowElement
接口。
<div> <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 树中的节点称为……鼓声响起……分布式节点!当插入点邀请它们时便可以越过 shadow 边界。
从概念上讲很奇怪的是,插入点并不会真正的移动 DOM。宿主元素的节点保持不动。插入点仅仅是将节点从宿主元素重新投射(re-project)到 shadow 树中。这是展现/渲染层面的事情:"把节点移动到这" "把节点渲染在这个位置。"
你无法遍历 <content>
中的 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 的子节点。这便引出了另一个结论:
插入点的功能极其强大。把它想象成一个为你的 Shadow DOM 创建"声明式 API" 的方法。宿主元素可以包含任意标记,但除非我使用插入点将它们引入到 Shadow DOM 中,否则它们毫无意义。
Element.getDistributedNodes()
我们虽然无法遍历 <content>
,但 .getDistributedNodes()
API 却允许我们查询一个插入点的分布式节点:
<div> <h2>Eric</h2> <h2>Bidelman</h2> <div>Digital Jedi</div> <h3>footer text</h3> </div> <template> <header> <content select="h2"></content> </header> <section> <content select="div"></content> </section> <footer> <content select="h3: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>
<h2>Light DOM</h2>
</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 树中。
事件模型
有些事件会越过 shadow 边界,有些不会。在越过 shadow 边界的情况中,事件目标会因为维护由 shadow root 上边界提供的封装而进行调整。也就是说,事件会被重定向,使它看起来是从宿主元素上发出,而并非是 Shadow DOM 的内部元素。
访问 event.path
来查看调整后的事件路径。
如果你的浏览器支持 Shadow DOM (它不支持),你应该能在下方看到一个用于可视化事件的测试区。黄色的元素属于 Shadow DOM 中的标记。蓝色的元素属于宿主元素。环绕在 "I'm a node in the host" 的黄色边框表明了它是一个分布式节点,通过 shadow 的 <content>
插入点混入在 Shadow DOM 中。
Play Action 按钮表示可以进行多种尝试。你可以点击它们来观察 mouseout
和 focusin
事件是如何冒泡到主页面的。
Play Action 1
- 这个很有意思。你会看到一个
mouseout
事件从宿主元素 (<div data-host>
)
传递到蓝色的节点。即便它是个分布式节点,但它始终处于宿主中,而不是在 Shadow DOM 里。随后继续移动鼠标至黄色区域内,再次导致蓝色的节点触发mouseout
事件。
Play Action 2
- 这是发生在宿主元素(发生的非常晚)上的一次
mouseout
事件。通常你会看到mouseout
事件会在所有的黄色块上触发。但是这一次不同,这些元素都在 Shadow DOM 的内部,事件的冒泡不会超出它的上边界。
Play Action 3
- 注意当你点击输入框时,
focusin
并没有发生在输入框上,而是在 - 点自身上。事件被重定向了!
始终停止的事件
以下事件永远无法越过 shadow 边界:
- abort
- error
- select
- change
- load
- reset
- resize
- scroll
- selectstart
总结
我希望你能认同 Shadow DOM 的功能令人难以置信的强大。这是有史以来第一次,我们有了合适的封装,不必再使用问题重重的 <iframe>
或其他古老的技巧。
Shadow DOM 是个难以驯服的猛兽,但是它却值得被加入到 web 平台中。花点时间去了解它,学习它,提出问题。
如果你想学习更多内容,看看 Dominic 的入门文章 Shadow DOM 101 和我的 Shadow DOM 201: CSS & Styling。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论