高级概念和DOM API
本文更详细地介绍了 Shadow DOM 可实现的诸多出色功能!它以 Shadow DOM 101 中讨论的概念为基础 和 Shadow DOM 201。
使用多个影子根
在举办派对时,如果所有人都挤在同一个房间内,可能会觉得很拥挤。 您想要将人员分布在多个房间中的选项。元素托管 Shadow DOM 也可以做到这一点,也就是说,它们可以托管多个阴影 同时处理 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>
尽管我们已经附加了影子树,但渲染的是“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、HTMLContentElement
和 HTMLShadowElement
构建 DOM
并提供了相应接口
<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
理解 Shadow DOM 的魔法并非易事。我记得我试过 我第一次把头缠住。
为了帮助直观了解 Shadow DOM 渲染的工作原理,我构建了一个 使用 d3.js。左侧的两个标记框都是 可修改。您可以随意粘贴自己的标记并到处浏览一下,看看 工作和插入点将主机节点调配到影子树中。
<ph type="x-smartling-placeholder">试用一下此功能,然后告诉我你的想法!
事件模型
有些事件会跨越影子边界,有些则没有。在事件发生时 系统就会调整事件目标 封装。也就是说,事件 进行重定向,使广告看起来像是来自主元素,而不是内部元素 元素添加到 Shadow DOM。
Play 操作 1
- 这题很有趣。您应该会看到主元素 (
<div data-host>
) 中的mouseout
蓝色节点尽管它是一个分布式计算服务 它仍然位于主机中,而不是 ShadowDOM。将鼠标移至 黄色再次会导致在蓝色节点上产生mouseout
。
Play 操作 2
- 主机上(最末尾)出现了一个
mouseout
。通常,您会 请查看所有黄色区块的mouseout
事件触发器。 不过,在这种情况下,这些元素是 Shadow DOM 内部元素, 不会冒过其上边界。
Play 操作 3
- 请注意,点击输入时,
focusin
不会出现在 而是在主机节点本身上运行。它被重新定位了!
始终停止的活动
以下事件永远不会跨越阴影边界:
- abort
- 错误
- select
- 更改
- 负荷
- 重置
- resize
- scroll
- 选择开始
总结
希望您能认同,Shadow DOM 非常强大。我们有史以来第一次能够进行适当的封装,而没有额外的 <iframe>
或其他旧技术。
Shadow DOM 无疑是复杂的巨兽,但它无疑值得添加到网络平台中。 请花些时间查看。了解详情。踊跃提问。
如需了解更多信息,请参阅 Dominic 的介绍文章 Shadow DOM 101 还有我的 Shadow DOM 201:CSS 和样式设置一文。