Web Components:原生组件化开发新趋势
Web Components:原生组件化开发新趋势
去年我们在一个大型项目中尝试了 Web Components,结果让我非常惊喜。这不是一篇理论文章,而是我们团队在真实开发过程中踩过的坑、做过的权衡,以及最终收获的收益。这篇文章里,我会用最真实的视角跟你聊聊为什么我们会选择 Web Components,它在实际开发中解决了哪些问题,还有那些让人抓头的技术细节。
如果你正在考虑要不要引入 Web Components 或者想了解它在真实项目中的表现,那这篇应该能帮到你。我们是从 Vue 和 React 混合技术栈迁移到 Web Components 的,整个过程有曲折也有成长,希望这些经验能帮你少走点弯路。
背景:为什么要尝试 Web Components?

我们当时维护着一个内部平台,这个平台需要支撑多个产品线,而且不同产品线的技术栈不统一。前端这边同时存在 Vue 2、Vue 3 和 React 16,每次要新增一个通用组件(比如按钮或弹窗),都要分别写三遍,并且维护起来相当痛苦。
更头疼的是,这些组件的样式和行为也很难保持一致。虽然我们都尽力统一设计系统,但因为技术实现不同,最终出来的效果总有细微差异。再加上一些老项目根本没办法升级框架版本,很多功能更新只能通过手动复制粘贴来实现。
这时候我们就想:有没有一种可以跨框架复用的方式?
我们遇到的挑战

起初我们是使用 Shadow DOM + Custom Elements API 自己封装了一些基础组件。看起来没问题,但随着组件复杂度上升,我们发现几个大问题:
- 事件传递和通信麻烦:尤其是组件嵌套时,父子元素之间的数据交互很难处理。
- 样式隔离导致主题切换困难:Web Components 的 Shadow DOM 默认样式隔离太强,当我们想动态修改主题颜色时,变得很麻烦。
- 缺乏开发工具支持:Chrome DevTools 对 Web Components 的调试体验差,调试属性、生命周期都不方便。
- 兼容性问题:虽然主流浏览器基本都支持了,但旧版 IE 和一些低版本移动浏览器还是不支持。
于是我们不得不深入研究 Web Components 相关生态,包括如何更好地组织代码结构、优化渲染性能以及解决样式问题。
我们的解决方案:选型与架构设计
经过一番调研和对比,我们决定采用以下方案组合:
- 基于 Lit 开发 Web Components,它是一个轻量级的库,基于原生模板语法提供响应式能力和便捷开发体验;
- 使用 Vite + TypeScript + Rollup 构建组件库;
- 所有公共组件统一打包成 NPM 包供各业务线接入;
- 通过 CSS Custom Properties 解决主题样式的灵活性问题;
- 引入 Playwright 编写 UI 组件测试;
- 对于需要兼容旧浏览器的场景,使用 Babel 和 polyfill 回退。

这样的架构让我们实现了几个目标:
- 一套组件,多端可用:可以在 Vue、React、甚至 jQuery 项目中直接使用;
- 统一设计语言:由于共享了同一个组件库,UI 层的一致性大大提升;
- 可扩展性强:新组件很容易添加,旧项目也能逐步迁移;
- 开发效率提升:借助 Lit 的简洁 API,开发和维护成本降低了不少。
代码实践:一个带状态的按钮组件
以我们最常见的按钮组件为例,来看看怎么用 Lit 来实现一个具备点击计数功能的按钮。
// counter-button.ts
import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';
@customElement('counter-button')
export class CounterButton extends LitElement {
@property({ type: Number })
count = 0;
static styles = css`
button {
background-color: var(--button-bg, #0366d6);
color: white;
padding: 10px 20px;
border-radius: 4px;
border: none;
cursor: pointer;
}
`;
render() {
return html`
<button @click=${this.handleClick}>
点击次数:${this.count}
</button>
`;
}
private handleClick() {
this.count++;
this.dispatchEvent(new CustomEvent('count-changed', {
detail: { count: this.count },
bubbles: true,
composed: true,
}));
}
}
使用方式
在 HTML 中直接使用:
<counter-button count="5"></counter-button>
在 Vue 或 React 中使用:
只需要确保你的构建工具正确打包,并全局注入自定义元素即可。不需要任何适配器。对于 React 项目,还需要添加一行配置:
// 忽略 React 对未知标签的警告
/* global JSX */
declare module 'react' {
namespace JSX {
interface IntrinsicElements {
'counter-button': any;
}
}
}
动态主题色支持
我们可以轻松地通过 CSS 变量来控制按钮的颜色,在全局或者页面级别设置变量即可生效:
:root {
--button-bg: #e91e63; /* 主题色 */
}
这样就可以在整个应用中动态更换主题而无需额外 JS 逻辑。
遇到的坑及填坑过程
Web Components 不是银弹,我们在实际使用中踩过不少坑,这里挑几个关键点分享一下。
1. Shadow DOM 隔离带来的样式限制
最初我们把所有组件都包裹在 Shadow DOM 里,后来发现有些高级功能比如 Portal(用于弹出层)就失效了——因为 Shadow DOM 是封闭的,不能访问外部的 DOM 节点。
解决方法:
- 对于需要挂载到 body 上的组件(如 Modal),我们干脆不使用 Shadow DOM,改用
<div>并挂在 body 下面; - 同时引入命名空间类名,避免样式污染;
- 利用 Scoped 样式配合 CSS-in-JS 工具实现更灵活的布局控制。
2. 生命周期不直观,容易造成内存泄漏
Web Components 的生命周期函数不像现代框架那样明确。例如,connectedCallback 相当于 mount,disconnectedCallback 相当于 unmount,但我们曾经在一个定时器组件里忘了清除 timer,导致页面卡顿。
解决建议:
- 为每个组件单独管理副作用;
- 引入类似
useEffect的封装; - 手动清理资源时一定要记得在
disconnectedCallback中释放。
3. 浏览器兼容性问题
虽然大部分现代浏览器都支持 Web Components v1 规范,但在客户现场部署时遇到某些低端 Android 设备,居然连 ES Modules 都无法运行!
解决办法:
使用 Babel 将代码转译成 ES5;
引入 Polyfill,比如:
npm install @webcomponents/webcomponentsjs并在入口处加载:
import '@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js'; import '@webcomponents/webcomponentsjs/webcomponents-bundle.js';
效果总结:迁移后的变化

从原来的“各自为政”到现在的统一组件库,整体上我们的研发流程发生了很大的改善:
| 方面 | 迁移前 | 迁移后 |
|---|---|---|
| 组件一致性 | 多套实现,风格不一 | 单库维护,风格统一 |
| 维护成本 | 高(重复开发) | 显著降低 |
| 技术栈绑定 | 强依赖框架 | 完全解耦 |
| 性能表现 | 无明显瓶颈 | 更轻量,更快启动 |
| 新人上手难度 | 学习曲线陡峭 | 组件文档清晰,易上手 |
特别是在我们后续重构一个中台项目时,几乎一天之内就完成了十几个核心组件的替换,而且没有引起用户侧的反馈。
还有一个意外的收获是我们终于告别了“框架之争”——因为无论谁用什么技术栈,只要调用同一个组件库即可,大家不再争执该不该升级 React 版本或者是否要迁移至 Vue 3。
给读者的建议和注意事项
如果你也在考虑采用 Web Components,我有几个来自实战的小建议:
✅ 建议这么做
- 从小范围开始试验:先从一些简单的非核心组件入手,比如按钮、输入框等。
- 结合 Lit 或 stencil 使用:不要裸写原生 Custom Element,开发体验真的差太多了。
- 使用 TypeScript:类型安全在组件间通信时特别重要。
- 组件尽可能 Stateless:尽量让组件专注于 UI 表现,减少对状态管理的依赖。
- 利用 CSS Custom Properties 实现主题化:这对视觉层面的复用非常重要。
⚠️ 注意事项
- 慎重使用 Shadow DOM:不是所有场景都需要它,特别是涉及 DOM 操作的组件。
- 提前规划好通信机制:组件之间传值最好通过标准事件传递。
- 兼容性要兜底:尤其在企业级应用场景中,别忽略了旧设备的支持。
- 注意 Tree-shaking 配置:如果只用了部分组件,但整包都打进去就有点浪费了。
- 配套文档和示例很重要:不然别的项目组可能不敢轻易引用。
未来展望
Web Components 正处于一个快速发展的阶段。越来越多的框架也开始集成其能力,比如 Vue 3 支持将 SFC 编译为 Web Component,React 也有相关实验性提案。
我相信在未来几年内,基于 Web Components 的“真正意义上的”跨框架组件生态将会更加成熟。对于我们这种拥有大量遗留系统的公司来说,它提供了一种“渐进迁移”的可行路径。
现在回过头来看,当初决定尝试 Web Components 算是我们团队在技术决策上的一个重要转折点。它不仅解决了组件复用的问题,更重要的是推动了前端工程化的标准化进程。
结语:Web Components 值得你尝试
Web Components 不是炫技,也不是噱头,它是解决组件跨项目复用、打破技术栈限制的一种务实之道。也许它的学习曲线不如 React/Vue 平滑,但它给我们的回报远超预期。
如果你还在纠结用哪种方式构建组件,不妨尝试 Web Components。也许刚开始会觉得“这玩意儿好像没什么特别”,但一旦在多个项目里打通了,你会发现它其实比你想的更有力量。
当然,这一切的前提是你要愿意动手试试看。
如需获取完整项目示例,请查看我们的 GitHub 公共仓库 https://github.com/yourname/components(注:请替换为真实地址)
📌 关注公众号「前端早茶」回复 “web-components” 获取实战源码。

评论 0