Web Components:原生组件化开发新趋势
上周五晚上 10 点,实验室里只剩我和隔壁工位的小王还在肝项目。他盯着控制台报错一脸懵:“这玩意儿怎么又把 Shadow DOM 的样式给吃了?” 我瞥了一眼,笑出声——这不就是我们最近在折腾的 Web Components 嘛。
我是杭电软件工程研二的学生,目前在实验室跟着导师做企业级中后台系统重构。杭州这边,阿里网易扎堆,机会不少,但卷得也厉害。为了能在秋招前多点拿得出手的项目经验,我主动揽下了“前端微前端架构升级”这个活儿。结果一上来就被领导扔了个需求:“能不能搞个跨框架复用的 UI 组件库?别再让 React 和 Vue 团队天天吵架了。”
行吧,被逼上梁山,那就学 Web Components 吧。
为啥是 Web Components?
说实话,之前我对 Web Components 的印象还停留在“浏览器原生支持、但没人用”的阶段。毕竟在 React/Vue 主导的世界里,谁还去手写 customElements.define 啊?直到去年双11期间,我们给某大厂做的运营活动页因为用了太多第三方 UI 库,首屏加载直接飙到 3.2s,产品总监差点冲进机房拔网线。
后来复盘时发现,问题核心在于:每个框架都有自己的组件模型,一旦要跨团队协作,就得重复造轮子,或者搞一堆胶水代码。而 Web Components 是浏览器原生支持的组件化方案,天生具备框架无关性——React 能用,Vue 能用,连老掉牙的 jQuery 项目都能塞进去。
最关键的是:它不用打包!不用编译!开箱即用!
(当然,这只是理想状态。现实是……后面再说。)
初体验:三件套走起
Web Components 核心就仨 API:
- Custom Elements:自定义标签
- Shadow DOM:样式和 DOM 隔离
- HTML Templates:声明式模板
举个最简单的例子,我想做个 <my-button>:
class MyButton extends HTMLElement {
constructor() {
super();
// 创建 Shadow Root
const shadow = this.attachShadow({ mode: 'open' });
// 模板
const template = document.createElement('template');
template.innerHTML = `
<style>
button {
background: #4CAF50;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}
button:hover {
opacity: 0.9;
}
</style>
<button><slot></slot></button>
`;
// 克隆模板并挂载
shadow.appendChild(template.content.cloneNode(true));
}
}
// 注册自定义元素
customElements.define('my-button', MyButton);
然后在 HTML 里直接用:
<my-button>点我啊!</my-button>
搞定!样式不会污染全局,DOM 结构被 Shadow DOM 隔离,还能通过 <slot> 插入内容。那一刻我仿佛看到了前端组件化的终极形态——真正的“一次编写,到处运行”。
踩坑实录:理想很丰满,现实很骨感
然而,当我兴冲冲把这个按钮集成到我们现有的 Vue 项目时,测试小姐姐发来消息:“你这按钮点了没反应啊?”
一查才发现:Vue 默认不会监听 Custom Element 的属性变化!比如我想加个 disabled 属性:
static get observedAttributes() {
return ['disabled'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'disabled') {
const button = this.shadowRoot.querySelector('button');
button.disabled = newValue !== null;
}
}
但在 Vue 里写 <my-button :disabled="isDisabled">,属性根本不会同步到 Custom Element 上。最后靠 v-bind="$attrs" 才勉强解决。
更惨的是兼容性问题。虽然现代浏览器基本都支持了,但Safari 对 Shadow DOM 的 CSS 变量支持有坑,IE?别想了,直接放弃。
还有性能问题。每个 Web Component 都会创建一个 Shadow Root,内存开销比普通 div 大不少。我们压测时发现,页面渲染 1000 个 <my-button>,FPS 直接掉到 40 以下。最后不得不加虚拟滚动 + 动态加载。
开发心得:稳定 vs 新潮的平衡术
在实验室做项目,导师总说:“新技术可以玩,但上线必须稳。” 所以我们最终采用了一个折中方案:
- 内部工具类组件(比如数据看板、配置面板):大胆用 Web Components,享受原生隔离和跨框架优势;
- 用户高频交互组件(比如表单、列表):还是用 Vue + TypeScript,保证性能和开发体验;
- 对外交付的 SDK:把核心逻辑封装成 Web Components,让客户无论用什么技术栈都能集成。
顺便安利一个神器:Lit。这是 Google 出的轻量级 Web Components 库,用装饰器语法写起来像 Vue 3 的 Composition API,还自带响应式更新。代码量少一半,心智负担小很多。
import { LitElement, html, customElement, property } from 'lit';
@customElement('my-button')
export class MyButton extends LitElement {
@property({ type: Boolean }) disabled = false;
render() {
return html`
<style>
/* ... */
</style>
<button ?disabled=${this.disabled}><slot></slot></button>
`;
}
}
简直不要太爽!
效果如何?值不值得投入?
经过三个月的实战,我们的 Web Components 组件库已经支撑了 5 个内部系统,首屏加载时间平均减少 1.1s,而且前端团队再也不用为“你用 React 我用 Vue”吵到运维介入了。
更重要的是,它让我重新思考了“组件”的本质。以前总觉得组件就是 UI + 逻辑的封装,但现在明白:真正的组件应该是“自治的、可组合的、与环境解耦的”。Web Components 虽然不够完美,但它指明了一个方向——回归浏览器原生能力,减少对框架的依赖。
写在最后:代码人生,不止 CRUD
有时候想想,我们这代程序员真的很幸运。十年前还在为 IE6 兼容掉头发,现在却能站在 Web Components、WebAssembly、WebGPU 这些前沿技术的门口张望。
当然,现实很骨感:产品经理明天又要改需求,测试说线上有个偶现 Bug,运维催着上线……但正是这些琐碎日常中的技术探索,才让“代码人生”有点意思。
所以,如果你也在杭州,也在卷大厂 offer,不妨试试 Web Components。它可能不是银弹,但绝对是一把值得放进工具箱的瑞士军刀。
毕竟,技术人的浪漫,就是在 deadline 前夜,还能写出一行让自己骄傲的代码。
附:主流方案对比(实验室实测)
| 方案 | 跨框架支持 | 样式隔离 | 性能 | 学习成本 | 适合场景 |
|---|---|---|---|---|---|
| React Component | ❌ | ❌ | ⭐⭐⭐⭐ | ⭐⭐ | 纯 React 项目 |
| Vue SFC | ❌ | ✅(scoped) | ⭐⭐⭐⭐⭐ | ⭐ | 纯 Vue 项目 |
| Web Components | ✅ | ✅(Shadow DOM) | ⭐⭐ | ⭐⭐⭐ | 跨框架/SDK/嵌入式 |
| Lit(基于 WC) | ✅ | ✅ | ⭐⭐⭐ | ⭐⭐ | 轻量级 WC 开发 |
数据来源:实验室 2024 Q2 前端技术选型报告(其实就是我和小王瞎测的)
P.S. 如果你也在折腾 Web Components,欢迎交流!我的 GitHub 仓库里有完整示例(带踩坑注释),链接就不贴了——毕竟导师说“别在博客里打广告”。

评论 0