Web Components:框架无关的组件标准
React/Vue 各自有组件,但换框架就白写。Web Components 是浏览器原生的组件标准——Custom Elements + Shadow DOM,跨框架复用。本文讲清 WC 的核心 API、和 React/Vue 组件的区别、什么时候值得用(组件库/微前端),什么时候别用。
本文要点
- 主题聚焦:React、TypeScript
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
Web Components:框架无关的组件标准
每个前端都经历过这种痛:用 React 写了套组件库,业务要迁移到 Vue,组件库全部重写。再迁移到原生?再重写一遍。
问题不在你,在框架。React 组件是 React 的,Vue 组件是 Vue 的,它们的"组件"都跑在各自的运行时里,换一个宿主就失效。
Web Components(WC)是另一条路:浏览器原生支持的组件标准。它不依赖任何框架,Chrome/Safari/Firefox/Edge 全部原生支持,写一次,React、Vue、Angular、原生 JS 都能用。
这篇文章讲清楚:WC 的三大核心 API、它和 React/Vue 组件到底差在哪、什么时候该上、什么时候别碰,以及 Google 的 Lit 怎么把 WC 的开发体验拉到接近 React。
一、Web Components 到底是什么
一句话定位:Web Components 是 W3C/WHATWG 制定的浏览器原生组件标准,由三个原生 API 组合而成。
它不是一个框架,而是一组浏览器能力:
customElements.define()—— 注册自定义 HTML 标签Element.attachShadow()—— 创建样式和 DOM 隔离的影子树<template>标签 —— 定义惰性、可复用的 DOM 片段
这三个 API 都是浏览器内置的,不需要 import 任何库,不需要打包器。你写一个 <my-button>,它就是 HTML 的一等公民,和 <button>、<div> 同级。
这正是 WC 最锋利的卖点:永恒且跨框架。只要 HTML 标准不废除它(几十年内不会),你的组件就永远能跑。
二、三大核心技术,挨个拆
1. Custom Elements:自定义 HTML 标签
Custom Elements 让你能注册一个全新的 HTML 标签,并定义它的行为。这是 WC 的灵魂。
class MyButton extends HTMLElement {
connectedCallback() {
// 元素插入 DOM 时触发
this.render();
}
// 监听属性变化(必须先在 observedAttributes 里声明)
static get observedAttributes() {
return ['disabled'];
}
attributeChangedCallback(name: string, _old: string, newVal: string) {
if (name === 'disabled') {
this.classList.toggle('is-disabled', newVal !== null);
}
}
private render() {
this.innerHTML = `<button>点我</button>`;
}
}
// 注册自定义元素,标签名必须带连字符,防止和原生标签冲突
customElements.define('my-button', MyButton);
用起来跟原生标签一模一样:
<my-button disabled></my-button>
几个关键点:
- 标签名必须带连字符(
my-button、ui-card),这是标准规定,为了不和未来的原生标签冲突。 - 生命周期:
connectedCallback(插入 DOM)、disconnectedCallback(移除)、attributeChangedCallback(属性变化),对应 React 的useEffect/useEffect cleanup/ 依赖变化。 - 子类化:可以继承原生元素(
class MyButton extends HTMLButtonElement),用法是<button is="my-button">。
2. Shadow DOM:样式和 DOM 的隔离墙
Shadow DOM 是 WC 的第二大杀器。它给元素挂一棵"影子 DOM 树",这棵树内部的样式和结构对外完全隔离——内部 CSS 不泄漏到外部,外部 CSS 也进不来。
class MyCard extends HTMLElement {
connectedCallback() {
// 开启 Shadow DOM,mode: 'open' 表示外部能通过 element.shadowRoot 访问
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
/* 这里的样式只作用于 Shadow DOM 内部,绝不污染全局 */
.card {
padding: 16px;
border: 1px solid #e5e7eb;
border-radius: 8px;
font-family: system-ui;
}
::slotted(*) { /* 把外部传进来的子节点样式化 */
color: #374151;
}
</style>
<div class="card">
<slot></slot>
</div>
`;
}
}
customElements.define('my-card', MyCard);
<my-card>
<p>这段文字通过 <slot> 投影进 Shadow DOM</p>
</my-card>
<slot> 是 Shadow DOM 的插槽机制,功能类似 Vue 的 <slot> 或 React 的 children,把外部子节点"投影"到组件内部指定位置。
Shadow DOM 的实战价值非常具体:
- 写组件库不用纠结 BEM 命名空间,样式天然隔离,不用担心用户全局 CSS 把你的按钮改得稀烂。
- 封装严格:外部
document.querySelector进不来 Shadow DOM,内部节点不会泄漏到全局。
代价也是有的:Shadow DOM 内的内容搜索引擎默认看不到,做 SEO 站要慎重(下文会展开)。
3. HTML Templates:可复用的惰性 DOM 模板
<template> 标签定义一段 DOM 结构,但浏览器不会渲染它,直到你 clone 一份塞进文档。配合 <slot> 用很顺手。
<template id="row-template">
<tr>
<td class="name"></td>
<td class="age"></td>
</tr>
</template>
<script>
const tpl = document.getElementById('row-template');
const row = tpl.content.cloneNode(true); // 深拷贝模板
row.querySelector('.name').textContent = '张三';
row.querySelector('.age').textContent = '28';
document.querySelector('tbody').appendChild(row);
</script>
<template> 内部是惰性的:图片不加载、脚本不执行、样式不生效。这种"声明结构但不立即渲染"的特性,在写复杂 WC 时是个很好的模板载体。
实战中,更多人直接用模板字符串(shadowRoot.innerHTML = '...'),<template> 反而用得少。但当你需要预定义静态结构、动态克隆时,它依然是最干净的方案。
三、和 React/Vue 组件的本质区别
把 WC 和 React/Vue 组件放一起对比,差异非常清晰:
| 维度 | Web Components | React/Vue 组件 |
|---|---|---|
| 运行依赖 | 浏览器原生,零依赖 | 必须引入对应框架运行时 |
| 跨框架复用 | 一个组件,所有框架通用 | 换框架=重写 |
| 生命周期 | 浏览器标准,稳定 | 跟着框架版本走,可能 breaking change |
| 模板/响应式 | 原生没有,得自己搞或用 Lit | 声明式模板 + 响应式数据,开箱即用 |
| 生态 | 相对小 | React 生态无可匹敌 |
| SSR/SEO | Shadow DOM 不友好 | 成熟方案(Next/Nuxt) |
一句话总结差异:React/Vue 组件是"框架里的组件",WC 是"HTML 里的组件"。
React 组件的本质是一个返回虚拟 DOM 的函数,它必须挂在 react-dom 运行时上,离开 React 就是一坨死代码。WC 的本质是一个真实的 HTML 元素,和 <div> 同级,任何能操作 DOM 的环境(JS、React、Vue、Angular、jQuery、原生)都能直接 <my-button /> 用。
// React 项目里直接用 WC,零适配
function App() {
return <my-button onClick={handleClick}>按钮</my-button>;
}
这种"组件存活期比框架还长"的特性,是 WC 唯一不可替代的价值。
四、WC 的优势:为什么有人选它
-
真正的跨框架。一套设计系统,React/Vue/Angular/原生项目通吃。这是 WC 最硬的场景,所有大厂的设计系统(Ionic、SAP UI5、Adobe Spectrum、GitHub 的部分组件)都用 WC 来实现。
-
不死。标准一旦被浏览器实现,就基本不会被移除。十年前写的 WC 今天还能跑;十年前用 Backbone 写的代码呢?React Class 组件如今也是"能用但不推荐"。
-
样式天然隔离。Shadow DOM 让你不用再纠结 CSS 命名空间、不用 Tailwind 也能写出干净的组件库。
-
分发简单。一个 WC 就是一个 JS 文件,
<script>引进去就能用,不需要 npm install 一堆依赖,适合嵌入式分发给第三方。
五、WC 的劣势:别当银弹
-
API 偏底层,工程体验差。原生 WC 没有声明式模板、没有响应式数据。你写个表单组件,要手动监听属性变化、手动
this.shadowRoot.querySelector更新 DOM——这是 jQuery 时代的开发体验。实际项目几乎都要配 Lit 这类库。 -
SEO 是硬伤。Shadow DOM 的内容默认不被搜索引擎索引(虽然近年有改善,但仍然不可靠)。需要 SSR + 重 SEO 的内容站,WC 不是好选择。
-
表单集成麻烦。原生 WC 元素默认不参与
<form>校验,要用ElementInternalsAPI 一通操作才能让自定义元素像<input>一样工作。这块心智成本不低。 -
生态远不如 React/Vue。React 有 20 万+ npm 包,WC 的生态小得多。你想要的轮子不一定有,有也不一定维护得好。
-
TypeScript 支持一般。JSX 里用 WC 标签,TS 默认会报类型错误,得自己声明
JSX.IntrinsicElements。
六、什么时候值得用
实战里我会推荐在这几个场景上 WC:
-
跨框架组件库 / 设计系统。这是 WC 最正确的场景。公司里有多个技术栈(老项目 jQuery、中台 React、新业务 Vue),设计系统用 WC 写一次全用上,不用维护三套。
-
微前端。把一个复杂模块封装成 WC,嵌入到任意主应用的任意位置。框架无关、样式隔离,天然适合跨团队、跨技术栈集成。
-
浏览器扩展。扩展的 content script 注入到第三方页面,WC 的 Shadow DOM 能保证你的 UI 样式不被宿主页面污染,也不污染宿主。
-
给第三方分发的嵌入式组件(评论框、播放器、支付按钮)。一个
<script>标签搞定,使用方不用装一堆依赖。
七、什么时候别用
反过来,这些场景上 WC 就是给自己挖坑:
-
单一框架的业务项目。公司就一个 React 中台,业务组件用 React 组件就好,响应式、生态、SSR 全是现成的。为了"未来可能换框架"提前上 WC,属于过度设计。
-
重 SEO 的内容站。博客、电商商品页、新闻站——Shadow DOM 内容搜索引擎抓不到,直接影响流量。用 Next.js/Nuxt 的 SSR 才是正解。
-
重度依赖表单和校验的后台系统。原生 WC 表单集成心智成本高,React/Vue 的表单库(Formik、react-hook-form、VeeValidate)省心得多。
-
小团队、追求快速迭代。WC 的开发体验(即使配 Lit)也比不上 React + Tailwind 的速度。
八、Lit:把 WC 从底层 API 解放出来
前面反复说原生 WC 体验差,行业解法是 Lit(Google 出品)。它是一个轻量库(约 5KB),在原生 WC 之上加了声明式模板和响应式属性,开发体验接近 React。
同样的按钮,用 Lit 写是这样:
import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';
@customElement('my-button') // 自动调用 customElements.define
export class MyButton extends LitElement {
// 响应式属性:变化时自动触发重渲染
@property({ type: Boolean }) disabled = false;
@property() label = '点我';
static styles = css`
button {
padding: 8px 16px;
border-radius: 6px;
background: #2563eb;
color: white;
cursor: pointer;
}
button.is-disabled {
background: #9ca3af;
cursor: not-allowed;
}
`;
render() {
return html`
<button class=${this.disabled ? 'is-disabled' : ''} ?disabled=${this.disabled}>
${this.label}
</button>
`;
}
}
对比原生版本,Lit 的好处一目了然:
- 声明式模板:
html模板字符串,不用手写innerHTML和querySelector。 - 响应式属性:
@property标注的属性变化时,自动局部重渲染(类似 React 的 state)。 - 样式作用域自动:
static styles自动注入 Shadow DOM,不用手动 attachShadow。 - 装饰器注册:
@customElement('my-button')一行搞定注册。
Lit 依然输出标准 WC,所以它跨框架、永恒、可分发的特性一个没丢。结论:如果决定上 WC,生产项目几乎一定配 Lit,别硬写原生。
小结
- Web Components 是浏览器原生的组件标准(Custom Elements + Shadow DOM + HTML Templates),零依赖、跨框架、不死。
- 跨框架、永恒、样式隔离是它的三大优势;API 底层、SEO 难、生态小是它的硬伤。
- 该用的场景:跨框架组件库/设计系统、微前端、浏览器扩展、给第三方分发的嵌入式组件。
- 别用的场景:单一框架的业务项目、重 SEO 内容站、重表单的后台系统、小团队快速迭代。
- 生产环境配 Lit,声明式 + 响应式,把原生 WC 的开发体验拉到接近 React。
金句:框架会过时,标准不会。Web Components 不是用来取代 React 的,它是用来写"活得比框架还久"的组件的。
Related