一次组件化演进的真实实战:Web Components 的探索与落地
开篇背景:为什么选择 Web Components?

去年,我负责的项目是一个大型的企业级后台管理系统,需要支持多个业务线的产品模块开发。每个业务线都有自己的前端团队,但大家都面临一个共同的问题:组件复用难、样式冲突频繁、协作成本高。我们试过使用 React 和 Vue 的组件库,也尝试过基于 CSS-in-JS 方案来隔离样式,但始终没找到一个真正“跨框架”的通用方案。
就在这个时候,我在一个技术会议上听到有人提到 Web Components,起初觉得这是一个“听起来很酷但没人真用的技术”,不过当深入研究后,我发现它不仅解决了我们很多痛点,而且浏览器支持也在不断完善,甚至主流框架(如 Vue、React)也开始拥抱它。
于是,我们决定在新一期重构中尝试用 Web Components 来做基础组件库的统一封装和分发。这篇文章就是我在这次项目中的真实经历分享。
问题描述:项目中的具体痛点

先说说当时我们面临的几个核心问题:
组件重复开发严重
不同团队都在重复造轮子,同一个“按钮”组件可能有三四种实现方式,功能相似却风格迥异。样式污染频繁
由于各业务线使用不同技术栈,CSS 冲突特别严重。有时候一个组件引入后整个页面样式就变了。协作效率低
需要维护多个版本的组件库,文档不一致,接口设计混乱。沟通成本极高。框架绑定明显
用 React 实现的组件无法在 Vue 或 Angular 项目中直接使用,必须重新实现,导致资源浪费。升级维护困难
每次主流程组件更新,都要同步多个仓库,修复多个依赖项,稍有不慎就会崩溃。
这些痛点让我们意识到:我们需要一种更底层、更原生、更标准的方式来构建可复用、独立、自洽的组件。
解决方案:Web Components 到底是什么?

如果你不了解 Web Components,我可以简单介绍一下它的核心技术点:
- Custom Elements(自定义元素):你可以注册一个新的 HTML 标签,比如
<my-button>。 - Shadow DOM(影子DOM):提供一个隔离的作用域,确保样式和结构不会影响全局。
- HTML Templates(模板标签):可以在 HTML 中声明一段不立即渲染的内容,方便复用。
- ES Modules 支持:现代浏览器支持通过原生 import 加载 JS 组件。
这几个特性的组合,让 Web Components 成为了一种真正的“组件化原生解决方案”。
我们的目标:
- 用 Web Components 构建统一的 UI 组件库
- 支持多框架环境下的快速接入
- 提供稳定 API 接口,避免样式污染
- 确保性能表现良好,兼容主流浏览器
代码实践:从零开始写一个 Web Component
为了验证可行性,我先写了一个最简单的 Web Component:一个带 icon 的按钮组件。这个组件后来演化成我们整个组件库的基础。
第一步:定义一个 Custom Element
class MyButton extends HTMLElement {
constructor() {
super();
// 创建 Shadow DOM
this.attachShadow({ mode: 'open' });
// 定义按钮内容
const button = document.createElement('button');
button.textContent = this.getAttribute('label') || '点击';
button.style.backgroundColor = this.getAttribute('color') || '#007bff';
button.style.color = '#fff';
button.style.padding = '8px 16px';
button.style.border = 'none';
button.style.borderRadius = '4px';
button.style.cursor = 'pointer';
// 添加 click 事件
button.addEventListener('click', () => {
this.dispatchEvent(new Event('click'));
});
// 将按钮插入 shadow DOM
this.shadowRoot.appendChild(button);
}
}
// 注册组件
customElements.define('my-button', MyButton);
使用方法
<!-- 在任何项目中使用 -->
<my-button label="提交" color="#28a745"></my-button>
<script>
const btn = document.querySelector('my-button');
btn.addEventListener('click', () => {
alert('点击了按钮!');
});
</script>
这个例子虽然简单,但它展示了 Web Components 的核心特性:自定义标签、封装逻辑、隔离样式,以及跨项目复用。
踩坑经验:遇到的那些事儿
说实话,在实际推进过程中,并不是一帆风顺的,我们踩了不少坑。
坑1:Shadow DOM 样式穿透问题
刚开始我们以为 Shadow DOM 是完全封闭的样式作用域,但实际上某些 CSS 属性还是会“漏出去”。比如:
font-family和color会继承自外层- 外层样式也能影响到组件内部
解决办法:
- 手动重置所有继承属性
- 给根节点加一些默认样式以防止继承污染
:host {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
坑2:IE11 兼容性限制
虽然我们主要面向的是现代化浏览器,但有些客户还是坚持使用 IE11,这就带来了麻烦。
- Web Components 在 IE11 上是不支持的
- 需要使用 polyfill,比如 webcomponents.js
我们最终做了以下处理:
- 对于旧浏览器降级显示基础样式
- 使用 feature detection 自动加载 polyfill
- 并设置白名单,只对必要组件启用 polyfill
坑3:动态属性传递与响应性不足
Web Components 本身不具备响应式能力,像属性变更并不会自动触发界面更新。
解决办法:
- 使用
attributeChangedCallback - 监控属性变化并手动更新组件状态
static get observedAttributes() {
return ['label', 'color'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'label' && this.button) {
this.button.textContent = newValue;
}
if (name === 'color' && this.button) {
this.button.style.backgroundColor = newValue;
}
}
坑4:打包和部署流程复杂
最初我们是手动写 ES Module 文件,然后在各个项目中引用,但后来发现这种方式难以维护。
解决方案:
- 用 Rollup 打包 Web Components
- 输出 ES5 + ESM 版本供不同环境使用
- 发布 NPM 包(私有 + 公共)
- 支持按需加载(结合动态 import)
实际项目整合:如何与主流框架共存?
我们并没有要求项目全部切换成 Web Components,而是让它们成为通用基础库的一部分。
以下是我们在不同框架中的集成方式:
在 Vue 项目中使用
Vue 默认支持自定义元素,只需在配置中忽略前缀即可:
Vue.config.ignoredElements = ['my-button'];
然后就能正常用了:
<template>
<div>
<my-button label="保存" @click="handleSubmit" />
</div>
</template>
在 React 项目中使用
React 不太喜欢小驼峰命名,所以你要用全小写的 tag 名:
function App() {
const handleButtonClick = () => {
console.log('clicked!');
};
return (
<div>
<my-button label="提交" color="#dc3545" onClick={handleButtonClick} />
</div>
);
}
需要注意的是,React 的合成事件不能直接绑在 Web Components 上,需要使用 addEventListener:
useEffect(() => {
const btn = document.querySelector('my-button');
btn.addEventListener('click', handleButtonClick);
return () => btn.removeEventListener('click', handleButtonClick);
}, []);
性能优化:别忘了用户体验
虽然 Web Components 本身轻量,但我们依然做了不少性能优化:
- 延迟加载策略:只有在元素首次出现在视口时才进行初始化。
- 减少不必要的重绘:组件内尽可能减少 DOM 操作。
- 使用虚拟滚动/可视区域渲染机制:对于列表类组件尤其重要。
- 静态资源懒加载:图片、字体等大文件都做了 lazy load。
- 构建压缩:Rollup + terser,体积控制在极小范围。
- 监控指标:用 Lighthouse 检查加载速度和交互时间。
另外,我们还开发了一个内部工具来可视化组件加载性能,帮助前端同学快速定位瓶颈。
效果总结:带来了哪些实质收益?
经过几个月的推进,我们取得了不错的效果:
| 项目 | 改进点 | 结果 |
|---|---|---|
| 组件一致性 | 统一组件样式和行为 | 各业务线视觉风格趋于统一 |
| 协作效率 | 组件库集中管理 | 新需求响应速度提高约40% |
| 技术债务 | 减少重复开发 | 组件数量减少了近一半 |
| 兼容性 | 支持多种框架 | 多个项目无缝接入 |
| 性能 | 优化加载和渲染 | 页面加载速度提升,Lighthouse 得分平均增加15分 |
最有意思的是,有位实习生说:“原来不用框架也能写出这么漂亮的组件!” ——这让我意识到,Web Components 让更多人看到了原生的力量。
我的几点建议与注意事项
如果你正在考虑是否采用 Web Components,或者已经在尝试的路上,我想给你以下几个建议:
✅ 适用场景推荐
- 企业级项目,涉及多个团队协作
- 需要跨框架复用的 UI 组件
- 对样式隔离有严格要求的项目
- 长期维护且希望降低技术债的系统
❌ 什么时候不推荐?
- 项目生命周期较短,追求快速上线
- 需要大量状态管理和动画交互(这时候更适合 React/Vue)
- 团队缺乏标准化意识或工程化能力较低
🛠 开发与调试小技巧
- 使用 Chrome DevTools 的“Elements”面板查看 Shadow DOM
- 在组件中加入
console.debug()日志辅助排查 - 利用 Storybook + WC 构建组件文档演示平台
- 用 ESLint 插件规范组件命名规则(如
kebab-case) - 设置合理的语义化标签名,方便语义化和 SEO
尾声:回到原点,看到未来

其实我一直觉得,技术的本质是解决问题,而不是追求酷炫的新名词。这次 Web Components 的尝试,让我们找到了一个既能满足现有需求又能面向未来的解决方案。
虽然它不是万能药,也不是银弹,但在我们项目的特定阶段,它确实带来了很多意想不到的价值。
如果你也正面临组件复用、协作困难、样式污染等问题,不妨试试 Web Components。它不一定适合所有人,但对于需要长期维护、注重架构设计和跨团队协作的项目来说,它真的值得一试。
或许有一天,你会发现:原来最好的组件,就是原生写出来的那一行 <my-component>。
写于深夜咖啡厅,窗外雨声沥沥,耳机里放着 Coldplay 的《The Scientist》——
「我们走得太远,是不是该回头看看?」
但有时候,回头看看,正是为了走得更远。

评论 0