Web Components:我在大型前端项目中的原生组件化实战探索
引言:为什么我又一次把目光投向了原生?

在一家用户量过千万的互联网公司,我作为前端团队的核心成员之一,主要负责产品中后台系统的搭建与维护。在过去几年中,我们经历了从 jQuery 到 React 再到 Vue 的技术栈变迁,也见证了组件化开发理念在前端领域的全面普及。
但最近,在一个跨平台需求繁重的新项目中,我重新将目光聚焦到了 Web Components 上。这不是一时兴起的技术尝鲜,而是经过深思熟虑后的选择。我想通过这篇笔记式的分享,讲讲我这段时间用 Web Components 搞定复杂组件复用、实现跨框架通信的真实经历。
问题描述:跨框架复用和组件耦合成了瓶颈

我们的业务场景其实很典型:一个中后台系统需要接入多个不同技术栈的产品模块。比如:
- 用户中心用了 Vue;
- 数据看板用的是 React;
- 一些老的页面仍然依赖 jQuery 插件;
- 还有一些公共组件(弹窗、按钮、数据表格)需要统一风格。
这导致几个明显的问题:
- 样式不一致:每个模块自己管理一套 UI 基础组件,视觉差异严重;
- 功能重复开发:同一个“带搜索的下拉菜单”,三个小组各自开发了一套;
- 跨框架通信困难:React 组件想传个事件给 Vue 得绕三道弯;
- 维护成本高:改一个通用按钮样式要同步三个仓库,还得小心别出兼容性问题。
我们尝试过很多方式来解决这些问题:封装 npm 包、使用 Shadow DOM 封装组件样式、引入设计系统(Design System),但始终没能根治这个问题的根本矛盾:组件是基于某个框架的,而不是原生的 Web 规范。
于是,我决定试试我一直没敢上生产环境的——Web Components。
解决方案:用 Web Components 构建真正的跨框架组件库
Step 1:选型调研
我们在内部组建了一个小团队进行技术选型,最终确定以 Web Components 为核心构建通用组件库,配合 Lit 作为开发框架。
Lit 是 Google 团队维护的库,轻量级、响应式更新机制清晰,而且对 TypeScript 友好。比起原生写 Custom Elements,它大大降低了开发门槛,还保留了足够的自由度。
我们评估过其他方案,比如 Stencil.js、Svelte 的 Custom Element 支持,但考虑团队学习成本和构建工具链的适配,最终选择了 Lit + Rollup 的组合。
Step 2:设计统一的组件 API 和生命周期
为了让组件真正“通用”,我们为所有 Web Components 设计了一套规范化的接口:
interface BaseComponent {
// 属性
disabled: boolean;
label: string;
options: Array<{ value: string; label: string }>;
// 事件
change: CustomEvent;
click: CustomEvent;
// 方法
focus(): void;
openModal(): void;
}
同时,我们遵循标准的 Custom Element 生命周期钩子(connectedCallback, disconnectedCallback 等),并在 Lit 中封装了一些通用逻辑:
class MyButton extends LitElement {
@property() label = '点击我';
@property({ type: Boolean }) disabled = false;
render() {
return html`
<button ?disabled="${this.disabled}" @click="${this.handleClick}">
${this.label}
</button>
`;
}
private handleClick() {
this.dispatchEvent(new CustomEvent('click'));
}
}
这样就能在任何框架里直接使用:
<!-- Vue -->
<my-button label="提交" @click="handleSubmit"></my-button>
<!-- React -->
<MyButton label="提交" onClick={handleSubmit}></MyButton>
<!-- jQuery -->
<my-button id="submitButton"></my-button>
<script>
document.getElementById('submitButton').addEventListener('click', () => {});
</script>
Step 3:封装样式与 Shadow DOM 的使用技巧
Web Components 非常适合封装隔离的样式。我们一开始用的是内置的 shadowRoot:
createRenderRoot() {
const root = this.attachShadow({ mode: 'open' });
const style = document.createElement('style');
style.textContent = baseStyles;
root.appendChild(style);
return root;
}
但很快我们就遇到一个问题:有些外部样式穿透进了 shadow DOM!

解决方案有两个方向:
- 严格控制 CSS 作用域:使用 BEM 或 CSS Modules 样式命名方式,尽量避免全局样式污染。
- 提供主题变量支持:使用 CSS 自定义属性(variables)让组件可以在运行时被“定制化”。
例如:
:host {
--button-bg: #007bff;
}
button {
background-color: var(--button-bg);
}
这样即使组件被嵌入不同的应用,也可以通过设置 --button-bg 来统一主题色。
Step 4:处理浏览器兼容性和性能优化
虽然现在主流浏览器都支持 Web Components,但为了兼容旧版本 Safari 和部分企业内网 IE(是的,还有人在用...),我们也做了一些兜底方案:
- 使用 Polyfill 加载器按需注入;
- 对于低性能设备,采用懒加载策略,只在首次使用时注册组件;
- 使用 lit-element performance tips 做了一些微调,比如减少不必要的渲染、合理使用
@property()装饰器等。
性能方面我们进行了几轮测试,发现 Web Components 的首屏加载速度比纯 JS 组件略慢,但在二次访问或缓存命中后,体验差距几乎可以忽略。
Step 5:跨框架协作与组件通信机制
Web Components 最大的优势之一就是它的通用性。我们借助它实现了跨框架的统一通信机制:
父子通信
使用属性和自定义事件即可完成基本交互,如:
document.querySelector('my-component').addEventListener('select', (e) => {
console.log(e.detail);
});
全局状态共享
为了支持更复杂的场景,我们引入了类似 Redux 的小型状态容器,并通过自定义事件向外广播变更,供各 Web Component 订阅监听。
比如某个“用户信息组件”会监听 user-updated 事件:
connectedCallback() {
super.connectedCallback();
window.addEventListener('user-updated', this.handleUserChange);
}
disconnectedCallback() {
window.removeEventListener('user-updated', this.handleUserChange);
}
虽然不是最完美的解法,但在没有全局状态管理的前提下,这种做法足够稳定有效。
效果总结:组件统一带来的效率提升
项目上线三个月后,我们总结了一下效果:
| 指标 | 结果 |
|---|---|
| 组件复用率 | 提升了约 70% |
| UI 风格一致性 | 视觉验收一次性通过,设计师不再频繁打回来 |
| 开发人员协作成本 | 下降了约 40%,特别是新同事上手更快 |
| 构建体积 | 相比之前多个组件库叠加,整体减少了 12% |
| 性能表现 | Lighthouse 分数保持在 90+,加载速度无显著下降 |
当然也有挑战,比如:
- 某些框架对 Custom Elements 支持不太友好(React 特别明显);
- 编写 TypeScript 类型时需要手动维护类型定义;
- 测试起来不如普通组件方便。
但我们觉得收益远大于代价。
经验分享:Web Components 真的是下一个趋势吗?

作为一个在一线摸爬滚打多年的前端开发者,我的体会是:
Web Components 不是用来取代 React/Vue 的,而是用来填补它们之间缝隙的。
下面是我整理的一些经验建议,希望对你有帮助:
✅ 哪些场景适合用 Web Components?
- 需要在多个不同技术栈间共享的组件;
- 想要做插件市场或嵌入式 SDK 的场景;
- 希望彻底隔离组件样式的组件;
- 需要长期维护、跨技术演进周期使用的组件。
⚠️ 哪些坑需要注意?
- 事件绑定语法在不同框架中的差异:React 默认不会自动绑定事件名(需要用
onXXX),而 Vue 更自然。 - TypeScript 支持需要额外配置:最好自己写
.d.ts文件或者用 custom-elements-manifest 自动生成类型定义。 - 不要过度追求“完全原生”:适当结合像 Lit 这样的小而美的库会事半功倍。
💡 我的小技巧 & 开发心得
- 调试 Shadow DOM 的时候可以用 Chrome DevTools 的 "Elements" 面板展开查看结构
- 用
window.customElements.get('xxx')检查组件是否已正确注册 - 利用
@web/test-runner编写单元测试,模拟不同宿主环境下的行为 - 对于复杂的交互,提前设计好接口边界,防止后期扩展失控
结语:前端生态百花齐放,我们也要多一把武器
Web Components 曾经被认为“未来可期却难落地”。但现在随着工具链成熟、浏览器厂商支持到位,它确实已经在越来越多的大厂项目中崭露头角。
在我参与的这个项目中,Web Components 成为了连接不同技术栈的一座桥梁,也成为我们统一设计语言的基石。
技术没有银弹,只有合适与否。我希望通过这次真实项目的分享,能帮你看到 Web Components 更实用的一面——它不仅仅是又一项炫酷的浏览器特性,而是真正能解决问题的工程实践。
如果你也在做跨框架组件复用、或者想打造自己的 UI 库,不妨试着迈出第一步。或许你会发现,原来原生开发也没那么难。

评论 0