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

作为前端开发者,我们每天都在与“复用”打交道。从早期的jQuery插件,到后来的React、Vue等现代框架,大家都在尝试更高效地组织代码、提高团队协作效率。但不管技术如何演进,组件化开发始终是不变的主题。
而近年来,随着浏览器对Web Components规范的支持不断完善,越来越多团队开始尝试这一“原生”的组件化方案。我也在去年参与的一个项目中首次引入了Web Components,从最初的怀疑到后期的依赖,整个过程让我对它的理解发生了翻天覆地的变化。
今天我就想和大家分享一下这段经历,说说我为什么选择它,遇到了哪些坑,最后取得了什么样的成果,以及给正在考虑是否采用Web Components的你一些实用建议。
问题描述:项目背景与挑战

项目背景
我们的项目是一个大型的跨部门系统集成平台。前端由多个独立模块组成,分别由不同团队负责开发维护。这些模块之间有很多UI控件需要复用,比如按钮、输入框、弹窗、表单验证组件等等。
最初的技术选型是使用Vue + Vuex + Vue Router,配合微前端架构(qiankun)实现模块之间的通信和整合。但随着项目的推进,我们发现:
- 组件复用成本高:每个团队都在重复造轮子,很多组件功能相似但风格不一
- 版本管理复杂:由于各个模块使用不同版本的公共组件库,出现样式/行为不一致的问题
- 性能负担加重:由于每个模块都自带一份框架副本,整体包体积增大,加载时间变长
此外还有一个比较棘手的问题:部分老旧系统必须运行在IE11环境下,这就导致我们不能轻易升级或引入较新的框架特性。
于是,我们在一次技术评审会上开始讨论一个可能的替代方案——是否可以利用原生的Web Components来解决这些问题?
解决方案:为什么是Web Components?

其实我一开始对Web Components并不感冒,觉得它只是一个玩具级的API,真正开发还是得靠成熟的框架。但在实际调研后我发现,Web Components并不是取代框架,而是提供了一个更通用、更底层的组件封装机制。
它有几个关键优势打动了我:
✅ 原生支持,无需框架依赖
这可能是最吸引人的地方。Web Components基于Custom Elements API构建,不需要任何框架即可运行。这意味着我们未来可以轻松将这些组件嵌入到React、Vue甚至Angular项目中,极大提高了可复用性。
✅ 风格隔离,避免CSS污染
Shadow DOM的引入让每一个组件都有自己的样式作用域,不会被外部全局样式污染,也避免了内部样式影响其他模块。这个特性对于多团队协同开发特别重要。
✅ 跨项目共享,统一版本控制
通过NPM发布自定义元素的方式,我们可以统一所有项目使用的组件库版本,从而保证视觉和行为的一致性。
✅ 性能优化潜力大
相比每个模块都引入一个完整的框架,纯HTML/CSS/JS构建的Web Component显然更加轻量,尤其适合那些只关心视图层的小型模块。
于是我们决定:以一个小的功能模块为试点,尝试迁移到Web Components技术栈,并评估其可行性。
实践落地:搭建第一个可复用组件

我们选了一个相对独立的功能模块作为试点:用户反馈评分组件。这是一个用于让用户打星评价的产品反馈收集控件,具备交互性和一定的业务逻辑,适合作为迁移目标。
技术栈选择
- 核心框架:原生Web Components(customElements + ShadowDOM)
- 构建工具:Rollup.js
- 样式预处理器:SCSS
- 测试框架:Jest + Puppeteer
为了兼容旧系统,我们还做了Babel降级处理(转译ES6+语法),并加入了polyfill支持。
组件结构设计
class FeedbackRating extends HTMLElement {
constructor() {
super();
this._rating = 0;
this.shadow = this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
this.setupEventListeners();
}
render() {
const style = document.createElement('style');
style.textContent = `
.stars {
display: flex;
gap: 5px;
cursor: pointer;
}
.star {
font-size: 24px;
}
.filled {
color: gold;
}
`;
const container = document.createElement('div');
container.className = 'stars';
for (let i = 1; i <= 5; i++) {
const span = document.createElement('span');
span.className = 'star';
span.setAttribute('data-rating', i);
span.innerHTML = '★';
container.appendChild(span);
}
this.shadow.innerHTML = '';
this.shadow.appendChild(style);
this.shadow.appendChild(container);
}
setupEventListeners() {
this.shadow.querySelectorAll('.star').forEach(star => {
star.addEventListener('click', (e) => {
this._rating = parseInt(e.target.getAttribute('data-rating'), 10);
this.dispatchRatingChange();
this.updateStars();
});
star.addEventListener('mouseover', () => {
this.highlightStars(parseInt(star.getAttribute('data-rating'), 10));
});
star.addEventListener('mouseleave', () => {
this.updateStars();
});
});
}
updateStars() {
this.shadow.querySelectorAll('.star').forEach(star => {
const value = parseInt(star.getAttribute('data-rating'), 10);
star.classList.toggle('filled', value <= this._rating);
});
}
highlightStars(rating) {
this.shadow.querySelectorAll('.star').forEach(star => {
const value = parseInt(star.getAttribute('data-rating'), 10);
star.classList.toggle('highlighted', value <= rating);
});
}

dispatchRatingChange() {
const event = new CustomEvent('rating-change', {
detail: { rating: this._rating },
bubbles: true,
composed: true
});
this.dispatchEvent(event);
}
}
customElements.define('feedback-rating', FeedbackRating);
使用方式
<feedback-rating></feedback-rating>
<script>
document.querySelector('feedback-rating').addEventListener('rating-change', (e) => {
console.log('用户评分:', e.detail.rating);
});
</script>
看起来是不是很简单?没错,这就是Web Components的魅力所在:高度封装、便于使用、无需引入复杂框架。
踩坑经验:从兴奋到崩溃再到豁然开朗
虽然整体体验还不错,但在开发过程中我们也踩了不少坑,这里我挑几个印象最深的分享给大家。
🚨 Shadow DOM样式穿透问题
我们一开始在外部页面写了一些CSS样式,试图修改组件的内部样式,结果完全不起作用——这是因为Shadow DOM默认是封闭的。
解决办法有两种:
- 在Shadow DOM内直接写样式(推荐)
- 使用
::part()暴露指定节点供外部访问
比如我们在组件里这样写:
container.setAttribute('part', 'stars-container');
然后在父页面中可以通过以下方式覆盖样式:
feedback-rating::part(stars-container) {
gap: 10px;
}
⚠️ 浏览器兼容性问题
虽然Chrome等现代浏览器已经较好支持Web Components v1规范,但我们还需要兼容IE11,这时候就必须加polyfill。
我们最终选择了SkateJS/web-component-shim这个社区维护较好的方案,效果不错。
使用方式如下:
<script src="path/to/webcomponents-bundle.js"></script>
需要注意的是,引入之后要添加defer属性,否则某些DOM操作会报错。
🐞 开发调试不够友好
相比于Vue DevTools那种所见即所得的调试方式,Web Components的调试确实有点原始。不过我还是总结了几个小技巧:
- 使用浏览器的Elements面板查看Shadow DOM结构
- 在组件构造函数中打印this,看看生命周期有没有正确触发
- 利用console.table()输出对象数据,更清晰
- 使用Jest做单元测试时,模拟事件触发判断状态是否变化
效果总结:迁移后的收益
经过大约一个月的重构和灰度上线,我们观察到了以下几个显著提升:
| 指标 | 迁移前 | 迁移后 |
|---|---|---|
| 页面首次加载速度 | 3.8s | 2.6s |
| 包体积大小 | 2.4MB | 1.6MB |
| 公共组件版本一致性 | 差 | 优 |
| 多团队协作难度 | 较高 | 显著降低 |
| 可维护性 | 中等 | 高 |
更重要的是,当我们需要把这个组件集成到老系统的时候,不再需要引入整个Vue环境,只需要一个简单的脚本标签就能搞定:
<script src="/dist/feedback-rating.min.js" defer></script>
<feedback-rating></feedback-rating>
这种“零门槛”接入方式受到了运维团队和产品组的高度认可。
经验分享:写给准备上车的你
如果你也在考虑使用Web Components,或者已经在试用了,那我可以把我踩过的坑和收获的经验总结成以下几点建议:
🎯 适用场景
- 多项目间需要共享组件
- 存在低配设备或旧浏览器需求
- 不想被框架绑架,希望保持灵活性
- 对性能敏感,追求更快的加载速度
🧱 技术建议
- 合理使用Shadow DOM和CSS Part,不要过度封闭,适当保留样式可定制能力
- 构建流程要自动化,用Rollup/Babel打包,结合TypeScript可以获得更好的类型保障
- Polyfill策略要灵活,按需加载而非全部引入,减少性能损耗
- 组件状态管理要小心,如果是纯展示组件还好,如果有复杂状态,建议搭配简单状态管理方案(如Redux-lite)
💡 心态调整
刚开始接触Web Components会觉得“怎么又回到手写DOM的时代了”,但慢慢你会发现,它其实是一种更高层次的抽象。你不再受限于某个特定的生态,而是拥有了一种可以在任何前端体系中工作的“元能力”。
结语:原生的力量不容忽视
如今回顾这次技术转型,我觉得最大的收获不是提升了多少性能,也不是节省了多少内存,而是让我重新认识到原生的力量。很多时候我们习惯性依赖框架,却忽略了浏览器本身提供的能力。
Web Components并不是银弹,但它提供了一个干净、简洁、跨框架的组件构建方式。特别是在当前前端生态百花齐放的情况下,能够有一套统一的组件通信语言,是非常有价值的事情。
如果你正苦于组件难以复用、框架绑定过重、性能优化乏力,不妨试试Web Components,也许它正是你需要的那一把钥匙。
别忘了,最好的工具永远是那个既能解决问题,又让人舒服的家伙。
作者注:文章中提到的完整示例代码已上传至GitHub仓库,欢迎Star交流:https://github.com/yourname/web-components-examples

评论 0