从零搭建到落地:Web Components 带给我的组件化新体验
作为一名前端开发者,经历过 jQuery 时代、 Backbone/AngularJS 的 MVC 潮流、再到 React/Vue 这类现代框架的洗礼之后,我对“组件化开发”这个概念可以说是再熟悉不过了。
然而,在一个项目中,我们遇到了前所未有的挑战:多个团队共用一套 UI 组件,但各自使用的技术栈不同,维护成本极高。
在尝试各种方案(包括跨技术栈封装、共享 NPM 包、微前端等)后,我们决定回到原点——Web Components,因为它足够原生、足够通用、足够灵活,且天生支持现代浏览器。
这篇文章会围绕我亲身经历的一个项目展开,分享我们是如何通过 Web Components 把看似不可能统一的 UI 层统一起来的。希望你也能从中看到它在未来可能扮演的角色。
背景:多技术栈下的 UI 碎片化难题

事情要从两年前说起。当时我在一家中大型互联网公司负责一个企业级系统架构的升级。我们的产品矩阵中有多个子系统,分别由不同的团队负责:
- 营销后台是用 Vue.js 开发的
- 数据分析平台是 Angular 的遗产项目
- 客服工单系统则是一个轻量级的 React SPA
- 更不用提还有一些老的 PHP 页面直接嵌套在主站里
这些系统虽然功能不同,却都需要使用一致的 UI 设计语言和交互规范。更关键的是,有几组核心组件(比如按钮、表单、模态框)是每个系统都在重复实现的。
最头疼的问题来了:UI 不一致 + 维护困难 + 功能更新同步滞后。
每次设计师说“按钮圆角换一下”,我们需要同步改四个地方……这显然是不合理的。
于是我们开始寻找一个能跨越技术栈的通用解决方案。
尝试过的路:不是不够好,就是太重或太脆弱

我们尝试过几种常见的做法:
1. 用 npm 包 + 各框架适配器
简单来说,把公共组件抽成 npm 包,然后为每个框架写一个 adapter。比如一个 <BaseButton> 在 Vue 中是个 .vue 文件,在 React 里是 .jsx 组件。
问题在于:
- 每次改动都要同时维护多个分支
- 需要额外处理样式隔离和依赖冲突
- 团队间协作效率低,尤其对非主流技术栈(如旧版 Angular)
2. 使用 Webpack Module Federation 微前端方案
我们想借助微前端的思想,让主应用加载其他子系统的组件资源。听起来不错,但在实际中出现很多兼容性问题,特别是在生命周期管理、状态隔离方面,复杂度陡增。
而且,并不是所有团队都准备好迁移到微前端结构,这种“为了复用而重构”的代价太高了。
转折点:Web Components 浮出水面

最终让我们眼前一亮的,是那个一直被提及却鲜有人真正在生产环境中使用的方案:Web Components。
你可能会问:“Web Components?这不是 HTML5 刚出来时候的概念吗?”
确实如此,但它的几个特性刚好解决了我们的痛点:
- 原生支持:不需要依赖任何框架
- 真正的封装性:样式隔离、DOM 封装
- 自定义标签:可以像普通 HTML 标签一样使用,语义清晰
- 可与任意框架集成:无论你是 Vue、React、Angular 甚至原生 JS,都能自由使用
这简直是最适合我们当前场景的解耦方式!
实践:从一个 Button 组件开始
为了验证可行性,我们先从小处入手:从零开始打造一个 <ui-button> Web Component。
Step 1:基本结构搭建
我们使用原生 JavaScript 创建了一个简单的按钮组件:
class UIButton extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
button {
padding: 8px 16px;
border-radius: 4px;
background-color: #007BFF;
color: white;
cursor: pointer;
}
button:hover {
background-color: #0056b3;
}
</style>
<button><slot>Default</slot></button>
`;
}
}
customElements.define('ui-button', UIButton);
然后直接在 HTML 或其他框架中使用它:
<ui-button>点击我</ui-button>
这一步非常顺利,也证明了它可以脱离框架运行。
Step 2:加上属性控制(props)
我们希望它支持一些常见的属性,比如 type="primary"、disabled、loading 等。
static get observedAttributes() {
return ['type', 'disabled', 'loading'];
}
attributeChangedCallback(name, oldValue, newValue) {
switch (name) {
case 'type':
this._setType(newValue);
break;
case 'disabled':
this._setDisabled(newValue !== null);
break;
case 'loading':
this._setLoading(newValue !== null);
break;
}
}

这部分花了些时间调试属性变更逻辑,但最后效果很自然,使用时就像操作普通 DOM 元素一样:
<ui-button type="success" disabled>提交</ui-button>
Step 3:事件绑定 & 行为封装
为了让组件具备交互能力,我们还加入了 click 事件触发机制:
this.shadowRoot.querySelector('button').addEventListener('click', () => {
const event = new CustomEvent('click', {
bubbles: true,
composed: true,
detail: { value: this.textContent.trim() }
});
this.dispatchEvent(event);
});
这样,外部就可以监听组件行为:
<ui-button id="my-button">提交</ui-button>
<script>
document.getElementById('my-button').addEventListener('click', e => {
console.log('按钮被点击啦');
});
</script>
深入实战:如何把它用进真实项目?
有了原型之后,我们开始将它整合进各个系统,过程中遇到一些坑,也摸索出了一些经验。
1. 如何在 Vue 中使用 Web Component
Vue 对于原生 Custom Elements 支持良好,只需要注册忽略列表即可:
// vue.config.js
module.exports = {
chainWebpack: config => {
config.module
.rule('js')
.test(/\.js$/)
.use('babel-loader')
.loader('babel-loader');
},
devServer: {
headers: {
"Access-Control-Allow-Origin": "*"
}
}
};
// main.js
import './components/ui-button';
const app = new Vue({
// ...
}).$mount('#app');
然后直接在模板里使用:
<template>
<ui-button @click="submitForm">提交</ui-button>
</template>
需要注意的是:Vue 的 v-model 和事件命名需要稍微调整,因为 Web Components 发布的事件通常小写加中划线形式,而 Vue 默认推荐大写驼峰,建议使用事件映射:
<ui-button :value.sync="formValue" />
<!-- 需改为 -->
<ui-button :value="formValue" @update-value="val => formValue = val" />
2. 在 React 中使用 Web Component
React 对 Custom Elements 的支持也不错,但需要安装 polyfill 来应对较老版本浏览器:
npm install @webcomponents/webcomponentsjs
入口文件添加:
import '@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js';
import '@webcomponents/webcomponentsjs/webcomponents-bundle.js';
然后在组件中引入并使用:
import './components/ui-button';
function App() {
return (
<div>
<ui-button onClick={() => alert('React 也能搞定!')}>React按钮</ui-button>
</div>
);
}
React 对大小写敏感,所以要注意属性命名尽量避免混淆。例如:
<ui-button type='success'>成功</ui-button>
3. 样式隔离 & 全局污染防御
我们发现一个问题:有些项目的全局 CSS 会影响到组件内部元素,导致样式错乱。
解决方法:
- 所有样式必须通过 shadow DOM 注入,不要使用 external stylesheets。
- 复杂组件可以考虑使用 CSS-in-JS 方案注入内部样式。
- 外部传入的主题变量可通过 CSS 变量传递,保持一定的灵活性。
:host {
--button-bg-color: #007bff;
}
挑战与优化:踩过的那些坑
当然,Web Components 并不是银弹,我们也遇到不少现实问题。
1. 浏览器兼容性问题
尽管主流浏览器已经广泛支持 Custom Elements V1,但在某些内核较低的环境下(比如微信小程序里的 WebView)仍存在兼容问题。
解决办法是引入官方 polyfill:
<script src="https://unpkg.com/@webcomponents/webcomponentsjs@2.4.3/webcomponents-bundle.js"></script>
如果你使用构建工具,也可以按需加载适配器:
import '@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js';
import '@webcomponents/webcomponentsjs/webcomponents-bundle.js';
2. 构建工具配置麻烦
刚开始的时候,我们尝试用 Rollup 来打包组件库,但遇到很多关于 ES Modules、Tree Shaking、打包格式的问题。
后来我们统一采用 Rollup + TypeScript + Babel 的组合:
- Rollup 负责打包模块
- TypeScript 提供类型安全
- Babel 负责向下兼容
最终输出两个版本:ESM(用于现代浏览器)和 UMD(用于传统环境),通过 package.json 的 module 和 main 字段进行区分。
3. 缺乏热更新调试能力
由于 Web Components 是原生 JS 类,不像 Vue/React 有 Hot Reload 工具链。我们在开发初期很痛苦,每次改完代码都得刷新页面看效果。
解决方案是在本地起一个小 demo 页,结合 Webpack Dev Server 实时预览组件变化。
最终成果:组件库上线 & 总体收益
经过两个月的时间,我们打造了一个基础 UI 库,包含按钮、输入框、表格、对话框等常用组件。
最终的使用情况如下:
| 团队 | 技术栈 | 是否接入 Web Components |
|---|---|---|
| A 组 | Vue 2.x | ✅ |
| B 组 | Angular 9+ | ✅ |
| C 组 | React 17 | ✅ |
| D 组 | Django 模板 | ✅ |
最大的收益就是:
- UI 一致性得到显著提升
- 维护成本大幅下降
- 设计变更只需一次发布,全系统生效
- 各团队不必再关心组件实现细节,专注业务开发
心得与建议:给准备上手 Web Components 的你
如果你也在面临类似的组件复用问题,或者只是好奇 Web Components 是否值得投入学习,我想给你几点真诚的建议:
✅ 推荐使用的场景
- 多技术栈并存的大型项目
- 需要对外暴露 SDK 或插件接口的产品
- 内部共享 UI 组件库建设
- 想减少第三方依赖、追求更轻量级的封装
⚠️ 需要谨慎的地方
- 不适合复杂的业务逻辑封装:Web Components 更偏向 UI 组件层面,业务逻辑还是交由上层框架处理更合适。
- 初期学习曲线较高:尤其是 Shadow DOM 的操作和样式限制,需要适应。
- 社区生态不如主流框架丰富:很多现成的库还没完全拥抱 Web Components。
写在最后:未来属于真正开放的标准
回顾这段 Web Components 的实践之路,其实最让我感动的,是一种久违了的“回归本质”的感觉。
曾经我们被各种框架、DSL、抽象层包围着,而现在,用原生的方式写出可以在任何地方工作的组件,反而让我觉得更加踏实。
Web Components 不完美,但它代表了一种趋势——用标准化的方式解决通用问题,而不是用封闭生态圈制造壁垒。
也许有一天,它不会取代 React 或 Vue,但它一定会成为每一个现代前端系统中不可或缺的一环。
最后送大家一句我很喜欢的话:
“好的技术,不在花哨,在于能不能让你轻松地做正确的事。”
希望你也能在这条路上找到属于自己的答案。
如有疑问,欢迎留言交流~

评论 0