Web Components:组件化开发的“原生”答案
引言:组件化浪潮中的“新选择”

我第一次听到 Web Components 这个概念,是在一个项目的架构评审会上。那是一个需要支持多个子产品、多端共用组件的大型项目,我们当时已经尝试了 React 和 Vue 的组件库方案,但随着产品线的扩张和团队人员的流动,维护成本越来越高。
“有没有一种方式,可以让我们不再依赖框架?” 我抛出了这个问题。同事笑了笑:“有啊,就是 Web Components。”
说实话,那时候我对它只有一个模糊的印象——浏览器原生支持的自定义元素?听起来很酷,但似乎离实际落地还有点远。不过,在接下来的一年里,我的看法彻底改变了。
问题描述:跨框架协作的困境

我们负责的是一款企业级 SaaS 平台,包含多个业务模块,分别由不同团队独立开发,使用不同的前端技术栈:
- 财务模块:Vue.js
- 客服系统:React
- 管理后台:Angular(没错,你没听错)
- 第三方集成:jQuery 混合写法的老项目
这本身不是什么大问题,直到我们要做统一的 UI 组件库时才暴露出痛点:
- 组件复用困难:每个团队都得重复实现类似功能的组件,比如按钮、弹窗、表格等。
- 样式冲突频繁:不同团队引入的 CSS 样式互相污染,尤其是第三方库和全局样式混在一起。
- 升级维护成本高:一次小改动可能要在多个仓库中同步更新,还容易出错。
- 性能难以统一优化:有些团队对渲染性能不敏感,导致整体页面加载慢。
这些问题最终演变成了一个严重的沟通瓶颈,甚至在几次上线过程中,UI 层因为样式或组件兼容性的问题而回滚发布。
这时候我才意识到:我们缺少一个真正“通用”的 UI 组件解决方案。
解决方案:回归原生的 Web Components

带着这样的思考,我又重新研究起了 Web Components。我发现它的几个核心特性非常契合我们的需求:
- Custom Elements(自定义元素)
- Shadow DOM(影子 DOM)
- HTML Templates(模板)
- ES Modules 支持
这些并不是新玩意,但结合当前浏览器的支持情况和 ES 模块生态,已经具备了实际应用的条件。
尝试阶段:从一个基础组件开始
我决定先做一个最小可行性验证,就从最基础的 <my-button> 开始,看看能不能跑通。
class MyButton extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
const button = document.createElement('button');
button.textContent = this.getAttribute('label') || 'Click me';
button.style.padding = '10px 20px';
button.style.backgroundColor = '#007bff';
button.style.color = '#fff';
button.style.border = 'none';
button.style.borderRadius = '4px';
button.addEventListener('click', () => {
this.dispatchEvent(new CustomEvent('my-click', { detail: 'custom event fired!' }));
});
shadow.appendChild(button);
}
}
customElements.define('my-button', MyButton);
然后在 HTML 页面中直接使用:
<my-button label="提交"></my-button>
<script type="module" src="/components/my-button.js"></script>
这个例子虽然简单,却让我看到了无限可能——它不依赖任何框架,能在任意 HTML 页面中运行,并且自带封装性和样式隔离能力。
推广阶段:构建可扩展的组件体系
尝到甜头之后,我们决定围绕 Web Components 构建一套统一的 UI 组件库,并制定了以下目标:
- 所有组件必须通过
customElements.define()注册 - 组件内部样式封装在 Shadow DOM 中
- 提供清晰的 API 文档,包含 props、events、slots
- 通过 npm 发布包,支持按需引入
- 兼容主流现代浏览器,IE11 不强制要求支持(基于项目实际情况)
技术选型:轻量封装 + 工程化支持
我们在实践中并没有完全从头造轮子,而是选择了两个工具来提升效率:
- lit-html:Google 出品的轻量级模板引擎,配合 lit-element 使用体验很好
- rollup:作为打包工具,能够将多个组件打包为 ESM 模块并自动优化输出体积
举个例子,如果我们想实现一个更复杂的带图标和插槽的按钮组件:
import { html, LitElement } from 'lit-html';
export class IconButton extends LitElement {
static get properties() {
return {
icon: String,
label: String
};
}
render() {
return html`
<style>
.icon-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
border-radius: 4px;
background-color: #28a745;
color: white;
}
</style>
<button class="icon-btn">
${this.icon ? html`<img src="${this.icon}" alt="">` : ''}
<slot>${this.label}</slot>
</button>
`;
}
}
customElements.define('icon-button', IconButton);
这种写法比纯原生的方式更易维护,也更容易与已有工具链集成。
效果总结:组件一致性、性能与可维护性的三重提升
1. 组件一致性显著提高
以前每个项目都有自己的“按钮”,现在整个公司都统一使用同一个 <my-button>,视觉风格和交互行为一致,极大提升了用户体验。
2. 性能优化空间更大
由于 Web Components 基于原生 DOM 操作,少了框架的中间层处理,性能表现更优。尤其是在某些老项目(如 jQuery 项目)中引入组件后,页面渲染速度反而比原先的复杂结构更流畅。
3. 维护成本大幅下降
统一组件库建立之后,升级某个功能只需要在一个地方改一次代码,npm 包更新后即可全平台生效。我们曾经有一次修复了一个点击穿透的 bug,影响了十几个页面,之前要手动修改,现在一个版本搞定。
4. 工程师之间的协作更顺畅
跨团队协作再也不需要讨论“用哪个框架”、“怎么传数据”,而是统一调用标准的 HTML 元素和属性。连测试同学都可以轻松理解组件行为。
经验分享:踩过的坑,以及值得坚持的地方

Web Components 是个好东西,但也不是银弹。在这一年多的实践中,我们也遇到了不少问题,走了一些弯路,这里给大家一些实战建议。
1. 别指望它能马上替代 React/Vue
如果你正在做一个全新的中大型项目,还是建议用成熟框架。Web Components 更适合做的是:组件共享、跨框架协作、嵌入式微件(widget),而不是主框架。
2. 不要忽略 Shadow DOM 的局限性
虽然 Shadow DOM 提供了样式隔离,但它也有代价:
- 无法被外部样式覆盖(除非你显式允许)
- 调试不便,开发者工具显示的是 #shadow-root
- 某些 CSS 特性受限,比如字体不能继承
因此,在设计组件时要考虑如何开放定制样式的能力,比如:
:host([primary]) button {
background-color: blue;
}
或者对外暴露变量名让使用者自定义:
document.documentElement.style.setProperty('--my-theme-color', '#ff0000');
3. 事件系统需要特别注意
Web Components 的事件是原生 CustomEvent,在 React 或 Vue 项目中使用可能会有点不兼容。建议:
- 使用
detail传递数据 - 监听事件时统一绑定到宿主节点
- 对框架项目做适配封装(例如写一个 React Wrapper)
4. 工具链一定要跟上
我们一开始没有好好考虑工程化,结果组件库越来越难维护。后来我们做了几件事:
- 引入 Rollup 打包成 ESM 模块
- 用 Storybook 做组件演示文档
- 自动化测试加入 Lit 测试工具(Lit Test)
- 发布到私有 npm 私服,版本管理更规范
5. 不要忽视开发体验
Web Components 的一大劣势是开发体验不如框架友好。所以我们要主动弥补:
- 给 IDE 装上 Web Components 插件
- 在 VSCode 设置中开启标签提示
- 使用 Prettier 插件格式化模板字符串
- 写好 TypeScript 类型定义文件(.d.ts),提升智能提示体验
结语:回到原生,不止是为了“解耦”
写这篇文章的时候,我在想:为什么我们会对 Web Components 产生兴趣?其实本质是因为我们渴望一个真正通用、轻量、稳定的组件交付方式。
在这个“万物皆组件”的时代,Web Components 给我们提供了一种不依赖框架、不绑架技术栈的可能性。它不像 React 那样“强控制”,也不像 Angular 那样“重配置”,而是以一种更“网页本色”的方式去解决问题。
当然,它并不完美,但也正因为如此,它留给了我们足够的自由去创造和探索。也许未来有一天,它会成为主流框架下的标配;也许不会,但它所代表的思想——面向原生、拥抱标准,始终是我们作为一名前端工程师应该坚守的方向。
希望这篇文章能为你打开一扇新的窗户。Web Components 并不是万能药,但在合适场景下,它可以是你组件化道路上不可或缺的利器。
如果你正在为组件复用发愁,不妨尝试一下,说不定会有意想不到的收获。

评论 0