Shadow DOM 301:高级概念 和 DOM API

发布于 2022-03-08 13:15:57 字数 10107 浏览 1284 评论 0

本文将讨论更多有关 Shadow DOM 应用的精彩内容!文中内容基于在 Shadow DOM 101Shadow 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>

Attaching multiple shadow trees

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>

Shadow insertion points

Light DOM

这个例子中有些有趣的地方:

  1. Root 2 FTW 依然渲染在 Root 1 FTW 上面。原因和我们放置 <shadow> 插入点的位置有关。如果你想颠倒顺序,那就移动插入点:root2.innerHTML = '<shadow></shadow><div>Root 2 FTW</div>';
  2. 注意此时在 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,尽可以使用 HTMLContentElementHTMLShadowElement 接口。

<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 DOM Visualizer

事件模型

有些事件会越过 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 按钮表示可以进行多种尝试。你可以点击它们来观察 mouseoutfocusin 事件是如何冒泡到主页面的。

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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

0 文章
0 评论
511 人气
更多

推荐作者

醉城メ夜风

文章 0 评论 0

远昼

文章 0 评论 0

平生欢

文章 0 评论 0

微凉

文章 0 评论 0

Honwey

文章 0 评论 0

qq_ikhFfg

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文