神奇的 Shadow DOM

发布于 2025-05-14 08:33:15 字数 10682 浏览 6 评论 0

你有好奇过这个问题吗,为什么只用 video 标签包裹着 source 标签,就可以完成一系列视频功能:播放/暂停按钮、进度条、视频时间显示、音量控制等等?既然 DOM 源码这么干净,你有想过实现这些组件的代码是从哪儿来的吗?

1. 简介

Shadow DOM 它允许在文档(document)渲染时插入一棵 DOM 元素子树,但是这棵子树不在主 DOM 树中。

因此开发者可利用 Shadow DOM 封装自己的 HTML 标签、CSS 样式和 JavaScript 代码。

看一个简单的 video:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Shadow DOM</title>
</head>
<body>

<video controls autoplay name="media" width="500">
    <source id="mp4" src="http://7ryl2t.com2.z0.glb.qiniucdn.com/572ffc37a2e5a.mp4" type="video/mp4">
</video>

</body>
</html>

页面完成了,在浏览器 chrome 中打开,然后打开 Chrome 的开发者工具,点击右上角的“Settings”按钮,勾选“Show user agent shadow DOM”。

Show user agent shadow DOM

浏览器截图:

Video Shadow DOM

shadow-root 称为影子根,可以看到它在 video 里面,换句话说,#shadow-root 寄生在 video 上,所以 video 此时称为影子宿主。可以看到上图有两个#shadow-root,这是因为#shadow-root 可以嵌套,形成节点树,即称为影子树(shadow trees)。影子树对其中的内容进行了封装,有选择性的进行渲染。这就意味着我们可以插入文本、重新安排内容、添加样式等等。如下所示:

影子树

2. 怎样创建 Shadow DOM

使用 createShadowRoot() 来创建 Shadow DOM,并赋值给一个变量,然后添加元素给变量即可。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Shadow DOM</title>
    <style type="text/css">
        .shadowroot_son {
            color: #f00;
        }
    </style>
</head>
<body>
    <div class="shadowhost">Hello, world!</div>
    <script>

        // 影子宿主(shadow host)
        var shadowHost = document.querySelector('.shadowhost');

        // 创建影子根(shadow root)
        var shadowRoot = shadowHost.createShadowRoot();

        // 影子根作为影子树的第一个节点,其他的节点比如 p 节点都是它的子节点。
        shadowRoot.innerHTML = '<p class="shadowroot_son">夏天夏天悄悄过去留下小秘密!</p>';

    </script>
</body>
</html>

浏览器截图:

创建 Shadow DOM

有没有注意到.shadowroot_son 的样式 color: #f00;不生效?!那是因为影子宿主和影子根之间存在影子边界(shadow boundary),影子边界保证主 DOM 写的 CSS 选择器和 JavaScript 代码都不会影响到 Shadow DOM,当然也保护主文档不受 shadow DOM 样式的侵袭。

3. 想要渲染影子宿主里的内容,那该怎么玩?

需要完成此项任务,需要两个利器: <content><template>

3.1 <content>

通过 <content> 标签把来自主文档并添加到 shadow DOM 的内容被称为分布节点。

<content> 的 select 属性告诉 <content> 标签有选择性的插入内容。select 属性使用 CSS 选择器来选取想要展示的内容,选择器包括类选择器、元素选择器等。

3.2 <template>

目前的模板 HTML 做法通常是在 <script> 中嵌入模板 HTML,让内部的 HTML 标签按照字符串处理的,从而使得内容不显示:

<script type="text/template">
// ...
</script>

<template> 元素的出现旨在让 HTML 模板变得更加标准与规范。

<template> 在使用前不会被渲染,不会执行加载等操作,也能够实现隐藏标签内容,而且位置任意性,可以在 <head> 中,也可以在 <body> 或者 <frameset> 中。

3.3 实例

通过以上对 <content><template> 的简单了解,我们来通过一个实例加深理解:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>content&template</title>
</head>

<body>

    <div class="shadowhost">
        <em class="shadowhost_content1">唱歌</em>
        <em class="shadowhost_content2">跳舞</em>
    </div>

    <!-- S 模板标签 template -->
    <template class="template">
        <h1>你<content select=".shadowhost_content1"></content>我<content select=".shadowhost_content2"></content>!</h1>
    </template>
    <!-- E 模板标签 template -->

    <script>
    var shadowHost = document.querySelector('.shadowhost');

    var shadowRoot = shadowHost.createShadowRoot();
    var template = document.querySelector('.template');

    // template.content 会返回一个文档片段,可以理解为另外一个 document。
    // 利用 document.importNode 获取节点,true 表示深度克隆。
    shadowRoot.appendChild(document.importNode(template.content, true));
    </script>

</body>

</html>

浏览器截图:

content&template

我们来看一下下面三个属性的用途:

console.log(template.innerHTML);   // 获取完整的 HTML 片段
console.log(template.content);  // 返回一个文档片段#document-fragment
console.log(template.childNodes);  // 返回[],说明 childNodes 无效

贪心插入点 :如果把 select=".shadowhost_content1" ​改成 select="" ​或者 select="*" ​,那么会有不一样的结果。因为贪心选择器放在了模板的第一个,他会将所有内容都抓取,不给其他 select 选择器留一点内容。浏览器截图如下:

贪心插入点

4. 关于样式

4.1 宿主样式 :host

在 shadow DOM 中利用:host 定义宿主的样式,当然用户可以在主文档中覆盖这个样式。

:host 是伪类选择器(Pseudo Selector),:host 或者 :host(*) 是默认给所有的宿主添加样式,或者单独给一个宿主添加样式,即通过:host(x),x 可以是宿主的标签或者类选择器等。

另外:host 还可以配合:hover、:active 等状态来设置样式,如:

:host(:hover) {
    border: 2px solid #0ff;
}

4.2 ::shadow

原则上来说,影子边界保证主 DOM 写的 CSS 选择器和 JavaScript 代码都不会影响到 Shadow DOM。 但你可能会想打破影子边界的所谓保证,主文档能够给 Shadow DOM 添加一些样式,这时可以使用::shadow。

4.3 /deep/

::shadow 选择器的一个缺陷是他只能穿透一层影子边界,如果你在一个影子树中嵌套了多个影子树,那么使用 /deep/ 。

4.4 ::content

还记得什么叫分布节点吗?通过 <content> 标签把来自主文档并添加到 shadow DOM 的内容被称为分布节点。

分布节点的样式渲染需要用到 ::content。即使分布节点为 em 标签,直接写 em {} 不生效,应该写成::content > em {}。

4.5 实例

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>::content&::shadow&/deep/</title>
    <style type="text/css">
    /* ::shadow */
    /*.shadowhost::shadow h1 {
        padding: 20px;
        border: 1px solid #f00;
    }*/

    /* /deep/  */
    .shadowhost /deep/ h1 {
        padding: 20px;
        border: 1px solid #000;
    }
    </style>
</head>

<body>
    <div class="shadowhost">
        <em class="shadowhost_content1">唱歌</em>
        <em class="shadowhost_content2">跳舞</em>
    </div>

    <!-- S 模板标签 template -->
    <template class="template">
        <style>
        /* 定义宿主样式:host */
        :host {
            color: #E85E5E;
        }
        /* 定义宿主 hover 状态下的样式 */
        :host(:hover) {
            color: #000;
        }

        /* 分布节点的样式渲染需要用到 ::content,直接写 em {} 不生效 */
        ::content > em {
            padding: 10px;
            color: #fff;
            background: #FFCC00;
            border-radius: 10px;
        }
        </style>
        <h1>你<content select=".shadowhost_content1"></content>我<content select=".shadowhost_content2"></content>!</h1>
    </template>
    <!-- E 模板标签 template -->

    <script>
    var shadowHost = document.querySelector('.shadowhost');

    var shadowRoot = shadowHost.createShadowRoot();
    var template = document.querySelector('.template');

    shadowRoot.appendChild(document.importNode(template.content, true));
    </script>

</body>

</html>

浏览器截图如下:

::content&::shadow&/deep/

5. JavaScript

5.1 重定向

Shadow DOM 里的 JS 与传统的 JS 一个真正不同的点在于事件调度(event dispatching)。要记住的一点是:原来绑定在 shadow DOM 节点中的事件被重定向了,所以他们看起来像绑定在影子宿主上一样。

当你点击“shadow text”的输入框时控制台却输出了宿主元素(就是 #host)的 id 。这是因为影子节点上的事件必须重定向,否则这将破坏封装性。

分布节点来自原有 DOM 结构,没必要重定向。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>select</title>
</head>
<body>
  <input id="normal-text" type="text" value="I'm normal text">

  <div id="host">
    <!-- “dustributed text”为分布节点,来自原有 DOM 结构,没必要重定向。 -->
    <input id="distributed-text" type="text" value="I'm distributed text">
  </div>

  <template>
    <div>
      <input id="shadow-text" type="text" value="I'm shadow text">
    </div>
    <div>
      <content></content>
    </div>
  </template>

  <script>
    var host = document.querySelector('#host');
    var root = host.createShadowRoot();
    var template = document.querySelector('template');
    root.appendChild(document.importNode(template.content, true));

    document.addEventListener('click', function(e) {
      console.log(e.target.id + ' click!');
    });
  </script>
</body>

</html>

分别单击每个输入框,控制台打印截图如下:

事件重定向

5.2 被阻塞的事件(Blocked Events)

事件 abort、 error、 select 、change 、load 、reset 、resize 、scroll 、selectstart 不会进行重定向而是直接被干掉,因此事件不能冒泡到文档中,事件监听重定向至文档,因此无法监听到这一事件。

把上面的监听事件 click 改成 select,即改成:

document.addEventListener('select', function(e) {
    console.log(e.target.id + ' select!');
});

分别双击每个输入框,你会发现,shadow text 的输入框没有打印,就是没有发生 select 事件。

被阻塞的事件

6. 兼容性

template 兼容性

Shadow DOM 兼容性

看上去只能在 chrome 中愉快地玩耍。webcomponents.js 使得 Shadow DOM 在非 native 支持的浏览器上实现。

7. 参考链接

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

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

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。