什么是 Shadow DOM?
Shadow DOM 是浏览器提供的一套原生 API,允许开发者在元素内部创建私有的 DOM 树和样式表。它主要用于实现 Web Components,使组件具有真正的封装性,避免样式和脚本冲突。
Shadow DOM 的核心概念包括:
- Shadow root:附着在宿主元素上的私有 DOM 根。
- Shadow tree:shadow root 中的 DOM 结构,对宿主以外的文档不可见。
- 光树(light DOM):宿主元素自身的内容。
- 样式封装:shadow tree 中的 CSS 不会泄漏到外部,外部样式也默认不会穿透。
创建 Shadow DOM
使用 attachShadow 方法:
1 | <my-component></my-component> |
mode 可以是 open 或 closed:
open:可以通过element.shadowRoot访问。closed:外部无法通过 API 访问,但并不是真正隐藏,只是通过标准接口不可见。
支持的浏览器
自 Chrome 及其衍生浏览器、Firefox、Safari、Edge 都支持 Shadow DOM,但在较旧版本或某些 WebView 中可能需要 polyfill。可以使用 if (Element.prototype.attachShadow) 进行检测。
样式封装细节
默认情况下,shadow tree 的样式只作用于自身。可以通过 CSS 组合选择器 ::slotted() 和 ::part() 实现与外部的有限交互。
1 | <template id="tmpl"> |
使用 slot 来投影 light DOM 内容:
1 | <my-component> |
slot 元素支持 name 属性,多插槽可用于复杂布局。
事件传播与封装
事件在 shadow tree 内部触发时会遵循正常的 DOM 事件传播,但进入宿主的过程有特殊规则:
- 事件会穿越 shadow boundary,但只会传播到宿主元素,而不会暴露内部节点。
- 可以使用
composed属性控制事件是否可穿越边界。
例子:
1 | this.shadowRoot.addEventListener('click', e => { |
只有当 composed: true 时,事件才能在 light DOM 中监听到。
深度插槽与选择器
CSS 中可以使用 :host 和 :host() 选择器来为宿主元素设置样式:
1 | :host { |
另外 :host-context() 允许基于宿主元素在外部环境的状态来应用样式。
代替 shadow DOM 原生封装,可通过 ::part 公开组件内部的特定部分,使外部样式可以作用于它们。
1 | my-component::part(button) { |
栈纵深与封装破坏
从 light DOM 访问 shadow DOM 内部只能通过 querySelector 在 shadowRoot 上执行;跨多个 shadow boundary 需要显式遍历,例如:
1 | const inner = document.querySelector('outer-component') |
注意,这种访问违反封装原则,应尽量避免。
动态创建与模板
使用 <template> 和 cloneNode 可以方便地在组件内部创建内容:
1 | <template id="card-tmpl"> |
然后在组件构造函数中:
1 | const tpl = document.getElementById('card-tmpl'); |
生命周期与自定义元素
Shadow DOM 通常与 Custom Elements 一起使用。Custom Elements 提供了回调:
connectedCallback- 元素插入文档时触发。disconnectedCallback- 元素从文档移除。attributeChangedCallback- 属性变化。adoptedCallback- 元素移动到新文档。
通过这些可以管理 shadow tree 的初始化和更新。
动态样式与 CSS Variables
CSS 变量在 shadow DOM 中可以跨边界作用:
1 | :host { |
父级定义的变量会传递给 shadow DOM,而在 shadow 中定义的变量不会传播到外部。
性能考虑
shadow DOM 的创建会带来一定的开销,尤其在大量元素时需要注意:
- 避免在短时间内频繁 attach 和 detach shadow root。
- 尽量复用 shadow DOM 实例,而不是销毁重建。
使用场景
- 封装复杂组件:日期选择器、表格、下拉菜单等。
- 第三方组件库:避免样式冲突。
- 独立可复用的 UI 模块。
常见问题
- 为什么样式不起作用? 检查是否忘记
::slotted()或使用了错误的选择器。 - 事件无法捕获? 确认事件是否设置了
composed: true。 - 无法获取 shadowRoot? 如果使用
mode: 'closed',则无法从外部访问。
调试工具
Chrome DevTools 在 Elements 面板中支持显示 shadow tree,选择元素后可以查看其 shadow roots。开发者也可以在控制台使用 $0.shadowRoot。
进阶技巧
- 混合模式:有时为了兼容,可以在某些情况下不使用 shadow DOM,而仅使用 CSS Modules 或 BEM 命名空间。
- polyfill:对旧浏览器或轻量级环境,可用
@webcomponents/shadydom做降级处理。
总结
Shadow DOM 是构建现代 Web Components 的基石。通过正确使用它可以实现强大的封装、样式隔离和可重用性。掌握其 API、事件传播规则和样式穿透机制,是开发高质量组件库的基础。尽管它并非适用于所有场景,但在需要隔离复杂 UI 时,是非常值得投入的技术。
继续探索其他 Web Components 标准,如 Custom Elements 和 HTML Templates,可以进一步提升前端架构的模块化水平。