Web Components:原生组件化开发新趋势
开篇:为什么我要聊聊这个话题?

去年公司接了一个大型后台管理系统重构项目。我们的前端团队用的是 React,虽然整体结构还算清晰,但随着业务增长,组件间的耦合越来越高,维护起来也越来越吃力。
更头疼的是,我们还有一部分历史代码是基于 Vue 的旧项目需要集成进来。当时我们就想:有没有一种方式,能绕过框架差异,实现真正意义上的“组件复用”?
就在那个时候,我重新关注起了 Web Components。它早就有耳闻,但说实话一直觉得太冷门、不好用。不过在那次项目中,我们抱着试试看的心态引入了 Web Components 的概念,并且用上了一些现代封装技术(如 Lit),结果大大超出预期——不仅解决了跨框架通信的问题,还在性能和可维护性上带来了不少惊喜。
所以今天我就结合自己的真实项目经验,来聊一聊:
为什么 Web Components 正在成为组件化开发的新趋势?
在实际项目中,它是怎么帮我们解决问题的?
背景和问题描述:一个让人挠头的项目需求

项目背景很简单:把老后台系统的核心功能模块抽离出来,以组件的形式提供给其他项目使用。其中涉及几个关键技术挑战:
- 不同子系统使用不同框架(React + Vue + 部分原生 JS)
- 组件样式和行为需要高度一致
- 需要支持异步加载和懒加载
- 希望组件能够做到真正的封装(样式不污染全局)
一开始我们考虑了几种方案:
- 用微前端 —— 模块间隔离彻底,但太重,且不利于细粒度交互。
- 封装为 npm 包供各框架调用 —— 样式容易冲突,组件状态管理复杂。
- 尝试 Web Components —— 听起来很理想,但不确定实际能不能行得通。
最终选择了第三个方向作为实验对象,主要出于两点考虑:
- Web Components 是原生浏览器支持的标准,理论上可以在任何框架中使用
- Shadow DOM 可以天然隔离样式作用域,避免全局污染
于是我们开始了对 Web Components 的深入实践……
解决方案与实现思路
技术选型
我们决定采用 Lit 这个库来进行组件封装。Lit 是 Google 团队维护的一个轻量级 Web Components 库,基于原生 JavaScript,提供了响应式能力、模板渲染等现代开发体验,同时又不绑定任何主流框架。
我们还搭配了如下工具链:
rollup:用于打包并生成兼容性更强的输出(ESM + UMD)TypeScript:提升开发体验和类型安全性Storybook:本地调试和文档化组件展示postcss + autoprefixer:处理 CSS 兼容性
整个构建流程大致如下:
src/Components/
└── MyComponent.ts → 编写组件逻辑
rollup.config.js → 构建配置
storybook/ → 展示故事书
dist/ → 打包后的组件库输出目录
举个例子:一个简单的按钮组件
比如我们要封装一个基础按钮组件,支持传入 label、disabled 状态、点击事件等属性。
import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';
@customElement('my-button')
export class MyButton extends LitElement {
@property({ type: String }) label = '';
@property({ type: Boolean, reflect: true }) disabled = false;
static styles = css`
:host {
display: inline-block;
}
button {
padding: 8px 16px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
`;
render() {
return html`
<button ?disabled=${this.disabled} @click=${this.handleClick}>
${this.label}
</button>
`;
}
private handleClick() {
this.dispatchEvent(new CustomEvent('click'));
}
}
这个组件会被打包成独立的 .js 文件,然后通过 <script> 引入即可使用,比如:
<script src="https://yourcdn.com/my-button.js"></script>
<my-button label="提交" @click="doSomething"></my-button>
它可以在 Vue 中使用,也可以在 React 中使用,甚至可以放在纯 HTML 页面里跑!
实践中的关键点与坑点分享

✅ 成功的关键:Shadow DOM 和样式隔离
这可能是 Web Components 最核心也最容易被忽视的优点之一。
我们之前经常遇到某个组件的样式不小心覆盖了全局 CSS,导致页面布局错乱。而使用 Shadow DOM 之后,每个组件内部都像是一个独立的小世界,CSS 完全互不影响。
比如上面的例子中,我们在组件内部写的 button { ... } 并不会影响外面的其他按钮样式。
❗️踩过的坑 1:如何调试 Shadow DOM 里的内容
刚开始时我发现 Chrome DevTools 默认是不会展开 Shadow DOM 的内容的。如果你不手动开启相关设置,会以为元素没正确渲染。
解决方法:
- 打开 DevTools 设置(右下角 ⚙️)
- 找到 “Preferences” -> “Elements”
- 勾选 “Show user agent shadow DOM”
这样你就可以看到 Shadow DOM 内部的内容了,方便调试样式和元素。
❗️踩过的坑 2:跨框架事件传递
虽然 Web Components 支持通过 CustomEvent 来传递数据,但在一些框架内(比如 React)并不会自动识别这种自定义事件。
比如下面这段 React 使用方式就不会触发回调:
<MyButton onClick={() => alert('clicked!')} />
因为 React 对非标准事件名不太友好,这时候需要用原生方式绑定监听:
useEffect(() => {
const el = document.querySelector('my-button');
const handler = () => alert('clicked!');
el?.addEventListener('click', handler);
return () => el?.removeEventListener('click', handler);
}, []);
或者,在 Web Components 里将事件改为小写命名:
this.dispatchEvent(new CustomEvent('click')); // 小写,更通用
并在 React 使用时改用驼峰转小写的格式:
<MyButton click={handleClick} />
❗️踩过的坑 3:样式暴露给外部定制
有时候我们也希望允许使用者自定义组件样式,这就需要借助 ::part() 和 ::slotted() API。
比如我们给按钮加一个自定义类名插槽:
<my-button><span slot="icon">⚙️</span> 设置</my-button>
然后组件内部:
<slot name="icon"></slot>
这样用户就可以自定义图标位置,配合 ::slotted([slot='icon']) 来为其添加样式。
这种方式让组件既保持了封装性,又能对外提供一定程度的可定制性。
结果与收益分析
经过三个月的实践打磨,我们完成了第一批 Web Components 的封装工作,包括表单输入、表格、弹窗、按钮等常用组件。
最终效果非常不错:
- 多框架共存不再难:Vue、React、jQuery 项目都能统一调用同一套 UI 组件
- 样式无冲突:得益于 Shadow DOM,各个组件之间互不影响,省去了很多样式调试时间
- 构建体积变小:相比引入一个完整 UI 库,单独打包的 Web Components 更轻量
- 开发体验升级:结合 Storybook 做组件文档和演示,提高了协作效率
更重要的是,我们这套组件逐渐成为了公司内部 UI 规范的一部分,被多个产品线引用。
我的一些建议和注意事项
1. 别再执着于框架之争了
现在很多人还在争论 Vue 好还是 React 好。但在 Web Components 面前,这些都可以共存。如果目标是打造一套稳定、可复用的 UI 组件系统,Web Components 绝对是一个值得投资的方向。
2. 注意兼容性问题
虽然现代浏览器对 Web Components 支持良好,但如果你还需要兼容 IE11 或低端移动端设备,就需要引入 polyfill。例如:
npm install @webcomponents/webcomponentsjs
然后在入口文件头部加上:
import '@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js';
import '@webcomponents/webcomponentsjs/webcomponents-bundle.js';
当然,现在大部分企业级应用已经不用支持那么老旧的浏览器了。
3. 工具链要统一
如果你打算把 Web Components 推广到整个团队,建议从一开始就统一构建流程。我们是使用 Rollup + TypeScript + PostCSS 的组合,配合统一的 Git Hook 和 CI 流程,保证组件库的质量一致性。
4. 调试和测试不能少
对于 Web Components,我们采用了 Puppeteer 编写 E2E 测试,同时用 Jest 做单元测试。推荐大家也做类似的事情,尤其是针对事件触发、属性绑定等细节场景。
总结
回过头来看这次尝试,其实 Web Components 并不是为了替代框架而存在的,而是为了解决框架无法很好处理的 组件复用 和 样式隔离 的问题。
它不像 Vue/React 那样给你完整的生态,但它轻量、标准、通用,尤其适合打造 UI 组件库、微组件、跨平台组件这类场景。
在未来,我非常看好 Web Components 会和现代框架进一步融合,成为组件化开发的重要一环。如果你也有类似跨项目复用、统一 UI 风格的需求,不妨从一个小部件开始尝试一下,说不定会有意想不到的收获。
毕竟,真正优秀的开发者,永远不应该被技术栈所限制,而应该用合适的技术去解决实际问题。
最后送一句我在项目中常常提醒自己同事的话:
“技术是用来解决问题的,而不是用来争输赢的。”

评论 0