Web Components:原生组件化开发新趋势
写这篇文章的时候,我刚从产品经理转岗做前端开发满三年。说实话,一开始我以为自己会一辈子画原型、写 PRD、跟开发撕需求,结果某天凌晨三点改完第 18 版交互稿后,突然觉得——“要不我自己上手写代码算了?”于是真就转了。现在白天写 React 组件,晚上研究 V8 引擎,周末还刷 LeetCode 准备跳槽……典型的斜杠青年(自封)。
上周五晚上十点半,办公室只剩我和运维老张在加班。他一边喝着红牛一边问我:“你们前端又搞啥新花样?怎么 CI 流水线里突然多了个 web-components 分支?”
我苦笑:“这不是被逼的嘛。”
事情是这样的:我们团队最近接到一个“战略性项目”——把公司内部多个业务系统的 UI 组件统一起来。听起来很美好,但现实很骨感:A 系统用 Vue 2,B 系统用 AngularJS(对,你没看错,2024 年还有人在维护 AngularJS),C 系统倒是用了 React,但版本是 16.x,而且没人敢升级。
更离谱的是,老板要求“所有系统下周上线新主题”,而设计团队给了我们一套包含 30+ 原子组件的设计规范。产品经理(没错,就是我以前的角色)拍胸脯说:“技术上肯定没问题!”
我当时真的想砸电脑。
为什么不用 React?
作为前产品经理 + 现前端,我对 React 有感情。过去三年,我写的每一个页面几乎都离不开它。但这次,React 根本救不了我们。
- 跨框架复用?不存在的。你总不能让 AngularJS 项目去装 React runtime 吧?
- Bundle 体积爆炸。每个系统都要引入一整套 React + 我们的 UI 库,光基础组件就多出 200KB。
- 版本冲突地狱。React 16 和 18 的 Context API 都不一样,更别说 hooks 了。
有一次我在测试环境部署了一个 React 组件,结果老 AngularJS 系统直接白屏,控制台报错:
Uncaught TypeError: Cannot read property 'memo' of undefined
——因为那个系统全局挂了个叫 React 的变量,但其实是他们自己魔改的伪 React。
那一刻我悟了:我们需要的是与框架无关的组件。
Web Components:被遗忘的原生方案
其实 Web Components 这玩意儿早在 2014 年就出来了,但一直不温不火。为啥?因为早期浏览器支持差、API 反人类、生态贫瘠。再加上 React/Vue 的崛起,大家更愿意用“高级”方案。
但今时不同往日。如今:
- Chrome/Firefox/Safari/Edge 全面支持 Custom Elements v1 和 Shadow DOM v1
- caniuse 显示全球覆盖率达 97%+
- 工具链也成熟了(后面会细说)
最关键的是——它原生、轻量、无依赖。一个 Web Component 就是一个 HTML 标签,像 <my-button> 这样直接用,任何框架都能嵌入。
于是我开始调研。没想到,这一试就停不下来。
实战:从零构建一个按钮组件
先别急着喷“Web Components 写法太啰嗦”。确实,原生 API 是有点 verbose,但有了现代工具,体验完全不一样。
我用的是 Lit —— Google 出的轻量级 Web Components 库。它不是框架,只是一个语法糖 + 响应式更新的小工具,打包后只有 5KB。
// my-button.ts
import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';
@customElement('my-button')
export class MyButton extends LitElement {
@property({ type: String }) variant: 'primary' | 'secondary' = 'primary';
@property({ type: Boolean, reflect: true }) disabled = false;
static styles = css`
:host {
display: inline-block;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
:host([variant="primary"]) {
background: #1890ff;
color: white;
}
:host([disabled]) {
opacity: 0.5;
pointer-events: none;
}
`;
render() {
return html`<slot></slot>`;
}
// 处理点击事件
connectedCallback() {
super.connectedCallback();
this.addEventListener('click', (e) => {
if (!this.disabled) {
this.dispatchEvent(new CustomEvent('my-click', { bubbles: true }));
}
});
}
}
关键点解释:
@customElement注册自定义标签@property定义属性,reflect: true会让属性同步到 DOM attribute(方便 CSS 选择):host是 Shadow DOM 的根元素样式<slot>支持内容分发(类似 React 的children)- 事件通过
CustomEvent派发,bubbles: true确保能被外部监听
然后在 HTML 里直接用:
<my-button variant="primary" @my-click="handleClick">
点我!
</my-button>
甚至在 React 里也能无缝集成:
function App() {
const handleClick = () => console.log('clicked!');
return (
<my-button variant="primary" onMyClick={handleClick}>
在 React 里用 Web Component
</my-button>
);
}
不需要任何 wrapper,不需要额外配置。这就是原生的力量。
工具链:告别手写 boilerplate
早期 Web Components 开发痛苦,一大原因是工具链缺失。但现在,生态已经相当完善:
| 工具 | 作用 | 个人体验 |
|---|---|---|
| Lit | 轻量级开发库 | 写起来像 Svelte,但更小 |
| Stencil | 编译型 Web Components 框架 | 适合大型组件库,支持 TSX |
| Vite + @web/dev-server | 开发服务器 | HMR 快得飞起 |
| Playwright | E2E 测试 | 可以直接测 Shadow DOM 内部 |
我最终选了 Lit + Vite。Vite 的插件生态对 Web Components 友好得惊人。比如这个 vite-plugin-wc 插件,能自动注册组件,连 customElements.define 都不用写了。
开发时启动命令:
npm run dev # 热更新快如闪电
构建命令:
npm run build # 输出 esm + systemjs 双格式,兼容老旧系统
最爽的是调试。Chrome DevTools 现在直接支持 Shadow DOM 调试,点开 Elements 面板就能看到内部结构,还能实时修改 CSS。
性能与兼容性:真香警告
上线前我做了对比测试(数据基于 1000 个按钮实例):
| 方案 | Bundle Size (gzip) | 渲染时间 (ms) | 内存占用 (MB) |
|---|---|---|---|
| React + UI Lib | 180KB | 120 | 45 |
| Web Components (Lit) | 25KB | 85 | 28 |
Web Components 不仅体积小,渲染还更快!原因很简单:没有虚拟 DOM diff,直接操作真实 DOM;没有 React reconciler 开销;Shadow DOM 还隔离了样式,避免全局污染。
兼容性方面,我们通过 @webcomponents/webcomponentsjs polyfill 支持到 IE11(虽然老板说 IE 用户不到 0.1%,但合规部门非要支持)。实际只增加了 15KB 的 polyfill 体积。
踩过的坑 & 解决方案
当然,路不可能一帆风顺。分享几个血泪教训:
1. 事件处理的坑
Web Components 默认不会冒泡到外部框架。解决方法是在派发事件时加 { bubbles: true, composed: true }:
this.dispatchEvent(new CustomEvent('change', {
bubbles: true,
composed: true, // 关键!允许穿越 Shadow DOM 边界
detail: { value: this.value }
}));
2. 样式穿透问题
Shadow DOM 隔离了样式,但也导致外部无法定制。解决方案是使用 CSS Shadow Parts:
// 组件内部
render() {
return html`<button part="base">...</button>`;
}
/* 外部定制 */
my-button::part(base) {
background: hotpink;
}
3. SSR 支持
Web Components 本身不支持服务端渲染。但我们用 Stencil 生成的组件库自带 hydrate 功能,配合 Next.js 或 Nuxt 可以实现 SSR + CSR 混合。
效果如何?
上周双11大促,我们的新组件库正式上线。结果:
- 所有系统 UI 一致性 100% 达成 ✅
- Bundle 体积平均减少 60% ✅
- 前端团队不再互相甩锅(Vue 组和 React 组终于握手言和)✅
最让我感动的是,那个 AngularJS 系统的老师傅跑来跟我说:“小伙子,你这按钮在我这儿跑得挺溜啊!”
最后:给想跳槽的你一点建议
写这篇文章时,我正投着简历。面试官问:“你为什么研究 Web Components?”
我说:“因为我受够了框架战争。用户不在乎你用 React 还是 Vue,他们只想要一个快、稳、好看的页面。”
如果你也在准备跳槽,我强烈建议你了解一下 Web Components。它不是要取代 React,而是提供一种更底层、更通用的组件化思路。大厂如 GitHub、Salesforce、Adobe 都在用它构建 Design System。
而且,懂 Web Components 的前端,在面试时真的能加分——毕竟,能讲清楚 Shadow DOM 和 Custom Elements 的人,不多。
PS:有人问我“产品经理转技术会不会吃亏”?我的回答是:恰恰相反。正因为当过 PM,我才更懂用户、更懂协作、更知道什么技术值得投入。技术只是手段,解决问题才是目的。
好了,我去改简历了。希望下一家公司,不要再让我凌晨三点改交互稿 😅

评论 0