Skip to main content

Web Components 与 Shadow DOM

Web Components 是浏览器原生的组件方案:自定义元素 + Shadow DOM + 模板。它最大的价值是跨框架复用与强样式隔离。

1. 自定义元素、Shadow DOM、模板​

1.1 Custom Elements​

注册自定义标签并管理生命周期。

1.2 Shadow DOM​

给组件一个独立的 DOM 与样式作用域;

  • HTML template + slot:声明结构模板并把外部内容分发到指定位置。
class ColorBadge extends HTMLElement {
constructor() {
super()
// constructor 里不能操作子元素,只能先建 shadow
this.attachShadow({ mode: 'open' })
}
connectedCallback() {
this.shadowRoot.innerHTML =
'<style>:host{color:#2563eb}</style><slot></slot>'
}
}
customElements.define('color-badge', ColorBadge)

生命周期顺序是 constructor → attributeChangedCallback → connectedCallback:在 constructor 里只能建 shadow、不能读或操作子节点,真正操作要等到 connectedCallback。

2. 样式隔离的两面性​

外部样式进不去、内部样式出不来——这一条既是对组件作者的保护,也是对使用者的限制。它带来的直接后果是主题定制不能靠「覆盖选择器」,只能走两条通道:CSS 自定义属性(变量会穿透 Shadow 边界继承进去)与 ::part()(组件作者显式暴露的可定制部分)。理解这一点就理解了为什么 Web Components 组件库的 API 设计里,part 与变量声明是头等大事。

/* 通道一:CSS 变量穿透 */
color-badge { --badge-color: #dc2626; }

/* 通道二:::part 定制显式暴露的部分 */
color-badge::part(dot) {
width: 12px;
height: 12px;
}

mode: 'closed' 只挡住 JS 通过 shadowRoot 访问,不构成任何安全边界,敏感内容不能放进去。它隔离的是选择器与继承,字体、CSS 变量、可继承属性仍会穿透。

边界两侧:什么能过去,什么过不去外层文档(Light DOM)全局选择器 .btn 选不中内部节点外部的 !important 也进不去Shadow Tree内部样式不外泄,不会污染全局内部选择器也出不来能穿过去:CSS 自定义属性(继承) · ::part() 显式暴露的节点 · composed 事件mode: closed 只挡住 JS 拿 shadowRoot,上面几条通道一条都不拦——它不是安全边界。
图:Shadow 边界与穿透通道——隔离的是 CSS 作用域,不是数据与事件

3. 在 React/Vue 里怎么用​

跨框架传值最容易出错的是属性与特性的区别:HTML attribute 只能是字符串,组件内部的 property 才可以是对象、数组。所以给自定义元素传复杂数据时必须走 property(el.config = {...}),不能写成 config="..."。事件方向相反:内部用 CustomEvent 派发,若希望事件能穿过 Shadow 边界到达外层文档,composed 必须为 true,需要冒泡再叠加 bubbles。

composedbubbles结果
truetrue可穿过 Shadow 边界到达外层文档
truefalse停在 Shadow 边界,外层收不到
falsetrue冒泡但出不了 Shadow 边界
this.dispatchEvent(new CustomEvent('change', {
detail: { value: this.value },
composed: true, // 允许穿过 shadow 边界
bubbles: true, // 允许向上冒泡
}))

React 对未知属性默认走 attribute,布尔与对象要特别注意——这类边界情况决定了「能不能顺滑接入」。原生表单组件要用 ElementInternals 才能参与外层表单的校验与提交,这部分点到为止。

3.1 它的甜区在哪里​

适合:跨框架组件库、微前端的公共组件、要嵌入第三方页面的部件。不适合:需要深度主题定制(受 Shadow 隔离所限)、依赖框架状态管理与路由的内部业务页面。它不解决状态管理、路由、数据绑定,定位是跨框架复用与强隔离的组件层。

服务端渲染这块,声明式 Shadow DOM 是唯一不需要 JS 就能产出影子树的写法:

<color-badge>
<template shadowrootmode="open">
<style>:host { display: inline-block; padding: 2px 8px; }</style>
<span><slot></slot></span>
</template>
新品
</color-badge>

3.2 服务端渲染这一段缺什么​

传统 Shadow DOM 在 SSR 下拿不到首屏内容,声明式 Shadow DOM(在 HTML 里直接写出 shadow 结构)改善了这一点,但服务端解析、流式渲染与工具链支持仍要逐个验证。要 SSR 就得先确认你的框架与构建链支持。

不支持声明式的浏览器还有一条退路,代价是首屏闪一下:

// 兜底:不支持声明式影子树时,客户端补一次 attachShadow
if (!HTMLTemplateElement.prototype.hasOwnProperty('shadowRootMode')) {
for (const tpl of document.querySelectorAll('template[shadowrootmode]')) {
const host = tpl.parentElement
const root = host.attachShadow({ mode: tpl.getAttribute('shadowrootmode') })
root.append(tpl.content)
tpl.remove()
}
}

样式穿透与插槽的行为以 MDN:Using shadow DOM 描述为准,尤其是 ::part 与 CSS 自定义属性的部分。