什么是 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <my-component > </my-component > <script > class MyComponent extends HTMLElement { constructor ( ) { super (); const shadow = this .attachShadow ({ mode : 'open' }); shadow.innerHTML = ` <style> .title { color: blue; } </style> <div class="title">Hello Shadow</div> ` ; } } customElements.define ('my-component' , MyComponent ); </script >
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 2 3 4 5 6 7 8 9 10 11 12 13 <template id ="tmpl" > <style > ::slotted (span ) { color : red; } ::part (button ) { background : green; } </style > <slot name ="foo" > </slot > <button part ="button" > Click</button > </template >
使用 slot 来投影 light DOM 内容:
1 2 3 <my-component > <span slot ="foo" > 插槽内容</span > </my-component >
slot 元素支持 name 属性,多插槽可用于复杂布局。
事件传播与封装
事件在 shadow tree 内部触发时会遵循正常的 DOM 事件传播,但进入宿主的过程有特殊规则:
事件会穿越 shadow boundary,但只会传播到宿主元素,而不会暴露内部节点。
可以使用 composed 属性控制事件是否可穿越边界。
例子:
1 2 3 4 5 6 7 8 this .shadowRoot .addEventListener ('click' , e => { console .log ('shadow click' ); }); this .dispatchEvent (new CustomEvent ('my-event' , { bubbles : true , composed : true , }));
只有当 composed: true 时,事件才能在 light DOM 中监听到。
深度插槽与选择器
CSS 中可以使用 :host 和 :host() 选择器来为宿主元素设置样式:
1 2 3 4 5 6 7 :host { display : block; } :host ([hidden] ) { display : none; }
另外 :host-context() 允许基于宿主元素在外部环境的状态来应用样式。
代替 shadow DOM 原生封装,可通过 ::part 公开组件内部的特定部分,使外部样式可以作用于它们。
1 2 3 my-component::part (button ) { font-size : 20px ; }
栈纵深与封装破坏
从 light DOM 访问 shadow DOM 内部只能通过 querySelector 在 shadowRoot 上执行;跨多个 shadow boundary 需要显式遍历,例如:
1 2 3 const inner = document .querySelector ('outer-component' ) .shadowRoot .querySelector ('inner-component' ) .shadowRoot .querySelector ('.foo' );
注意,这种访问违反封装原则,应尽量避免。
动态创建与模板
使用 <template> 和 cloneNode 可以方便地在组件内部创建内容:
1 2 3 4 5 6 <template id ="card-tmpl" > <style > </style > <div class ="card" > <slot > </slot > </div > </template >
然后在组件构造函数中:
1 2 3 const tpl = document .getElementById ('card-tmpl' );const shadow = this .attachShadow ({ mode : 'open' });shadow.appendChild (tpl.content .cloneNode (true ));
生命周期与自定义元素
Shadow DOM 通常与 Custom Elements 一起使用。Custom Elements 提供了回调:
connectedCallback - 元素插入文档时触发。
disconnectedCallback - 元素从文档移除。
attributeChangedCallback - 属性变化。
adoptedCallback - 元素移动到新文档。
通过这些可以管理 shadow tree 的初始化和更新。
动态样式与 CSS Variables
CSS 变量在 shadow DOM 中可以跨边界作用:
1 2 3 4 5 6 7 :host { --main-color : blue; } .slot-content { color : var (--main-color); }
父级定义的变量会传递给 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,可以进一步提升前端架构的模块化水平。