Web Components:原生组件化开发新趋势 —— 一次重构之旅的思考与实践
开篇:从一个老项目的困境说起

如果你现在负责维护一个多年未更新的前端项目,你会怎么做?三年前我接手了一个公司内部使用的管理后台系统,它最初是用 jQuery 起家的,后期虽然陆续引入了 Vue 的部分功能,但整体结构依然混乱。页面之间有很多重复的交互逻辑和 UI 组件,比如日期选择器、表单验证、模态框等,每个地方实现方式都不一样,甚至同一个按钮在不同页面长得还不太像。
这让我意识到一个问题:我们在构建现代 Web 应用时,越来越需要一种统一而灵活的方式去组织这些组件。 框架虽好,但过度依赖框架也带来了技术债务和迁移成本的问题。有没有一种更轻量、更通用、更原生的解决方案?于是,我开始接触并研究 Web Components 技术栈。
本文就结合我个人在一个实际项目中的应用经验,聊聊 Web Components 是什么,为什么值得你关注,以及我在落地过程中踩过的那些坑和收获的心得。
问题描述:组件复用困难 + 技术栈割裂

在那个管理后台的重构过程中,我们遇到几个明显的问题:
- 组件不统一:很多 UI 组件都写在页面里,没有良好的封装和命名规范,导致风格不统一,维护困难。
- 技术栈混杂:有的模块用 Vue,有的直接用 jQuery,还有一些干脆是纯 HTML/JS 实现,代码耦合严重。
- 跨项目复用困难:一些基础组件如搜索框、分页控件等希望复用到其他项目中,但因为依赖具体框架或样式库,很难抽出。
- 团队协作效率低:新人加入后,面对各种“自由发挥”的组件实现方式,学习成本高。
我们迫切需要一种既能独立运行,又能无缝集成到任意框架(Vue、React、Angular 甚至原生 JS)中的组件方案,这样既能统一 UI 风格,又能提升组件可维护性与复用性。
解决方案:为什么选择 Web Components?

这个时候,我开始调研 Web Components,并决定将其作为项目重构的核心技术之一。
什么是 Web Components?
Web Components 是一套浏览器原生支持的组件开发标准,主要包括以下几个核心特性:
- Custom Elements(自定义元素)
- Shadow DOM(影子 DOM)
- HTML Templates(模板标签)
- ES Modules(模块化 JavaScript)
你可以把它理解为“浏览器提供的组件化能力”,不需要任何第三方框架就能创建封装良好、样式隔离、行为可控的组件。
为什么选择它?
相比传统框架组件或 UI 库,Web Components 有几个显著优势:
| 特点 | 说明 |
|---|---|
| 原生支持 | 浏览器内置 API,不依赖第三方框架 |
| 样式隔离 | Shadow DOM 提供真正的样式封闭机制,避免样式冲突 |
| 渐进增强 | 可以逐步引入,不影响现有页面结构 |
| 真正跨框架 | 无论是 Vue、React 还是 jQuery 页面,都能使用 |
| 扩展性强 | 支持生命周期回调,易于扩展和组合 |
我们团队最终选择 Web Components 的理由总结成一句话就是:我们需要一种能长期稳定、兼容性强、真正解耦的组件方案。
项目实践:重构一个搜索组件


为了验证可行性,我决定从最简单的一个组件入手 —— 一个通用搜索框组件。这个组件在各个页面都有出现,但实现各不相同。需求如下:
- 支持输入搜索关键词
- 支持 placeholder
- 支持点击清空
- 支持键盘事件触发搜索
- 样式统一,不能被全局 CSS 影响
技术选型和工具链搭建
- 构建工具:使用 Vite 搭建开发环境,配合 ES Module 原生导入导出。
- 打包工具:使用 Rollup.js 进行构建,生成 ESM 和 UMD 两种格式。
- 开发流程:本地调试使用 Vite,打包发布到 npm。
- 组件测试:使用 Web Component Tester 和 Storybook 编写 UI 示例。
核心组件实现代码
class SearchInput extends HTMLElement {
constructor() {
super();
// 创建 Shadow DOM
this.shadow = this.attachShadow({ mode: 'open' });
// 创建模板内容
const template = document.createElement('template');
template.innerHTML = `
<style>
:host {
display: inline-block;
}
.search-input {
padding: 8px 12px;
border: 1px solid #ccc;
border-radius: 4px;
outline: none;
width: 200px;
}
.search-input:focus {
border-color: #55aaff;
}
</style>
<input class="search-input" type="text" placeholder="${this.getAttribute('placeholder') || '请输入关键字'}">
<button class="clear-btn">×️</button>
`;
// 将模板插入 Shadow DOM
this.shadow.appendChild(template.content.cloneNode(true));
// 获取真实 DOM 元素
this.input = this.shadow.querySelector('.search-input');
this.clearBtn = this.shadow.querySelector('.clear-btn');
// 绑定事件
this.input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
this.dispatchEvent(new CustomEvent('search', { detail: this.input.value }));
}
});
this.clearBtn.addEventListener('click', () => {
this.input.value = '';
this.dispatchEvent(new CustomEvent('clear'));
});
}
connectedCallback() {
// 可选:组件挂载时执行的逻辑
}
disconnectedCallback() {
// 可选:组件卸载时执行清理逻辑
}
}
// 注册组件
customElements.define('my-search-input', SearchInput);
使用方式
<my-search-input placeholder="请输入用户名"></my-search-input>
<script>
document.querySelector('my-search-input').addEventListener('search', (e) => {
console.log('用户输入了:', e.detail);
// 执行搜索逻辑
});
document.querySelector('my-search-input').addEventListener('clear', () => {
console.log('用户清空了输入框');
});
</script>
通过这段代码,我们创建了一个完全样式隔离、行为清晰的搜索组件,可以轻松嵌入到任何页面中。最关键的是,它的样式不会受到页面上其他 CSS 的影响 —— 因为我们使用了 Shadow DOM。
踩坑经历:那些年我们一起走过的弯路
尽管 Web Components 的理念很好,但在落地过程中还是遇到了不少挑战,下面是我亲身经历的一些坑:
1. 浏览器兼容性问题
虽然大多数现代浏览器都已经原生支持 Web Components,但在 IE11 上几乎无法使用,甚至连 Polyfill 都不是很稳定。我们的产品还需要在部分企业内网环境中运行,这确实是个头疼的问题。
✅ 解决方法:采用 SkateJS 或者官方推荐的 webcomponents.js,并在构建流程中根据目标浏览器动态添加 Polyfill。
2. CSS 样式穿透问题
虽然 Shadow DOM 可以隔离样式,但有时候我们需要从外部控制组件的主题色或者字体大小等,这就涉及到了“开放样式接口”的问题。
✅ 解决方法:使用 CSS Variables(即
--primary-color)来提供可配置项。例如:
:host {
--primary-color: #3366cc;
}
然后在 Shadow DOM 内部使用:
.search-input:focus {
border-color: var(--primary-color);
}
这样外部可以通过设置宿主元素的变量值来修改组件样式。
3. 开发调试不够友好
由于 Web Components 是通过 customElements 注册的方式创建的,很多时候你会发现 Chrome DevTools 中看不到组件的具体结构,尤其是用了 Shadow DOM 之后。
✅ 解决方法:
- 在 DevTools 设置中启用 “Show user agent shadow DOM” 功能;
- 使用 Storybook for Web Components 构建组件演示文档,提升开发体验。
4. 性能优化不可忽视
虽然组件本身轻巧,但如果大量使用复杂组件或频繁操作 DOM,也会影响性能,特别是在数据密集型场景下。
✅ 解决方法:
- 合理使用虚拟滚动(Virtual Scrolling),减少不必要的 DOM 插入;
- 使用 IntersectionObserver 实现懒加载;
- 对于大型组件,考虑引入 React/Preact 来做状态管理,而不是全靠原生逻辑。
效果总结:组件化的价值逐渐显现
经过几个月的努力,我们将原本散落在多个页面中的 UI 组件逐步迁移到 Web Components 方案中,取得了不错的效果:
- 统一风格:所有组件样式一致,设计语言得到统一
- 复用率提高:大部分组件可以在不同项目中直接使用,不再重复开发
- 开发效率提升:新人更容易理解代码结构,减少了沟通成本
- 维护成本降低:出现问题可以直接定位到某个组件内部,而非整个页面
- 性能优化空间更大:基于组件粒度进行性能监控和优化,更加精细
更重要的是,这些组件不再绑定某个特定的技术栈,我们可以轻松地将它们用在 Vue 项目中、React 项目中、甚至是 SSR 服务端渲染中。
经验分享:给正在探索 Web Components 的你
如果你也在尝试或准备使用 Web Components,这里有一些建议想送给你:
1. 不要一开始就追求完美组件
先做出最小可行版本,再慢慢迭代。毕竟这是一个相对新的领域,社区生态仍在成长中。不要一开始就想实现完美的 state 管理或复杂的事件通信。
2. 合理权衡是否使用 Shadow DOM
虽然它提供了样式隔离的能力,但也会增加组件与外界交互的成本。某些场景下(如动态主题切换),反而更适合普通 DOM 加命名空间的方式来管理样式。
3. 组件也要有文档和测试
即使是小而美的组件,也需要完善的文档和单元测试。可以借助 Web Component Tester 或 Jest 进行测试,Storybook 做展示。
4. 与主流框架共存没问题
Web Components 本就是用来弥补框架之间的差异的。在 Vue 或 React 项目中使用它们,只需要当作普通的 HTML 标签即可。如果涉及双向绑定或异步状态,可以借助属性传递与事件驱动。
5. 性能是关键考量因素
对于高频渲染或大数据量场景,建议做好性能评估。必要时也可以用 React/Preact 封装部分 Web Components,利用其虚拟 DOM 的优势。
结语:回到初心,拥抱原生的力量
回过头来看,当初选择 Web Components 并不是为了赶潮流,而是因为它真的解决了我们在实际工作中遇到的问题。它让我们回归到 Web 本身的本质——HTML、CSS 和 JS,而不是依赖某一个框架。
在这个万物皆组件的时代,Web Components 虽然不像 React 或 Vue 那样流行,但它代表了一种“更轻、更原生”的可能性。它是浏览器赋予我们的能力,也是未来跨框架协作的一种方向。
也许你现在还不打算全面转向 Web Components,但我希望你能多了解一下它的魅力。它不一定适合每一个项目,但当你真正用它解决一个具体问题的时候,你会感受到那种纯粹的快乐 —— 就像第一次写出可以重复使用的函数一样。
最后附上一句感悟送给大家:技术没有好坏,只有合适与否。真正的高手,是在合适的场景,用合适的方式,把事情做得漂亮。

评论 0