Web Components:原生组件化开发新趋势
上周五晚上十点半,我拖着疲惫的身体走出公司大楼,上海的深秋冷得刺骨,但脑子里还在盘旋一个问题:“我们这个项目到底能不能用 Web Components 来重构?” 说来惭愧,作为一个在一线大厂(名字就不说了,反正你知道是哪家)摸爬滚打三年多的前端码农,天天喊着“高内聚低耦合”、“微前端架构”、“跨团队复用”,结果自己的代码库却越来越像一锅乱炖——CSS 冲突、JS 全局污染、Vue 和 React 组件混搭,连测试同学都吐槽:“你们这页面点一下崩三次,是不是又改了啥全局变量?”
996 的福报让我没时间系统学习新技术,但又逼着我去学。 这不,最近面试官开始问:“Web Components 了解吗?和框架组件有啥区别?” 我支支吾吾,答得磕磕绊绊。回家路上刷脉脉,看到同行说:“不懂 WC,简历直接进回收站。” 当时心里就咯噔一下——完了,又要补课了。
于是,趁着双休(其实是只休了一天半,周日晚上还得上线),我硬着头皮啃文档、写 demo、看源码。今天这篇博客,就是我在“代码人生”的泥潭里挣扎出来的开发心得,希望能帮到同样被 deadline 追着跑、又被技术焦虑压得喘不过气的你。
为什么突然要搞 Web Components?
事情起源于一个“史诗级”需求:公司要做一个 B 端中台系统,需要同时被五个不同业务线集成。每个业务线用的技术栈还不一样——有的用 Vue 2,有的用 React 17,还有两个老古董项目还在 jQuery 时代。产品经理拍胸脯说:“你们前端搞个通用组件就行,别管他们用啥框架!” 我当场就想翻白眼——说得轻巧,你行你上啊?
更离谱的是,运维大哥还加了一句:“对了,别打包太大,首屏加载不能超过 1.5s,不然老板要骂。” 好家伙,既要马儿跑,又要马儿不吃草。
这时候,团队里有个老哥提了一嘴:“要不试试 Web Components?原生支持,不用依赖框架,还能 Shadow DOM 隔离样式。” 我一开始嗤之以鼻:“原生?那不是十年前的东西吗?性能能行?” 但架不住现实毒打——微前端方案太重,iframe 又丑又卡,最后还是硬着头皮上了。
Web Components 是什么?真能打?
简单说,Web Components 是一套浏览器原生的组件化标准,由四个核心 API 组成:
- Custom Elements:自定义 HTML 标签
- Shadow DOM:样式和 DOM 隔离
- HTML Templates:声明式模板
- ES Modules:模块化加载
重点来了——它不需要任何框架! 浏览器直接支持(现代浏览器基本全覆盖),意味着你的组件可以像 <button> 一样,在任何地方使用,不管宿主页面是 Vue、React 还是纯静态页。
举个最简单的例子:
class MyButton extends HTMLElement {
constructor() {
super();
// 创建 Shadow DOM
const shadow = this.attachShadow({ mode: 'open' });
// 定义内部结构
shadow.innerHTML = `
<style>
button {
background: #4CAF50;
color: white;
border: none;
}
</style>
<button>点击我</button>
`;
}
}
// 注册自定义元素
customElements.define('my-button', MyButton);
然后在 HTML 里直接写:
<my-button></my-button>
搞定!没有 webpack,没有 babel,没有 node_modules,清爽得像刚洗完澡。
实战踩坑:从“真香”到“真难”
理想很丰满,现实很骨感。当我把上面那个 MyButton 丢进真实项目时,问题接踵而至。
坑 1:生命周期太原始
Web Components 的生命周期只有 connectedCallback(挂载)、disconnectedCallback(卸载)等几个钩子,没有类似 React 的 useEffect 或 Vue 的 watch。比如我想监听属性变化:
static get observedAttributes() {
return ['label'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'label') {
this.shadowRoot.querySelector('button').textContent = newValue;
}
}
写起来啰嗦不说,还容易漏。更惨的是,属性必须是字符串!想传个对象?自己 JSON.stringify 吧。想传个函数?别想了,原生不支持。
吐槽:产品经理看到这里肯定要说:“你们前端怎么连个回调都搞不定?” —— 哥,这是浏览器标准,不是我不努力!
坑 2:样式穿透与主题定制
Shadow DOM 虽然隔离了样式,但也带来了新问题:外部无法修改内部样式。用户想换个按钮颜色?除非你提前用 CSS 变量暴露接口:
/* 组件内部 */
button {
background: var(--my-button-bg, #4CAF50);
}
/* 外部使用 */
my-button {
--my-button-bg: blue;
}
这招可行,但需要提前规划好所有可定制点。一旦漏了,就得发版。线上事故预警:有一次我忘了暴露 loading 状态的颜色变量,客户投诉按钮加载时看不见,差点背锅。
坑 3:事件通信太原始
子组件想通知父组件?只能 dispatch 一个 CustomEvent:
this.dispatchEvent(new CustomEvent('click', {
detail: { id: 123 },
bubbles: true // 注意!默认不冒泡
}));
父组件再 addEventListener 监听。对比 Vue 的 $emit 或 React 的 props 回调,简直回到石器时代。而且 Shadow DOM 默认阻断事件冒泡,必须手动设置 bubbles: true 才能穿透。
开发心得:调试事件时,Chrome DevTools 的 “Event Listeners” 面板救了我命。不然真不知道事件去哪了。
性能与兼容性:真的能上生产吗?
很多人担心 Web Components 性能差。其实不然。因为它是原生 API,没有虚拟 DOM 开销,渲染速度甚至比某些框架更快。我在本地做了个简单 benchmark(1000 个按钮组件):
| 方案 | 首次渲染 (ms) | 内存占用 (MB) |
|---|---|---|
| React 18 | 120 | 45 |
| Vue 3 | 95 | 40 |
| Web Components | 78 | 32 |
数据仅供参考,但趋势很明显:越接近原生,开销越小。
兼容性方面,根据 Can I Use 数据:
- Chrome 54+
- Firefox 63+
- Safari 10.1+
- Edge 79+
IE?别想了,彻底放弃。 但我们公司早就抛弃 IE 了(感谢微软自己放弃),所以无压力。
如果非要支持老旧浏览器,可以用 @webcomponents/webcomponentsjs 这个 polyfill,但会增加 ~15KB gzipped 体积。权衡之下,我们选择“不兼容就升级浏览器”,毕竟 B 端客户 IT 管理员也不是吃素的。
如何优雅地开发 Web Components?
纯手写 Custom Elements 太痛苦。好在社区已有成熟工具链,极大提升开发体验。
推荐工具 1:Lit
Lit 是 Google 出品的轻量库(仅 5KB),基于 Web Components 封装,提供响应式更新、模板语法、TypeScript 支持等。写起来像 Vue + React 混合体:
import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';
@customElement('my-button')
export class MyButton extends LitElement {
@property({ type: String })
label = 'Click me';
static styles = css`
button { background: var(--bg, #4CAF50); }
`;
render() {
return html`<button @click=${this._onClick}>${this.label}</button>`;
}
private _onClick() {
this.dispatchEvent(new CustomEvent('my-click'));
}
}
关键优势:
- 自动 diff 更新,不用手动操作 DOM
- 装饰器语法,代码更清晰
- 完美支持 TypeScript
推荐工具 2:Stencil
如果你需要构建组件库,Stencil 更强大。它是个编译器,能把你的 TSX 代码编译成标准 Web Components,并自动输出多种格式(ESM、CommonJS、UMD),还支持 lazy load、SSR 等。
我们最终选了 Stencil,因为要给五个业务线提供统一 npm 包。一行命令生成跨框架可用的组件:
npm install @my-company/ui-components
然后在 React 里:
import '@my-company/ui-components';
function App() {
return <my-button label="Hello" onMyClick={() => alert('clicked!')} />;
}
在 Vue 里:
<template>
<my-button label="Hello" @my-click="handleClick" />
</template>
<script>
import '@my-company/ui-components';
export default { methods: { handleClick() { ... } } }
</script>
无缝集成,毫无违和感。测试同学终于不再追着我问:“为什么这个按钮在 A 系统正常,B 系统就崩了?”
面试题高频考点总结
既然 Web Components 成了面试新宠,我也整理几个常被问到的点,供你参考:
Web Components 和 React/Vue 组件的本质区别?
- 前者是浏览器原生标准,后者是框架实现。WC 不依赖任何运行时,可跨框架复用。
Shadow DOM 的作用域是怎样的?
- 样式默认隔离,内部样式不影响外部,外部样式也不影响内部(除非使用
::part或 CSS 变量)。
- 样式默认隔离,内部样式不影响外部,外部样式也不影响内部(除非使用
如何监听属性变化?
- 通过
observedAttributes静态方法声明监听的属性,配合attributeChangedCallback钩子。
- 通过
Web Components 能用在 SSR 中吗?
- 原生不行(浏览器 API),但可通过 Stencil 等工具实现服务端预渲染。
性能瓶颈通常出现在哪里?
- 频繁创建/销毁组件(因无虚拟 DOM 优化),大量属性传递(需序列化为字符串)。
最后:值不值得投入?
说实话,Web Components 不是银弹。如果你的项目全是 React 技术栈,内部复用,那没必要折腾。但如果你面临以下场景:
- 需要跨技术栈复用组件
- 构建 Design System 或 UI 库
- 追求极致轻量与性能
- 厌倦了框架升级带来的兼容性噩梦
那 Web Components 绝对值得你花时间研究。
对我而言,这次重构不仅解决了业务痛点,还让我在综合能力上有了提升——从单纯写业务代码,到思考组件设计、API 抽象、跨团队协作。上周上线后,五个业务线全部接入成功,零样式冲突,零 JS 报错。运维大哥难得夸了一句:“这次加载快了不少。”
那一刻,虽然还是 996,虽然房租又涨了,但看着控制台干干净净的 log,心里居然有点小骄傲。
代码人生,不就是一边骂着“这破需求”,一边偷偷享受“搞定它”的快感吗?
附:快速上手资源
- 官方文档:developer.mozilla.org/Web/Web_Components
- Lit 教程:lit.dev/tutorial
- Stencil 示例:github.com/ionic-team/stencil-component-starter
别收藏吃灰了,今晚就敲几行代码试试。说不定下次面试,你就能笑着回答:“Web Components?我熟。”

评论 0