Shadow DOM

6 阅读4分钟

核心概念

Shadow DOM 是 Web 组件标准的一部分,它主要解决了一个问题:将一个组件的 HTML 结构、CSS 样式与主文档隔离开来,使其不会受到外部样式的影响,也不会影响到外部文档。

你可以把它想象成一个**“样式作用域”“封装罩”**。

解决的问题

在没有 Shadow DOM 的时代,我们经常会遇到这样的麻烦:

  1. 样式冲突:你写了一个复杂的组件(例如一个标签页组件),用了类名 .tab.active。但如果页面其他地方也有一个同名的 CSS 类,你的组件样式就会被意外修改,页面变得一团糟。
  2. 结构暴露:你的组件 HTML 结构完全暴露在全局文档中,如果其他脚本不小心用 document.querySelector 选择了你组件内部的元素,可能会引发难以调试的 Bug。
  3. 缺乏封装性:很难编写一个可以“即插即用”、自包含的独立组件。

Shadow DOM 就是为了实现真正的“封装”而生的。

Shadow DOM 的关键术语和关系

要理解 Shadow DOM,需要先了解几个关键概念:

  • Light DOM: 这是指我们平常写的、在全局文档流中的普通 DOM 节点。也就是“外部”。
  • Shadow Host: 一个普通的 DOM 节点,它是 Shadow DOM 的“挂载点”或“容器”。
  • Shadow Tree: 挂在 Shadow Host 内部的、被封装起来的 DOM 树。这就是 Shadow DOM 本身。
  • Shadow Root: Shadow Tree 的根节点。我们可以通过它来操作 Shadow Tree 内部的内容。

它们的关系可以用一个简单的比喻来理解:

  • Shadow Host 就像一个带插座的电源墙插
  • Shadow Root 就是墙插上的插座口
  • Shadow Tree 就是插在插座上的一个黑盒子电器(比如电视机)。
  • 墙插(Host)和外部电路(Light DOM)是连通的,但电视机内部(Shadow Tree)的复杂电路和元件是被外壳封装起来的,外部世界无法直接触碰。

如何创建和使用 Shadow DOM

HTML 结构(Light DOM):

<!DOCTYPE html>
<html>
<body>
    <!-- 这是一个普通的 div,它将作为 Shadow Host -->
    <div id="my-shadow-host"></div>

    <script>
        // 1. 先找到 Shadow Host
        const hostElement = document.getElementById('my-shadow-host');

        // 2. 为这个 Host 附加一个 Shadow Root(打开封装模式)
        // mode: 'open' 表示可以从外部通过 JavaScript 访问 Shadow Root
        // mode: 'closed' 则完全封闭,外部无法访问(很少用)
        const shadowRoot = hostElement.attachShadow({ mode: 'open' });

        // 3. 向 Shadow Root 内部添加内容和样式(这就是 Shadow Tree)
        shadowRoot.innerHTML = `
            <style>
                /* 这个 p 标签的样式只在这个 Shadow Tree 内部生效! */
                p {
                    color: red;
                    font-weight: bold;
                }
                /* 外部的 CSS 无法覆盖这个规则 */
            </style>
            <p>这段文字在 Shadow DOM 内部,是红色的粗体。</p>
        `;
    </script>

    <!-- 这是 Light DOM 里的一个普通 p 标签,不受上面样式影响 -->
    <p>这段文字在 Light DOM 内部,保持默认样式。</p>
</body>
</html>

运行结果: 页面上会显示两段文字:

  1. 第一段是红色的粗体(在 Shadow DOM 内部)。
  2. 第二段是普通的默认样式(在 Light DOM 内部)。

即使你在外部 CSS 中写 p { color: blue !important; },也无法将第一段文字变成蓝色! 这就是样式的封装性。

总结

浏览器原生技术,它通过创建独立的 DOM 子树和样式作用域,解决了样式和脚本的全局污染问题

  1. 隔离的 DOM: Shadow Tree 内部的节点不会在全局的 document.querySelector(‘p’) 中被找到。你必须通过 Shadow Root 来查询,例如 shadowRoot.querySelector(‘p’)
  2. 作用域 CSS: Shadow Tree 内部定义的 CSS 只作用于内部。外部的 CSS 规则(除了少数继承性样式,如 color, font-family,或使用 ::part 等特殊伪类)也无法影响内部。
  3. 简化 CSS: 因为样式被隔离了,你可以在 Shadow DOM 内部使用非常简单的选择器(如 button, .class),而不用担心和全局样式冲突。

应用场景

Shadow DOM 最主要的应用就是 Web Components

现代前端框架(如 Vue、React、Angular)在底层也利用了类似的封装思想来构建组件。很多浏览器原生组件也是用 Shadow DOM 实现的,例如:

  • <video> 元素:它内部有复杂的播放/暂停按钮、进度条等控件,但这些结构对你是隐藏的,这就是通过 Shadow DOM 封装的。
  • <input type="range">:滑动条也是用 Shadow DOM 实现的。

你可以打开浏览器的开发者工具,在 Settings > Preferences > Elements 中勾选 “Show user agent shadow DOM”,然后检查 <video><input type=“range”> 元素,就能看到其内部被隐藏的 Shadow DOM 结构了。