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 变量、可继承属性仍会穿透。
3. 在 React/Vue 里怎么用
跨框架传值最容易出错的是属性与特性的区别:HTML attribute 只能是字符串,组件内部的 property 才可以是对象、数组。所以给自定义元素传复杂数据时必须走 property(el.config = {...}),不能写成 config="..."。事件方向相反:内部用 CustomEvent 派发,若希望事件能穿过 Shadow 边界到达外层文档,composed 必须为 true,需要冒泡再叠加 bubbles。
| composed | bubbles | 结果 |
|---|---|---|
| true | true | 可穿过 Shadow 边界到达外层文档 |
| true | false | 停在 Shadow 边界,外层收不到 |
| false | true | 冒泡但出不了 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 自定义属性的部分。