CSS-深度解析影子DOM:Web 组件化的“隐身术”

0 阅读3分钟

前言

在大型项目开发中,最头疼的莫过于“样式污染”。你在 A 组件写了一个 .title,结果 B 组件的标题也变色了。Shadow DOM 正是为了解决这个问题而生的,它允许我们将隐藏的 DOM 树附加到常规的 DOM 树中,实现真正的样式隔离和组件封装。

一、 Shadow DOM 的核心概念

Shadow DOM 允许你在文档中创建一个“隔离区”。在这个区域内,所有的 CSS 样式和 DOM 结构都是私有的。

1. 四个关键词

  • Shadow Host(影子宿主) :影子 DOM 附加到的常规 DOM 节点(如示例中的 div)。

  • Shadow Root(影子根) :影子树的根节点。

  • Shadow Tree(影子树) :影子 DOM 内部的 DOM 树。

  • Mode(模式)

    • open:可以通过 JavaScript 的 host.shadowRoot 访问内部。
    • closed:禁止从外部访问影子根。

2. 基础用法

const host = document.createElement("div");
// 1. 创建影子根
const shadow = host.attachShadow({ mode: "open" });

// 2. 编写内部结构与样式
shadow.innerHTML = `
  <style>
    p { color: red; } /* 这里的样式不会影响外部的 p 标签 */
  </style>
  <p>我是被隔离的内容</p>
`;

document.body.appendChild(host);

二、 巧妙使用插槽 (Slot)

当你为一个元素附加 Shadow DOM 时,浏览器会优先渲染影子树,原本该元素内部的内容会被“吞掉”。为了保留并控制这些内容的显示位置,我们需要使用 <slot>

1. 具名插槽 (Named Slots)

通过 name 属性,我们可以精确控制原内容渲染到影子树的哪个位置。

// 1. 原生 DOM 结构
document.body.innerHTML = `
  <div id="parent"> 
    <span slot="title">标题内容</span> 
    <span slot="desc">描述文字</span> 
  </div>`;

// 2. 附加 Shadow DOM 并使用插槽
document.querySelector("#parent").attachShadow({ mode: "open" }).innerHTML = `
  <div class="card">
    <h2><slot name="title"></slot></h2>
    <p><slot name="desc"></slot></p>
  </div>
`;

三、 影子 DOM 的事件冒泡

这是开发者最容易产生误解的地方。

1. 事件重定向

当 Shadow DOM 内部触发事件(如 click)并冒泡到外部时,为了维持封装性,浏览器会重定向事件源

  • 从外部看,事件的 target 会被设置为 影子宿主(Shadow Host) ,而不是内部具体的按钮或文字。

2. 穿透边界

如果你确实需要知道是影子内部哪个元素触发的,可以使用 event.composedPath(),它会返回一个数组,包含事件流经的所有对象(包括影子内部节点)。

host.addEventListener('click', (e) => {
  console.log(e.target); // 打印的是 host 元素
  console.log(e.composedPath()); // 打印的是完整的事件路径,包含内部节点
});

四、 为什么需要 Shadow DOM?

  1. 样式隔离:内部样式不溢出,外部样式(除非继承属性)不进入。
  2. DOM 封装:外界无法通过 document.querySelector 轻易获取组件内部细节。
  3. 代码复用:是构建跨框架组件(Web Components)的基石。

五、 面试模拟题

Q1:Shadow DOM 里的样式会继承外部样式吗?

参考回答: 大多数样式(如 background, width, border)是不会进入影子内部的。但具有继承性的 CSS 属性(如 color, font-family, line-height)依然会穿透影子边界,影响内部元素。

Q2:mode: "closed" 真的绝对安全吗?

参考回答: 不是绝对的。虽然无法通过 element.shadowRoot 访问,但开发者仍可以通过重写 Element.prototype.attachShadow 等 Hack 手段在根节点创建时拦截引用。在大多数生产场景下,建议使用 open 模式以方便调试。

Q3:Shadow DOM 与 Vue/React 的 Scoped CSS 有什么区别?

参考回答:

  • Vue/React Scoped:是通过增加 Data 属性(如 [data-v-xxx])和 CSS 选择器实现的模拟隔离,DOM 依然在主文档树中。
  • Shadow DOM:是浏览器原生层面的物理隔离,创建了独立的 DOM 树,隔离效果更彻底。