一、Web Components 概述:组件化的原生答案
在现代前端开发中,组件化已成为构建复杂应用的标准范式。React、Vue、Angular 等框架各自实现了私有的组件模型,但这些组件无法跨框架复用。Web Components 是一组浏览器原生提供的 API,允许开发者创建可复用、封装良好的自定义 HTML 元素,真正实现"一次编写,到处使用"。
Web Components 由三项核心技术组成:
- Custom Elements(自定义元素):定义新的 HTML 元素及其行为
- Shadow DOM(影子 DOM):实现 DOM 和样式的完全封装
- HTML Templates(HTML 模板):声明可复用的标记片段
这三项技术共同构成了一个与框架无关的组件模型,所有现代浏览器(Chrome、Firefox、Safari、Edge)均已原生支持,无需任何 polyfill。
二、Custom Elements:定义你自己的 HTML 标签
Custom Elements 是 Web Components 的基础,它允许开发者扩展 HTML 词汇表,创建语义化的自定义标签。
2.1 核心概念与命名规范
自定义元素名称必须包含至少一个连字符(hyphen),这是为了与内置 HTML 元素区分。例如 <my-button>、<app-header>、<data-grid> 都是合法的命名。
2.2 生命周期回调(Lifecycle Callbacks)
Custom Elements 提供了一组特殊的生命周期钩子,让开发者可以在关键时刻介入组件行为:
class MyComponent extends HTMLElement {
constructor() {
super();
// 元素创建时调用——初始化状态、绑定事件、创建 Shadow DOM
console.log("元素被创建");
}
connectedCallback() {
// 元素被插入文档 DOM 时调用
this.render();
this.addEventListener("click", this.handleClick);
}
disconnectedCallback() {
// 元素从文档 DOM 中移除时调用——清理工作
this.removeEventListener("click", this.handleClick);
clearTimeout(this._timer);
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
this.render();
}
}
static get observedAttributes() {
return ["theme", "size", "disabled"];
}
}
customElements.define("my-component", MyComponent);
2.3 元素升级(Element Upgrade)
自定义元素可能在定义之前就已经存在于 DOM 中。浏览器会自动"升级"这些预存在的元素——先创建实例,再执行 connectedCallback。这意味着即使脚本异步加载,元素仍能正确初始化。
2.4 自定义内置元素(Customized Built-in Elements)
通过继承内置元素(如 HTMLButtonElement、HTMLParagraphElement),可以创建增强版的原生元素:
class FancyButton extends HTMLButtonElement {
constructor() {
super();
this.style.background = "linear-gradient(135deg, #667eea 0%, #764ba2 100%)";
this.style.border = "none";
this.style.color = "white";
}
}
customElements.define("fancy-button", FancyButton, { extends: "button" });
// 使用: <button is="fancy-button">点击我</button>
三、Shadow DOM:真正的封装边界
Shadow DOM 是 Web Components 中最强大的特性,它在主文档 DOM 中创建一个完全隔离的子树,实现了样式和 DOM 的封装。
3.1 核心机制与术语
- Shadow Host:附着 Shadow DOM 的常规 DOM 节点
- Shadow Root:Shadow DOM 子树的根节点,通过 attachShadow() 创建
- Shadow Boundary:Shadow DOM 与常规 DOM 之间的边界
3.2 open vs closed 模式
- mode: "open":外部 JavaScript 可以通过 element.shadowRoot 访问 Shadow Root
- mode: "closed":element.shadowRoot 返回 null,完全不可从外部访问
注意:closed 模式提供的"安全"是名义上的,实际项目中推荐使用 open 模式。
3.3 样式封装的真正含义
Shadow DOM 的样式隔离是双向的:内部样式不溢出、外部样式不侵入。例外情况是继承属性(color、font、line-height)会穿透 Shadow Boundary。
3.4 CSS 自定义变量穿透 Shadow Boundary
利用 CSS 自定义变量可以实现可控的样式穿透:
// Shadow DOM 内部
button {
background: var(--my-button-bg, #3498db);
color: var(--my-button-color, white);
}
// 外部页面修改样式
my-button {
--my-button-bg: #e74c3c;
}
四、Slot 机制:声明式的组件组合
Slot 是 Shadow DOM 中的占位符机制,允许用户向自定义元素注入内容。
4.1 默认 Slot 与命名 Slot
// 组件定义
shadowRoot.innerHTML = `
<header><slot name="header"></slot></header>
<main><slot></slot></main>
<footer><slot name="footer"></slot></footer>
`;
// 使用方式
// <my-layout>
// <h1 slot="header">标题</h1>
// <span>主体内容</span>
// <slot name="footer">底部</slot>
// </my-layout>
4.2 Slot 变化监听
const slot = shadowRoot.querySelector("slot");
slot.addEventListener("slotchange", (e) => {
const nodes = e.target.assignedNodes();
console.log(`Slot 变化,当前包含 ${nodes.length} 个节点`);
});
五、HTML Templates:高效的可复用标记
<template> 标签中的内容不会被浏览器解析或渲染,它只是一个"休眠"的 DOM 片段,等待被 JavaScript 激活。
<template id="user-card">
<style>.card { padding: 16px; border-radius: 8px; }</style>
<div class="card">
<h2 class="name"></h2>
<p class="bio"></p>
</div>
</template>
<script>
const template = document.querySelector("#user-card");
const clone = template.content.cloneNode(true);
clone.querySelector(".name").textContent = "张三";
document.body.appendChild(clone);
</script>
六、完整实战:构建 DataTable 组件
综合运用 Web Components 三大技术,构建一个功能完整的数据表格:
class DataTable extends HTMLElement {
static get observedAttributes() {
return ["columns", "data", "sortable"];
}
constructor() {
super();
this.attachShadow({ mode: "open" });
this._data = [];
this._sortColumn = null;
this._sortDirection = "asc";
}
connectedCallback() {
this._columns = JSON.parse(this.getAttribute("columns")) || [];
this.render();
this._attachEvents();
}
set data(value) {
this._data = value;
this.render();
}
render() {
const sortable = this.hasAttribute("sortable");
this.shadowRoot.innerHTML = `
<style>
:host { display: block; font-family: system-ui, sans-serif; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #e0e0e0; }
th { background: #f5f5f5; font-weight: 600; cursor: ${sortable ? "pointer" : "default"}; }
::slotted(.empty-state) { padding: 40px; text-align: center; color: #999; }
</style>
<table>
<thead><tr>${
this._columns.map(col => `<th data-key="${col.key}">${col.label}</th>`).join("")
}</tr></thead>
<tbody>${
this._sortedData.map(row => `<tr>${
this._columns.map(col => `<td>${row[col.key]}</td>`).join("")
}</tr>`).join("")
}</tbody>
</table>
<slot class="empty-state"></slot>
`;
}
get _sortedData() {
if (!this._sortColumn) return this._data;
return [...this._data].sort((a, b) => {
const diff = a[this._sortColumn] > b[this._sortColumn] ? 1 : -1;
return this._sortDirection === "asc" ? diff : -diff;
});
}
_attachEvents() {
this.shadowRoot.querySelectorAll("th[data-key]").forEach(th => {
th.addEventListener("click", () => {
const key = th.dataset.key;
if (this._sortColumn === key) {
this._sortDirection = this._sortDirection === "asc" ? "desc" : "asc";
} else {
this._sortColumn = key;
this._sortDirection = "asc";
}
this.render();
this._attachEvents();
});
});
}
}
customElements.define("data-table", DataTable);
七、框架互操作:与 React/Vue/Angular 的共生
Web Components 最大的优势是与框架无关,但框架与原生 DOM 之间的交互需要适配。
7.1 React 19+ 的改进
React 19 自动识别自定义元素的属性传递:字符串类型映射到 attribute,对象/数组类型映射到 property。
7.2 Vue 3 集成
// vite.config.ts
export default defineConfig({
plugins: [
vue({
template: {
compilerOptions: {
isCustomElement: (tag) => tag.includes("-")
}
}
})
]
});
7.3 Angular 集成
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class AppModule {}
八、Lit 与 Stencil:Web Components 的工程化利器
8.1 Lit(by Google)
Lit 基于 Web Components 标准,提供响应式属性系统和声明式模板:
import { LitElement, html, css } from "lit";
class CounterButton extends LitElement {
static properties = {
count: { type: Number, reflect: true },
variant: { type: String }
};
static styles = css`
button {
padding: var(--counter-padding, 8px 16px);
background: #3498db;
color: white;
border: none;
border-radius: 4px;
}
`;
constructor() {
super();
this.count = 0;
this.variant = "primary";
}
render() {
return html`
<button @click=${() => this.count++}>
点击次数: ${this.count}
</button>`;
}
}
customElements.define("counter-button", CounterButton);
8.2 Stencil(by Ionic)
Stencil 是一个编译器,将类 JSX 语法编译为高性能 Web Components,同时生成框架特定绑定。
九、企业级应用实践模式
9.1 微前端架构中的组件共享
- 设计系统构建框架无关的 UI 组件库
- Widget 嵌入第三方页面
- 渐进式迁移遗留系统
9.2 性能优化策略
- 延迟定义(Lazy Define)控制加载顺序
- Tree Shaking 按需加载组件
- Declarative Shadow DOM 支持 SSR
9.3 可访问性(Accessibility)
- 设置 role 属性提供语义信息
- 使用 ARIA 属性描述交互状态
- 确保键盘导航正常工作
- ElementInternals API 接入表单校验
十、浏览器兼容性与未来展望
当前支持状态
- Chrome/Edge:完整支持(Custom Elements v1 + Shadow DOM v1)
- Firefox:完整支持(版本 63+)
- Safari:完整支持(版本 10+)
正在推进的新特性
- Scoped Element Registry:元素注册的隔离范围
- DSD(Declarative Shadow DOM):HTML 中声明 Shadow DOM
- ariaMixin:简化 ARIA 属性设置
- ::theme() 伪类:跨 Shadow Theme 传递主题变量
十一、总结
Web Components 代表了前端组件化的终极愿景——在浏览器原生层面实现复用。掌握 Web Components 的核心原理,是前端工程师从框架使用者迈向架构设计者的关键一步。2026 年,随着 Declarative Shadow DOM 普及、Lit 性能突破、以及 Scoped Element Registry 落地,Web Components 已经从"未来的标准"变成了"现在的选择"。

发表评论 取消回复