在企业级前端开发中,不同业务线往往采用不同的框架,React、Vue、Angular 并存是常态。如果为每个框架单独维护一套 UI 组件,成本极高且容易风格割裂。Web Components 是一组浏览器原生标准,包含 Custom Elements、Shadow DOM 和 HTML Templates,它允许开发者定义自己的 HTML 标签,并且这些标签的行为不依赖任何上层框架,从而成为构建与框架无关的 UI 组件库的理想选择。

为什么选择 Web Components 来做跨项目组件库
传统 UI 库如 Ant Design 面向 React,Element 面向 Vue,它们的组件逻辑和生命周期深度绑定框架运行时。一旦项目技术栈切换,旧组件无法直接复用。Web Components 则运行在浏览器底层,框架只是它的使用者而非载体。在 React 里你可以写 <my-button>,在 Vue 模板里同样可以写 <my-button>,两者调用的都是同一个原生自定义元素。
另一个关键优势是隔离性。通过 Shadow DOM,组件的内部结构和样式对外不可见,外部全局 CSS 不会污染组件,组件样式也不会泄漏到页面。这对跨项目交付尤其重要,因为不同项目的 CSS 重置方案和命名习惯千差万别,原生隔离能大幅降低样式冲突概率。
核心标准与基本结构
Web Components 主要依赖三项技术。Custom Elements 用来注册自定义标签并定义其生命周期;Shadow DOM 提供封装;HTML Templates 和 Slots 用于内容分发。下面我们用一个最简按钮组件说明基本写法。
class MyButton extends HTMLElement {
constructor() {
super();
// 创建影子根,开启样式与 DOM 隔离
const shadow = this.attachShadow({ mode: 'open' });
const style = document.createElement('style');
style.textContent = `
button {
padding: 8px 16px;
border-radius: 4px;
border: none;
background: #3b82f6;
color: #fff;
cursor: pointer;
}
`;
const btn = document.createElement('button');
btn.textContent = this.getAttribute('label') || '按钮';
shadow.appendChild(style);
shadow.appendChild(btn);
}
}
// 注册自定义元素
customElements.define('my-button', MyButton);
上面代码中,我们继承 HTMLElement 并实现构造函数,在内部挂载 Shadow DOM。注意 customElements.define 的标签名必须包含连字符,这是规范硬性要求,避免与未来原生标签冲突。
该组件在任意框架中都能以 <my-button label="提交"></my-button> 形式使用。如果需要在 React 中使用,由于 React 对未知属性处理有差异,建议通过 ref 设置属性或使用事件代理,但标签本身无需任何包装层。
属性与事件的跨框架通信设计
组件库要真正跨项目,必须规范数据进出方式。推荐做法是:单向数据流用 Attributes 或 Properties,用户交互用 CustomEvent 派发事件。框架层负责监听事件并更新自身状态,而不是由组件内部直接修改外部状态。
以下示例展示一个带计数的组件,父级可以通过属性传入初始值,点击后派发事件通知外部:
class CounterEle extends HTMLElement {
static get observedAttributes() {
return ['count'];
}
constructor() {
super();
this._count = 0;
const shadow = this.attachShadow({ mode: 'open' });
this.btn = document.createElement('button');
this.btn.textContent = 'count: 0';
this.btn.addEventListener('click', () => {
this._count++;
this.btn.textContent = 'count: ' + this._count;
// 向外部派发自定义事件
this.dispatchEvent(new CustomEvent('count-change', {
detail: { value: this._count },
bubbles: true,
composed: true
}));
});
shadow.appendChild(this.btn);
}
attributeChangedCallback(name, oldVal, newVal) {
if (name === 'count') {
this._count = parseInt(newVal, 10) || 0;
this.btn.textContent = 'count: ' + this._count;
}
}
}
customElements.define('counter-ele', CounterEle);
这里 observedAttributes 声明需要监听的属性,attributeChangedCallback 在属性变化时触发。事件设置 composed: true 才能让事件穿透 Shadow DOM 边界,被外部框架捕获。Vue 中可用 v-on:count-change 监听,React 中可在 JSX 写 onCountChange 或通过 addEventListener 绑定。
很多团队忽略事件命名规范,导致 Angular 和 React 监听写法不一致。建议统一使用 kebab-case 事件名,并在文档中明确 detail 结构,避免对接时反复试错。
样式主题与插槽机制
跨项目组件库常需适配不同主题。利用 CSS 自定义属性(变量)是最轻量的方案。组件内部样式引用 var(--my-primary, #3b82f6),使用方在全局或宿主元素上定义变量即可换肤,不需要穿透 Shadow DOM。
内容分发则用 Slot。下面的卡片组件允许外部插入标题和正文:
<template id="card-tpl">
<style>
.card { border: 1px solid #e5e7eb; border-radius: 8px; padding: 16px; }
::slotted(h3) { margin: 0 0 8px; }
</style>
<div class="card">
<slot name="title"></slot>
<slot name="body"></slot>
</div>
</template>
<script>
class MyCard extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
const tpl = document.getElementById('card-tpl');
shadow.appendChild(tpl.content.cloneNode(true));
}
}
customElements.define('my-card', MyCard);
</script>
使用方式为 <my-card><h3 slot="title">标题</h3><p slot="body">内容</p></my-card>。Slot 让组件保持结构灵活,又不影响内部封装。需注意 ::slotted 只能选择直接插槽节点,不能深层选择,复杂样式应交给使用方通过变量控制。
构建与跨框架集成建议
手写原生类适合演示,实际组件库应使用构建工具。推荐用 Vite 配合 @web/dev-server 或 Rollup 将每个组件打包为独立 ES Module,输出 my-button.js 这类文件。各项目通过 import 'ipipp.com/ui/my-button.js' 引入即可,无需 npm 依赖具体框架包。
在 React 中集成时,由于 React 17 之前不会把部分属性如 class 映射到自定义元素,建议使用 reactify-wc 类小工具或直接在 useEffect 里操作 ref。Vue 3 对 Custom Elements 支持良好,只需在编译选项配置 isCustomElement 忽略标签校验。Angular 则提供 CUSTOM_ELEMENTS_SCHEMA 直接在模块中声明。
| 框架 | 集成要点 | 常见坑 |
|---|---|---|
| React | 用 ref 设属性,监听原生事件 | 属性名驼峰映射、事件需手动绑定 |
| Vue 3 | 配置 isCustomElement | 模板校验误报、v-model 不支持 |
| Angular | 添加 CUSTOM_ELEMENTS_SCHEMA | 变更检测需手动触发 |
最后,组件库应配套编写运行时文档站点,用原生示例展示用法,并提供 TypeScript 类型声明文件,描述 Attributes 和 Events 的接口。这样不论哪个框架的开发者,都能依据类型提示正确接入,减少沟通成本。
总结与实践路线
构建与框架无关的 UI 组件库,核心是用 Web Components 标准把视图和交互封进自定义元素,对外只暴露属性、事件和插槽。第一步盘点通用组件清单,第二步用原生或轻量封装库如 Lit 编写组件,第三步规范通信协议与主题变量,第四步配置构建并验证在 React、Vue、Angular 项目的表现。沿着这条路线,团队可以用一套代码支撑多个技术栈,显著降低长期维护负担。
Web_ComponentsUI组件库跨框架修改时间:2026-08-09 23:03:40