Web Components:原生组件化开发新趋势
开篇

去年年中,我参与了一个比较大的项目,背景是一个大型企业级系统,里面包含大量的表单、数据展示模块和UI交互控件。整个系统采用传统的Vue + Vuex架构,但在多个业务线同时迭代下,出现了不少“重复造轮子”、“样式冲突”、“跨团队协作困难”的问题。那时候我们就一直在想,有没有一种更“通用”又能保持性能的方式,来构建这些共用的 UI 组件?
于是我们开始探索一些非框架的解决方案。最让我感兴趣的是 Web Components —— 一个基于浏览器原生能力的技术方案。虽然这个概念并不新鲜,但随着现代浏览器支持越来越好,它真的可以成为组件化开发的一条新出路。
这篇文章就想从我实际工作中的经历出发,聊聊我们在 Web Components 方面的实践过程,踩过的坑,以及最终带来的收益和思考。
我们遇到了什么问题?

当时我们的前端工程面临几个很现实的问题:
多框架共存带来维护成本上升
不同业务线使用了 Vue2、Vue3、React 甚至 Angular,导致很多基础组件都要为每个框架单独实现一套。不仅人力浪费大,样式一致性也难以保障。样式污染严重
每个小组自己定义 CSS 样式,经常出现 class 冲突、全局污染。即使用了 CSS Modules,也很难做到完全隔离。组件可移植性差
一旦某个组件需要在其他框架或项目中使用,就必须进行大量适配改造,几乎相当于重新写一遍。沟通成本高
各组之间对组件理解不统一,文档更新慢,大家各自改自己的版本,最后变成一团乱麻。
这些问题逼着我们不得不重新审视目前的架构方式。我们需要一种框架无关、封装良好、易维护、可复用的组件方案。Web Components 正好切中要害。
我们的解决方案:选择 Web Components
我们决定尝试用 Web Components 来构建一些通用的基础组件。比如:按钮、输入框、表格、弹窗、树形结构等。目标是把这些组件抽象出来作为“原子”,供所有项目直接调用,不需要依赖特定框架。
Web Components 的核心技术包括:
Custom Elements(自定义元素)Shadow DOM(影子节点)HTML Templates(模板)ES Modules(现代 JavaScript 模块)
它们组合在一起,能让我们创建出真正意义上的“封装组件”,具有独立作用域和行为,并可以在任意 HTML 页面中通过原生标签调用。
实践过程:一步步搭建一个 Web Component
项目初始化
我们选用了 Vite 作为构建工具,因为它天然支持 ES Modules,并且对 TypeScript 集成友好。整个工程结构非常简单:
src/
├── components/
│ └── my-button/
│ ├── index.js
│ └── template.html
├── index.html
└── main.js
每个组件都作为一个 Custom Element 注册到 window 上。以下是 my-button/index.js 的简要实现:
import template from './template.html?url';
import { fetchTemplate } from '../utils';
class MyButton extends HTMLElement {
constructor() {
super();
this.shadow = this.attachShadow({ mode: 'open' });
// 加载 html 模板内容
fetchTemplate(template).then((html) => {
const temp = document.createElement('template');
temp.innerHTML = html;
const clone = temp.content.cloneNode(true);
this.shadow.appendChild(clone);
// 获取按钮并绑定点击事件
const button = this.shadow.querySelector('button');
button.addEventListener('click', () => {
const event = new CustomEvent('click', {
bubbles: true,
composed: true,
});
this.dispatchEvent(event);
});
});
}
}
customElements.define('my-button', MyButton);
上面的代码做了几件事:
- 使用 Shadow DOM 封装内部结构和样式
- 引入外部 HTML 模板并通过
fetchTemplate动态加载 - 在 shadow root 中插入 HTML 结构
- 给按钮添加事件监听器,并触发自定义事件以便外部监听
这种方式的好处是结构、样式和逻辑都隔离得很好,不会影响页面其他部分。
遇到的坑与解决办法
坑1:无法直接使用框架的功能(如 Vue/React)
这是 Web Components 最让人纠结的地方。由于它是原生机制,不能像框架那样使用响应式状态管理或者 JSX。但我们并没有追求“全能”,而是明确其定位:只用来做基础组件封装。
我们给各个业务团队提供了一份简单的使用指南,说明如何引入组件、监听事件、传参等。
坑2:样式穿透
虽然 Shadow DOM 默认隔离了样式,但有时候我们希望父级样式能够对子组件生效,比如字体颜色、字号等。这时候可以通过 inherit 或使用 CSS 变量来传递样式:
:host {
font-size: var(--my-font-size, 14px);
}
然后在宿主页面设置:
<style>
body {
--my-font-size: 16px;
}
</style>
这样就能灵活控制公共组件的外观而不过度耦合。
坑3:兼容性和 polyfill 支持
虽然现代浏览器基本都支持 Web Components,但对于 IE11 这种老家伙还是得借助 polyfill。我们使用的是官方推荐的 webcomponentsjs,通过动态判断用户 UA,按需加载 polyfill 脚本。
当然,这种策略不是万能的,polyfill 会增加包体积,某些高级特性也无法完美模拟。所以我们建议如果项目需要兼容老旧环境,需要评估是否值得投入。
成果与反馈
经过大约两个月的努力,我们将 20 多个常用组件进行了封装,并集成到了不同项目中。效果还是很明显的:

| 项目类型 | 组件复用率提升 | 跨框架集成耗时 | 样式冲突频率 |
|---|---|---|---|
| Vue 项目 | 90%+ | 几乎无 | 显著下降 |
| React 项目 | 85%+ | 略微适配 props 即可 | 基本解决 |
| 独立 HTML 页面 | 完全开箱即用 | 无需适配 | 无冲突 |
团队之间的协作效率明显提高,文档编写压力也小了很多,因为接口统一了,大家只需要看一个示例即可。
而且最重要的是,这些组件不再依赖某个框架版本,未来即使技术栈升级,这些 Web Components 依然可用。
一些建议与注意事项

如果你也在考虑使用 Web Components,以下几点是我从实践中总结出来的经验:
别期望一步到位解决所有问题
Web Components 更适合做底层组件库,不适合复杂应用层逻辑。如果你的组件本身就有很多状态、动画、交互,可能更适合在框架里处理。结合 ES Module 和现代打包工具
使用 Rollup 或 Webpack 构建 Web Components 库可以优化输出大小和兼容性。对于小型项目,Vite 是个很好的选择。注意事件传递机制
自定义事件要用CustomEvent并设置bubbles和composed为 true,确保能正确冒泡到上层。合理使用 CSS 变量和继承样式
保证组件外观可定制,但又不至于破坏封装性。测试一定要跟上
Web Components 很容易被误用,最好配合单元测试(Jest / Testing Library)和文档自动化生成(如 Storybook)来规范使用方式。关注浏览器兼容性报告
Can I Use - Custom Elements
写在结尾
Web Components 并不是万能药,但它确实给我们带来了全新的思路和更轻量化的组件封装方式。特别是在如今前端技术频繁更迭的大环境下,它提供的那种“一次开发,到处可用”的感觉,真香。
如果你也在做跨团队协作、跨框架迁移、组件共享平台之类的事情,不妨试试 Web Components。它也许不是你唯一的答案,但一定是值得认真考虑的一个选项。
在这个项目之后,我也深刻意识到,真正的优秀架构,不是建立在某一个框架之上,而是建立在浏览器本身的能力之上。用标准的东西解决问题,才是最稳当的做法。
希望这篇来自实战的文章能给你一些启发。如果你已经尝试过 Web Components 或者有相关疑问,欢迎留言交流。我们一起打造更有生命力的前端生态 👇

评论 0