Web Components:原生组件化开发的新趋势
开篇:背景与缘由

去年年底,我们团队接手了一个大型的前端重构项目。项目的前身是一个用了七八年的旧系统,代码结构混乱、组件复用性差,维护成本非常高。更麻烦的是,这套系统已经被部署到多个客户环境里,不同客户间还存在部分定制化需求。
面对这种情况,团队内部对于技术选型产生了分歧:有人主张继续使用现有的 Vue,也有人想试试 Angular 的封装能力。而我提出了一个看似“保守”的建议——要不要尝试一下 Web Components?
起初大家对这个提议有些犹豫,毕竟当时主流框架如 React 和 Vue 都已经做得非常成熟,Web Components 听起来就像是浏览器原生的“冷门玩法”。但经过深入讨论和验证后,我们最终决定采用 Web Components 来作为这次重构的核心方案。
现在回过头来看,这不仅是一次技术上的小突破,更是让我重新认识了组件化开发的本质。
问题描述:我们在老项目中遇到了什么挑战?

复杂度高、难以维护
原来的项目使用的是 jQuery + 原生 JavaScript 混合搭建的一套 SPA 架构,里面充斥着大量全局变量、DOM 直接操作、状态管理混乱。每次修改一个功能点,都像在拆地雷,稍有不慎就会影响其他模块。
而且由于历史原因,很多 UI 组件是直接复制粘贴写的,虽然看起来一样,但实际逻辑完全不同。比如一个“表格”组件,在不同的页面中竟然有不同的样式、不同的数据绑定方式,甚至还有不同的事件触发机制。这种“伪复用”反而成了负担。
客户定制难统一
由于我们的产品部署在多个客户环境中,每个客户都有自己的一些个性化需求。这些定制需求往往只涉及某个 UI 元素的显示逻辑或交互方式,但由于所有组件都是紧耦合的,想要做微调就必须 fork 整个组件甚至整个页面结构,导致后续升级困难重重。
技术栈锁定严重
原来的老项目基于某版本 Vue(还是 options API),但我们内部其实已经有新项目开始使用 React 和 TypeScript。这就造成一个问题:如果我们要做一个通用组件库,应该基于哪个框架写?不同技术栈之间的兼容性如何保证?
这也是我最初提出 Web Components 的关键原因之一——它是一种真正的跨框架通信语言。
解决方案:为什么选择 Web Components?
Web Components 是一组标准规范,主要包括:

- Custom Elements:定义 HTML 标签
- Shadow DOM:实现样式的封装和隔离
- HTML Templates:声明式模板支持
- ES Modules:模块化打包基础
它们并不属于某个具体的框架,而是原生浏览器的能力,只要你的项目能跑 JS,就能运行 Web Components。
我之所以推荐 Web Components,有几个关键点打动了我:
- 天然跨框架:你可以把它嵌入到 Vue、React、Angular,甚至是 jQuery 页面中。
- 可封装性强:通过 Shadow DOM 实现样式和行为的隔离,避免全局污染。
- 渐进式集成:可以逐步将老项目中的模块替换为 Web Components,而不必全盘重写。
- 轻量级无依赖:不需要引入庞大框架,适合构建小型工具类组件库。
- 未来趋势明确:越来越多的大厂开始重视,并构建基于 WC 的 UI 库(比如 GitHub Primer、Salesforce Lightning Web Components)。
我们是怎么做的?
第一步:选好脚手架工具
Web Components 虽然是原生特性,但真要从头开发也会遇到不少坑,比如:
- 如何组织目录结构?
- 如何调试?
- 如何支持现代语法(TypeScript、CSS 变量、ES6 模块)?
- 如何打包输出并集成到各种项目中?
我们调研了几种方式,最终选择了 OpenWC 提供的脚手架。它提供了一整套开箱即用的构建流程,包括:
- 使用 Rollup 打包
- 支持 ES Module 和 SystemJS
- 内置 LitElement 和一些优秀的 Web Components 辅助库
- 单元测试、lint 工具一应俱全
此外,我们还结合 Storybook for Web Components 来构建我们的组件文档系统,这对于后期交接和客户沟通至关重要。
第二步:从核心组件入手
我们决定先从几个高频使用的组件开始封装:按钮、对话框、分页器、表格等。目标不是追求大而全,而是确保这些组件在多种场景下都能稳定运行。
举个简单的例子,我们封装了一个 <my-button> 组件:
class MyButton extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({ mode: 'open' });
this._shadowRoot.innerHTML = `
<style>
:host {
display: inline-block;
background-color: var(--button-bg, #007bff);
color: white;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}
:host([disabled]) {
opacity: 0.5;
pointer-events: none;
}
</style>
<slot></slot>
`;
this.addEventListener('click', () => {
if (!this.disabled) {
this.dispatchEvent(new CustomEvent('my-click', { detail: {} }));
}
});
}
get disabled() {
return this.hasAttribute('disabled');
}
set disabled(val) {
val ? this.setAttribute('disabled', '') : this.removeAttribute('disabled');
}
}
customElements.define('my-button', MyButton);
这段代码虽然不复杂,但它展示了 Web Components 的几个特点:
- 封装了内联样式,防止外部干扰
- 支持通过 CSS Variables 自定义样式
- 用
slot实现内容透传 - 用自定义事件进行通信
我们还给这个组件写了 Storybook 的 Demo:
export const Primary = () => html`
<my-button @my-click=${() => alert('clicked!')}>提交</my-button>
`;
这样不仅能展示基本样式,还能模拟真实业务场景下的交互行为。
第三步:处理样式冲突与性能优化
Web Components 虽然自带 Shadow DOM,但也不是万能药。我们在实际开发过程中遇到了几个典型问题:
1. 主题样式穿透问题
有时候我们需要让 Web Components 支持父级的主题变量(比如颜色、字体大小)。这时候我们会使用 CSS 变量,配合 ::part 伪元素来暴露子元素样式接口:
:host {
--inner-text-color: red;
}
<span part="text">${this.text}</span>
在外部样式表中就可以:
my-button::part(text) {
color: var(--inner-text-color);
}
2. 性能瓶颈分析
我们发现当页面上同时渲染几十个 Web Components 时,会有轻微卡顿。于是我们用 Chrome DevTools 的 Performance 面板做了分析,发现是因为某些组件频繁触发 reflow。
解决办法是对组件生命周期进行优化,例如使用 debounce 控制更新频率,或者通过 MutationObserver 进行响应式更新控制。
3. 跨框架集成细节
在 Vue 或 React 中使用 Web Components 时需要注意以下几点:
- 属性绑定需使用
:attr或setAttribute方式传递非字符串类型 - 事件需要手动监听并绑定回调函数
- 使用 JSX/TSX 时可能需要额外 polyfill 或配置 Babel 插件(如
@babel/plugin-transform-react-jsx)
比如在 React 中使用:
function App() {
const onMyClick = useCallback(() => {
console.log('Web Component Clicked!');
}, []);
return (
<div>
<my-button onClick={onMyClick}>Submit</my-button>
</div>
);
}
如果事件不能正常触发,可以尝试加 dangerouslySetInnerHTML 或用 addEventListener 动态绑定。
成果与收益

经过半年多的努力,我们成功将原有系统的大部分通用 UI 组件替换为 Web Components。整体效果出乎意料的好:
| 指标 | 替换前 | 替换后 |
|---|---|---|
| 代码冗余率 | 40%+ | <10% |
| 组件加载时间 | 600ms+ | ~180ms |
| 团队协作效率 | 低 | 显著提升 |
| 定制化交付周期 | 1-2周 | 3天内 |
最重要的是,我们终于建立了一个真正意义上的跨项目、跨框架、跨客户的 UI 组件库,并且这套体系几乎不依赖任何第三方框架,极大降低了后期维护成本。
更难得的是,Web Components 的封装性和灵活性让我们在应对客户个性化需求时更加从容。我们可以快速构建新分支,修改少量样式和逻辑即可适配,而不必大规模改动代码结构。
我的经验分享
如果你也在考虑是否尝试 Web Components,我想分享几个心得体会:
✅ 不要用 Web Components 解决所有问题
它并不是银弹,更适合用于构建UI 组件层的基础设施,而不是用来替代 Vue/React 等应用层框架。
如果你要做的是复杂的单页应用、状态管理、服务端渲染……那它肯定不是首选。但在构建组件库、设计系统、微前端通信等领域,它的优势非常明显。
✅ 从简单组件开始,循序渐进
不要一开始就追求完美。先封装几个简单控件(比如按钮、输入框),熟悉生命周期管理和 Shadow DOM 的特性,再逐步封装复杂组件(如表格、树形菜单)。
✅ 注意浏览器兼容性
目前大多数主流浏览器都已支持 Web Components,但如果你的产品还需要支持 IE11,那就需要引入 polyfill(比如 @webcomponents/webcomponentsjs)。不过这类项目一般也比较有限,多数情况我们可以忽略。
✅ 配套工具链不能少
Web Components 本身是标准化规范,但它并不等于开箱即用。你需要一套完整的工程化体系:编译、打包、文档生成、单元测试,以及调试工具。
我们用到的工具有:
- Storybook:文档 & 示例中心
- Rollup:打包构建
- ESLint + Prettier:代码规范
- Karma/Mocha:单元测试
- Chrome DevTools:调试利器
✅ 调试技巧:善用 DevTools
你可以直接在 Elements 面板中查看 Web Components 的 Shadow DOM 结构,也可以右键点击组件节点选择 “Reveal in Elements Panel”,快速定位到具体元素。
如果你想查看组件注册情况,可以在 Console 输入:
window.customElements.get('my-component')
结语:为什么说它是未来的趋势?
说实话,在刚接触 Web Components 时,我也曾质疑它的实用性。毕竟主流框架生态太强大了,Vue 也好,React 也罢,各自都有庞大的社区和丰富的生态。但经历过这次项目实战后,我才真正意识到它背后所代表的意义:
Web Components 是 Web 标准的延伸,而不是某种“框架的附属品”。
它的价值在于,它让我们不再受限于技术栈的边界,而是回归 Web 原本的设计哲学:开放、标准化、跨平台、互操作性。
如果你问我未来还会不会继续使用它?我的答案是肯定的。
当然,我不会盲目推广它去取代一切现有方案。但如果哪天你面临类似的问题:组件复用难、框架不统一、维护成本高……不妨试着打开 DevTools,写下你的第一个 Web Component,亲自感受下“原生组件化”的魅力。
相信我,那种“简洁而强大”的感觉,真的很爽。

评论 0