Web Components:原生组件化开发新趋势?别被标题骗了,其实它早就悄悄上场了!

需求之外
2025-12-13 13:42
阅读 576

哈喽大家好,我是小林,在成都一家中型电商公司做推荐算法工程师,不过别被“算法”两个字吓到——我其实是个前端出身的“伪算法”。工作两年来,除了调模型、搞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

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝