Web Components:原生组件化开发新趋势?别被标题骗了,其实它早就悄悄上场了!
哈喽大家好,我是小林,在成都一家中型电商公司做推荐算法工程师,不过别被“算法”两个字吓到——我其实是个前端出身的“伪算法”。工作两年来,除了调模型、搞AB实验、分析用户增长漏斗,还得时不时帮前端兄弟们救火。毕竟在我们这种产品迭代快、活动上线急的小团队里,“全栈”不是选择题,是生存技能。
上周五晚上10点,我正盯着双11大促的实时点击率曲线,产品经理突然在钉钉群里@我:“小林,下个月有个跨平台活动页,H5+小程序+App内嵌都要用同一套UI组件,能搞个通用方案不?最好别依赖框架,我们小程序那边React/Vue都跑不动……”
我当时差点把咖啡喷到MacBook上——这需求听着耳熟吗?是不是像极了那些年你我在面试时被问烂的“如何实现跨框架组件复用”?
没错,这题我答过,但从来没在生产环境真·落地过。
于是,抱着“要么学,要么卷死”的心态,我翻开了尘封已久的 Web Components 文档。没想到这一翻,直接打开了新世界的大门。
起初:我以为它只是个“玩具”
Web Components 这玩意儿,说白了就是浏览器原生支持的一套组件化规范,不用 React、Vue、Angular,也能写自定义标签。比如你可以直接在 HTML 里写:
<my-product-card sku="123456" price="99.9"></my-product-card>
听起来很酷,对吧?但以前我一直觉得它“不实用”——兼容性差、生态弱、文档晦涩,连 Chrome DevTools 都不怎么友好。再加上我们团队早就是 Vue 3 + Vite 的忠实用户,谁还折腾原生?
但这次不一样。跨平台、零依赖、轻量级——这三个词精准戳中了我们当前的痛点。尤其是小程序那边,JS 引擎限制多,打包体积卡得死死的,任何第三方库都得精打细算。
于是,我决定:干!
实战第一步:从一个“商品卡片”开始
我们的第一个目标很简单:做一个可复用的商品展示卡片,支持传入 SKU、价格、图片 URL,点击还能触发自定义事件(比如加购)。理想状态下,它应该能在 H5 页面、App WebView、甚至微信小程序(通过自定义组件桥接)里无缝运行。
1. 创建 Custom Element
Web Components 的核心之一是 customElements.define()。我新建了一个 product-card.js:
class ProductCard extends HTMLElement {
constructor() {
super();
// 必须先 attach shadow DOM,否则样式会污染全局
this.attachShadow({ mode: 'open' });
}
// 属性监听(关键!)
static get observedAttributes() {
return ['sku', 'price', 'image'];
}
// 属性变化时自动触发
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
this.render();
}
}
connectedCallback() {
this.render();
// 绑定点击事件
this.shadowRoot.querySelector('.add-cart-btn')?.addEventListener('click', () => {
// 派发自定义事件,供外部监听
this.dispatchEvent(new CustomEvent('add-to-cart', {
detail: { sku: this.getAttribute('sku') },
bubbles: true,
composed: true // 确保事件能穿透 Shadow DOM
}));
});
}
render() {
const sku = this.getAttribute('sku') || 'N/A';
const price = this.getAttribute('price') || '0.00';
const image = this.getAttribute('image') || '/default.jpg';
this.shadowRoot.innerHTML = `
<style>
.card { border: 1px solid #eee; border-radius: 8px; padding: 12px; }
.price { color: #e60012; font-weight: bold; }
.add-cart-btn { background: #ff6700; color: white; border: none; padding: 6px 12px; border-radius: 4px; }
</style>
<div class="card">
<img src="${image}" width="100" height="100" alt="product">
<p>SKU: ${sku}</p>
<p class="price">¥${price}</p>
<button class="add-cart-btn">加入购物车</button>
</div>
`;
}
}
customElements.define('product-card', ProductCard);
📌 踩坑提醒:一开始我忘了加
composed: true,结果在 Vue 项目里监听不到add-to-cart事件,调试半小时才发现是 Shadow DOM 的“隔离墙”在作祟。
2. 在 HTML 中使用
<!DOCTYPE html>
<script type="module" src="./product-card.js"></script>
<product-card
sku="P123456"
price="89.9"
image="https://example.com/product.jpg"
></product-card>
<script>
document.querySelector('product-card').addEventListener('add-to-cart', (e) => {
console.log('用户想加购:', e.detail.sku);
// 这里可以调用你的业务逻辑
});
</script>
效果立竿见影! 不用任何构建工具,直接 <script type="module"> 引入,就能跑起来。而且样式完全隔离,不怕和页面其他 CSS 冲突。
工具链:别以为它只能裸奔
很多人以为 Web Components 就是“手写原生 JS”,其实不然。现在已经有成熟的工具生态了:
| 工具 | 用途 | 我的体验 |
|---|---|---|
| Lit | 轻量级 Web Components 库(Google 出品) | 写起来像 React + Hooks,超爽!模板用 JS tagged literals,比 innerHTML 安全多了 |
| Stencil | 编译成标准 Web Components 的编译器 | 支持 TS、JSX、懒加载,适合大型项目 |
| Vite + @web/dev-server | 开发服务器 + HMR | 热更新速度飞起,比 webpack 快 10 倍 |
| Playwright | E2E 测试 | 直接操作 Shadow DOM 元素,测试覆盖率杠杠的 |
我最终选了 Lit,因为它的 API 简洁又现代。上面那个商品卡片用 Lit 重写后,代码更干净:
import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';
@customElement('product-card')
export class ProductCard extends LitElement {
@property({ type: String }) sku = '';
@property({ type: Number }) price = 0;
@property({ type: String }) image = '';
static styles = css`
.card { /* ... */ }
.price { color: #e60012; }
`;
render() {
return html`
<div class="card">
<img src="${this.image}" alt="product" />
<p>SKU: ${this.sku}</p>
<p class="price">¥${this.price}</p>
<button @click=${this._onAddCart}>加入购物车</button>
</div>
`;
}
private _onAddCart() {
this.dispatchEvent(new CustomEvent('add-to-cart', {
detail: { sku: this.sku },
bubbles: true,
composed: true
}));
}
}
TypeScript + Decorators + Template Literals,这组合拳打下来,开发体验直接拉满。而且 Lit 只有 ~5KB,gzip 后几乎可以忽略不计。
兼容性 & 性能:真的能上生产吗?
这是我和前端老大最担心的问题。查了下 Can I Use,好消息是:
- Chrome、Edge、Firefox、Safari 全面支持(包括 iOS 10.3+)
- 唯一的短板是 IE —— 但我们公司早就放弃 IE 了,连产品经理都说“IE 用户就让他们用旧版吧”
至于性能,实测下来:
- 首次渲染:比 Vue 组件快(因为无虚拟 DOM 开销)
- 内存占用:每个实例约 2-3KB,远低于 React/Vue 组件
- 交互响应:直接操作 DOM,延迟几乎为 0
我们在测试环境跑了一个 A/B 实验:同一页面分别用 Vue 组件和 Web Components 渲染商品列表。结果 Web Components 版本的 FCP(First Contentful Paint)快了 18%,Lighthouse 分数高了 7 分。
💡 小技巧:用
performance.mark()和performance.measure()手动打点,能精准对比组件初始化耗时。
面试题挑战:为什么不用 Web Components?
最近参加了一场成都本地的前端 Meetup,有个同学问我:“你们公司真在用 Web Components?那为啥大厂还是主推 React/Vue?”
这问题问得好。我也曾以为 Web Components 是“银弹”,但实战后发现它更适合 特定场景:
✅ 适合用 Web Components 的情况:
- 需要跨技术栈复用(比如微前端、Widget 嵌入)
- 对包体积极度敏感(如广告脚本、埋点 SDK)
- 构建 Design System 基础组件(按钮、输入框等)
❌ 不适合的情况:
- 复杂状态管理(Redux/Zustand 还是香)
- 需要 SSR(虽然有解决方案,但麻烦)
- 团队不熟悉,学习成本高
所以我的答案是:它不是替代 React/Vue,而是补充它们。
就像我们现在的架构:主应用用 Vue 3,但所有“可嵌入式模块”(比如活动弹窗、商品卡片、评分组件)全部用 Web Components 输出。这样,无论是 App 内嵌、第三方合作方接入,还是未来迁移到小程序,都能一键复用。
最后的碎碎念
写这篇文章的时候,已经是凌晨1点。窗外成都的夜生活还没结束,而我刚修完一个线上 Bug——因为某个老安卓机不支持 CSS.supports('display', 'grid'),导致商品卡片错位。最后用 @supports 包了一层 fallback,才搞定。
技术哪有什么银弹,都是在妥协中前进。
但说实话,这次折腾 Web Components 的经历让我挺开心的。它让我重新思考“组件化”的本质:不是框架的附属品,而是浏览器原生的能力。 当你不再依赖某个生态,反而获得了更大的自由。
如果你也在被“跨平台复用”折磨,不妨试试 Web Components。它可能不够炫酷,但足够扎实。就像我们成都人说的:“巴适得板,不整虚的。”
附:快速上手 checklist
- 用
Lit而不是裸写 Custom Elements - 记得加
composed: true让事件穿透 Shadow DOM - 样式用
:host和::slotted控制外部定制 - 测试时用 Playwright,别只靠 Chrome DevTools
- 生产环境加 polyfill(仅针对老旧 Android)
好了,我去睡了。明天还要改推荐策略,据说双11的 GMV 差 0.1% 就拿不到年终奖……(哭)
共勉,前端人!

评论 0