Web Components 是一套浏览器原生支持的组件标准,让你无需引入任何框架,就能构建出可在任意项目里复用的 UI 组件。当团队同时维护 Vue、React 与原生页面时,用 Web Components 沉淀一套统一的设计资产,能显著降低重复造轮子与样式冲突的成本。
一、为什么需要 Web Components
现代前端往往是多框架并存:后台用 Vue,营销页用 React,老系统还停留在 jQuery。同一套按钮、日期选择器、图表卡片,在每个技术栈里都要重写一遍,样式还容易互相污染。Web Components 的出现就是为了解决「跨框架复用」这一顽疾——它把组件封装成浏览器原生认识的自定义元素,任何页面只要引入一段 JS,就能像 <user-card></user-card> 这样直接当原生标签使用,与宿主框架彻底解耦。
举个真实的场景:同一个「用户头像卡片」在 Vue 后台、React 落地页、原生 jQuery 旧站里被写了三遍,每次设计改版都要同步三处,稍有遗漏就出现样式错位。把它改造成一个 <user-card> 之后,三端只需各自引入一段脚本,改一处源码即可全站统一生效,维护成本直接降了一个维度。
二、三大核心基石
Web Components 并非单一 API,而是三套标准的组合,各自解决一个关键问题:
| 标准 | 作用 | 解决的问题 |
|---|---|---|
| Custom Elements | 注册自定义标签并绑定行为 | 让浏览器识别并实例化你的组件 |
| Shadow DOM | 把 DOM 与样式封装在独立作用域 | 彻底隔离样式,避免全局污染 |
| HTML Template / Slot | 声明可复用的结构模板与插槽 | 结构复用与内容分发 |
三者配合,就能交付一个「自带样式、跨项目可用、不依赖任何运行时」的组件。
三、实战:从零定义 <user-card>
3.1 用 class 定义组件与生命周期
每个 Web Component 都是一个继承自 HTMLElement 的类,通过 customElements.define 注册标签名。标签名必须包含连字符,这是规范强制的命名约定。
class UserCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' }); // 开启 Shadow DOM
}
connectedCallback() {
this.render(); // 元素插入 DOM 时触发
}
render() {
this.shadowRoot.innerHTML = '<p>Hello Web Components</p>';
}
}
customElements.define('user-card', UserCard);
3.2 Shadow DOM 封装样式
把模板和样式写进 shadowRoot,外部 CSS 无论如何都影响不到组件内部——这正是「样式隔离」的核心。组件自身用 :host 选择宿主元素。
connectedCallback() {
this.shadowRoot.innerHTML = `
<style>
:host { display: block; font-family: system-ui; }
.card { padding: 16px; border-radius: 12px; background: #f5f7ff; }
.name { color: #3b5bff; font-weight: 600; }
</style>
<div class="card">
<span class="name"></span>
</div>
`;
}
3.3 用 <template> 与 <slot> 做内容分发
模板放进 <template> 后不会立即渲染,克隆进 Shadow DOM 即可复用;<slot> 则允许外部传入内容,实现「插槽」式组合,兼具封装与灵活。
<template id="card-tpl">
<style>.card{border:1px solid #eee;padding:12px}</style>
<div class="card">
<slot name="avatar"></slot>
<slot>默认内容</slot>
</div>
</template>
<script>
const tpl = document.getElementById('card-tpl');
class Card extends HTMLElement {
constructor(){ super(); this.attachShadow({mode:'open'}); }
connectedCallback(){
this.shadowRoot.appendChild(tpl.content.cloneNode(true));
}
}
customElements.define('x-card', Card);
</script>
四、属性反射与响应式更新
通过 observedAttributes 声明要监听的属性,浏览器会在属性变化时回调 attributeChangedCallback,组件据此更新视图,实现「属性驱动」的响应式。
class UserCard extends HTMLElement {
static get observedAttributes() { return ['name', 'role']; }
attributeChangedCallback(name, oldV, newV) {
if (oldV === newV) return;
if (name === 'name') {
this.shadowRoot.querySelector('.name').textContent = newV;
}
}
}
// 使用:<user-card name="小李" role="admin"></user-card>
这里有个新手极易踩的坑:HTML 属性(attribute)和 JS 属性(property)是两回事。用户在 DOM 上写 name="小李" 是 attribute,组件内部通常通过 this.getAttribute('name') 读取;而直接给元素赋值 el.name='小李' 则是 property,它不会自动触发 attributeChangedCallback。如果你需要属性与特性双向同步,记得在 setter 里手动调用 setAttribute,否则外部赋值不会反映到视图上。
五、与 Vue / React 协作
Web Components 的最大魅力就是「框架无关」。在 Vue3 组合式 API 或 React Server Components 项目里,你只需 import 一段定义脚本,就能像原生标签一样使用它,无需任何适配器。对于需要统一视觉语言的团队,配合 Tailwind CSS 原子化方案 做主题 token 注入也很顺手。
// Vue3 单文件组件中直接当标签用
import './user-card.js';
// 模板:<user-card :name="user.name"></user-card>
// React(含 RSC / 客户端组件)
import './user-card.js';
function Page() {
return <user-card name="小李" />;
}
在 VS Code 里开发组件时,装好 几款提效插件 能让自定义元素的智能提示与片段补全更顺手。
六、样式隔离的坑与 :host / ::slotted
Shadow DOM 虽好,也有边界:外部样式默认进不来,组件内部样式也出不去。想让外部控制主题,要靠 :host 接收继承的 CSS 变量;想给插槽内容设样式,用 ::slotted() 且仅能选中顶层节点。千万别指望用全局 class 去改组件内部——那是设计上就禁止的。
如果确实想让外部主题切入组件,最干净的做法是在 :host 里读取 CSS 自定义属性(例如 --card-bg),由外部在宿主元素上赋值。这样既保持了样式隔离,又留了一个可控的调节口:外部只改变量、不碰内部结构,组件作者也无需为每种主题写分支。
七、生产落地检查清单
| 检查项 | 建议 |
|---|---|
| 命名 | 必须含连字符,避免与未来标准标签冲突 |
| 样式隔离 | 用 :host 控制外部样式入口,少写全局副作用 |
| 可访问性 | 提供 role / aria 与键盘交互,别只支持鼠标 |
| 构建打包 | 用 Vite 库模式输出 ES module,便于按需引入 |
| 兼容性 | 旧浏览器引入 @webcomponents polyfills 兜底 |
八、调试技巧与浏览器兼容性
开发时打开 Chrome DevTools 的「Elements → Shadow DOM」面板,就能直接展开组件内部节点、逐层审查样式的真实来源;如果组件没有渲染,优先确认 customElements.whenDefined('user-card') 是否已经 resolve,以及定义脚本是否在 DOMContentLoaded 之前就被加载执行。兼容性方面,Custom Elements 与 Shadow DOM 自 2020 年起就已被所有主流浏览器原生支持,只有极少数老旧 WebView 才需要 @webcomponents polyfills 兜底,因此可以放心用于生产环境。
九、什么时候不适合用 Web Components
Web Components 并不是银弹。如果你的应用只跑在单一框架里、且不需要把组件对外输出,那么框架自带的组件体系开发体验更好、生态也更完整;此外,像 React Server Components 这类「在服务端渲染、不携带客户端运行时」的场景,自定义元素也很难发挥作用。判断标准其实很简单:当你需要「一次编写、多处、跨框架复用」时再上 Web Components,否则不要为了用而用,避免引入额外的心智负担。
十、小结
Web Components 用浏览器原生能力补齐了「跨框架复用 UI」的最后一公里:Custom Elements 负责定义、Shadow DOM 负责隔离、Template/Slot 负责结构复用。它没有虚拟 DOM、没有运行时依赖,特别适合做设计系统、嵌入式 widget 与多技术栈共存下的公共组件。下次当你准备在三个项目里第三次重写同一个下拉框时,不妨先想想:它能不能是一个 <user-card>?




