Web Components:在组件化开发中的新探索
开篇:一个老前端的自省与尝试

我是张凯,5年多的前端开发老兵。从早期 jQuery 一把梭,到 Angular、React、Vue 三大框架轮流上阵,再到如今对技术选型越来越理性的我来说,每一次技术变迁都带来不同的思考。
最近两年,在一次大型重构项目中,我们团队面临一个现实问题:现有组件库维护成本越来越高,复用性却逐渐变差。尤其是当我们想要将某些核心组件跨项目复用时,因为依赖太多业务逻辑和框架特性,总是“拆不干净”。就在那时,我开始重新审视 Web Components 这个概念,并逐步将其引入项目实践。
今天,我想分享的就是这一段经历——关于为什么选择 Web Components、遇到了哪些坑,以及它到底能为我们的工作带来什么。
问题描述:组件的可复用之痛

这个故事要从一个需求说起。
我们的公司有多个产品线,各自基于 Vue 和 React 搭建,其中有一个非常常见的 UI 组件叫 FilterBar,也就是筛选栏,每个页面基本都会出现。最初是基于 Vue 实现的,封装得还不错,也做了一些抽象。但随着业务复杂度上升,它慢慢变得臃肿:
- 外部样式依赖越来越多(scoped vs. global 样式冲突)
- 需要注入大量的状态管理代码
- 跨项目使用时必须连带引入整个 Vue 实例或者一堆公共方法
- 最关键的是:换框架就彻底歇菜 —— 我们想把它的能力复用到 React 项目里,根本无从下手。
这并不是个例。像按钮、弹窗、表单控件这类基础组件都在各个项目中被重复实现过至少两次。每次迁移都要写一遍适配层。我们意识到,这种高度耦合框架的设计方式,限制了组件的真正生命力。
于是我们在技术方案讨论会上提出了一个问题:“有没有一种办法,能让这些基础组件不再依附于具体的技术栈?”
答案就是 Web Components。
解决方案:Web Components 来了

初识 Web Components
Web Components 是一组浏览器原生支持的标准,主要包括:
- Custom Elements(自定义元素)
- Shadow DOM(影子 DOM)
- HTML Templates(模板标签)
- ES Modules 加载机制
简单来说,你可以通过 JavaScript 创建一个全新的 HTML 元素,比如 <my-button>,它有自己的结构、样式和行为,完全隔离于外部环境。
听起来很理想,那怎么落地呢?
为什么这次决定尝试 Web Components?
主要有几个动因:
- 技术解耦:无需依赖 React 或 Vue 的语法体系
- 样式隔离:Shadow DOM 可以天然防止样式污染
- 渐进集成:可以在任何项目中直接使用
- 生态兼容性:主流现代浏览器均已原生支持(甚至 IE11 + polyfill 也能玩)
当然,最大的吸引力在于:它是一个标准方案,不需要我们再搞一套复杂的封装机制来模拟组件系统。
代码实践:从零开始打造一个真正的 Web Component

先来看一个小案例 —— 我们来写一个最简单的按钮组件 <custom-button>。
<!-- index.html -->
<custom-button color="blue">点击我</custom-button>
接下来是定义这个组件的 JS 文件:
// custom-button.js
class CustomButton extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
const template = document.createElement('template');
template.innerHTML = `
<style>
button {
padding: 10px 20px;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
background-color: ${this.getAttribute('color') || 'gray'};
color: white;
border: none;
}
</style>
<button><slot>默认文本</slot></button>
`;
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
}
customElements.define('custom-button', CustomButton);
然后在 HTML 中引入:
<script type="module" src="./custom-button.js"></script>
这样你就能像使用普通标签一样使用 <custom-button>,而且:
- 它内部的样式完全独立,不会影响外部样式
- 你可以传入属性(如
color),也可以通过插槽传内容 - 它可以运行在任何框架的环境中,甚至静态 HTML 页面也能使用!
踩坑经验:理想丰满,现实骨感
你以为事情就这样结束了?并没有。实际开发过程中我们踩了不少坑。
坑一:事件绑定的困扰
最开始我们以为只要监听 Shadow DOM 内部的节点就可以了,但遇到一个问题:当用户点击按钮时,父级组件拿不到事件!因为在 shadow root 里面触发的事件,默认不会冒泡到外层。
解决方法:手动派发 Event 并设置 bubbles: true
const btn = this.shadowRoot.querySelector('button');
btn.addEventListener('click', () => {
this.dispatchEvent(new CustomEvent('click', { detail: '来自 web component 的点击事件', bubbles: true }));
});
这样外面就可以通过 @click 或 addEventListener 来监听了。
坑二:生命周期处理不够智能
不同于 Vue 或 React 的组件生命周期管理,Web Components 在这方面完全是“裸奔”。比如我们要监听属性变化怎么办?
需要自己手动观察属性:
static get observedAttributes() {
return ['color'];
}
attributeChangedCallback(name, oldVal, newVal) {
if (name === 'color') {
const button = this.shadowRoot.querySelector('button');
button.style.backgroundColor = newVal;
}
}
虽然麻烦一点,但也给了我们更细粒度的控制权限。
坏掉的小插曲
有一次在开发一个比较复杂的下拉框组件时,发现样式在不同页面上偶尔会错乱。排查了很久才发现是因为没有正确使用 adoptedStyleSheet 来共享全局样式。
后来改用 CSSStyleSheet + adoptedStyleSheets 的方式来统一管理公共样式,才避免了这个问题。
构建优化的问题
刚开始的时候我们只是在 HTML 页面中用 <script type="module"> 直接引用 ES Module,但项目一旦复杂起来就很慢。所以后来我们引入了构建工具。
我们最终选择的方式是:
- 使用 Vite 构建工具打包 Web Components
- 对于生产构建,用 Rollup 打成 ESM 和 UMD 版本
- 在不同项目中作为 NPM 包安装使用
配置文件简化如下(Vite):
// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
build: {
target: 'es2015',
outDir: 'dist',
lib: {
entry: './src/index.js',
name: 'MyComponents',
fileName: (format) => `my-components.${format}.js`
},
rollupOptions: {
external: ['react', 'vue'],
output: {
globals: {
react: 'React',
vue: 'Vue'
}
}
}
}
});
效果总结:一次成功的重构尝试
回过头来看这个决策,确实让我们团队受益不少。
收益点一:跨项目复用效率大幅提升
以前我们要在两个框架中重复实现一个组件,现在只需要写一份 Web Components 就可以同时服务 React、Vue 甚至是 jQuery 项目。组件本身也不再需要依赖某个构建体系,真正做到了“随取随用”。
收益点二:样式隔离让协作更容易
大家都知道,样式冲突是最让人头疼的问题之一。Shadow DOM 提供了一种近乎完美的解决方案,我们再也不怕第三方组件污染样式了。
收益点三:维护成本下降
由于脱离了框架,我们不再需要为不同项目升级 Vue 或 React 而重复更新组件库。更新只需关注功能本身即可。
经验分享:给你的几点建议
如果你也在考虑是否采用 Web Components,我可以给出以下几个建议:
✅ 不要用 Web Components 替代全部组件
Web Components 是轻量级、通用性优先的选择。对于业务组件,尤其涉及复杂状态管理和业务逻辑的,还是建议留在框架内。Web Components 更适合做 UI 层基础组件库。
✅ 推荐配合构建工具一起使用
直接用 script 引入没问题,但如果是中大型项目,推荐配合 Vite 或 Rollup 打包构建,能更好管理模块依赖和输出格式。
✅ 浏览器兼容性要评估清楚
虽然现代浏览器已经广泛支持,但在低版本浏览器上仍需 polyfill(如 @webcomponents/webcomponentjs)。我们为了兼容 IE11 也曾被迫加上 polyfill,性能有一定影响。
✅ 注意交互细节
别忘了这是面向用户的组件。哪怕是个按钮,也要注意 accessibility、focus 状态、键盘操作等细节。
技术趋势:Web Components 正在崛起
其实早在几年前,Web Components 就已经初露锋芒。如今,越来越多的大厂开始采用 Web Components 来打造跨框架组件库,比如 Salesforce 的 Lightning Design System、Google 的 Material Web Components,还有 Adobe Spectrum 等。
而社区也在不断涌现新的工具链,例如 StencilJS、LitElement 等,它们进一步降低了开发门槛,甚至允许你用类 React 的方式来写组件逻辑。
更重要的是,它背后不是某一家公司的利益驱动,而是 W3C 主导的开放标准。这比任何一家公司主导的框架更具有生命力。
结语:写给未来自己的提醒

Web Components 并非万能钥匙,但它提供了一种“回归本源”的思路。
在这次项目实践中,我收获了很多,也明白了一个道理:技术选型从来都不是追求最新或最流行,而是要看它能不能真的解决问题。
Web Components 让我重新认识了“组件”这件事的本质——它不是一个语法糖,不是一个框架功能,而是应该属于 Web 本身的语言能力。
希望这篇文章能为你带来一些启发,也欢迎你在评论区留言交流想法或实践经验。一起成长,一起进步。
文末彩蛋:如果需要完整的组件示例代码或工具链配置,欢迎私信我获取 GitHub 示例仓库链接。

评论 0