Web Components:原生组件化开发新趋势?我在小红书踩过的坑和收获

南城开发者
2025-12-15 02:54
阅读 1743

大家好,我是成都某厂(对,就是你想的那个红色图标App)的推荐算法工程师,入行快两年了。虽然主业是搞召回、排序、AB实验那一套,但因为团队人少活多,前端活儿也经常被拉去“支援”——尤其是用户增长相关的落地页、活动弹窗这些轻交互模块。

上周五晚上,我又一次被产品拉进紧急会议:“双12大促马上来了,我们需要一个通用的‘商品推荐卡片’组件,能嵌入任何H5页面,不管你是React、Vue还是纯静态页,都得跑起来!”
我内心OS:又来?上次双11那个弹窗还在生产环境偶尔白屏呢……

但这次我学聪明了——与其在React/Vue生态里打补丁,不如试试 Web Components。这玩意儿最近在技术圈吹得挺凶,说是“原生组件化”的未来。正好我也想跳槽面试前充充电,顺便把“面试题挑战”里的“如何实现跨框架组件复用”给啃下来。


被逼上梁山:为什么选 Web Components?

我们团队的现状很典型:

  • 主App用 React + TypeScript
  • H5活动页有的用 Vue 2,有的直接 jQuery 写
  • 运维那边还塞进来几个老CMS系统,连 ES6都不支持

之前的做法?写三套一模一样的推荐卡片,改个UI要同步改三次,测试同学都快把我微信拉黑了。有一次改了个按钮颜色,Vue 版漏了,上线后用户投诉“按钮看不见”,运维半夜打电话把我从火锅局叫回来回滚……那场面,真的想砸电脑。

所以这次我咬牙拍板:用 Web Components,一次编写,到处运行

Web Components 是一套浏览器原生支持的组件化标准,包含三个核心 API:

  • Custom Elements:自定义 HTML 标签
  • Shadow DOM:样式和 DOM 隔离
  • HTML Templates:声明式模板

不需要打包、不依赖框架,现代浏览器基本都支持(IE 除外,但谁还在乎 IE 呢?)。


动手干:从零写一个 <x-rec-card>

目标很明确:一个能展示商品图、标题、价格、点击埋点的卡片,支持动态传参。

第一步:定义 Custom Element

// rec-card.js
class RecCard extends HTMLElement {
  constructor() {
    super();
    // 创建 Shadow DOM,隔离样式
    this.attachShadow({ mode: 'open' });
  }

  // 属性变化时触发(用于响应式更新)
  static get observedAttributes() {
    return ['title', 'price', 'img-url'];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue !== newValue) {
      this.render();
    }
  }

  connectedCallback() {
    // 元素插入 DOM 时初始化
    this.render();
    this.setupEventListeners();
  }

  render() {
    const title = this.getAttribute('title') || '默认标题';
    const price = this.getAttribute('price') || '¥0.00';
    const imgUrl = this.getAttribute('img-url') || '/default.jpg';

    this.shadowRoot.innerHTML = `
      <style>
        :host { display: block; font-family: -apple-system, sans-serif; }
        .card { border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
        .img { width: 100%; height: 160px; object-fit: cover; }
        .info { padding: 12px; }
        .title { font-size: 16px; font-weight: 600; margin: 0; }
        .price { color: #e64340; font-size: 18px; margin: 8px 0 0; }
      </style>
      <div class="card">
        <img class="img" src="${imgUrl}" alt="${title}">
        <div class="info">
          <h3 class="title">${title}</h3>
          <p class="price">${price}</p>
        </div>
      </div>
    `;
  }

  setupEventListeners() {
    this.shadowRoot.querySelector('.card').addEventListener('click', () => {
      // 埋点上报(简化版)
      console.log('RecCard clicked:', this.getAttribute('title'));
      // 实际项目中会调用我们的数据上报 SDK
    });
  }
}

// 注册自定义元素
customElements.define('x-rec-card', RecCard);

第二步:在任意页面使用

<!-- 纯 HTML 页面 -->
<script type="module" src="./rec-card.js"></script>

<x-rec-card 
  title="【双12爆款】智能空气炸锅" 
  price="¥299"
  img-url="https://example.com/fryer.jpg">
</x-rec-card>

甚至在 React 里也能直接用(别忘了加 type="module"):

// React 组件中
import './rec-card.js'; // 注意:需确保只加载一次

export default function HomePage() {
  return (
    <div>
      <h1>猜你喜欢</h1>
      <x-rec-card 
        title="无线蓝牙耳机" 
        price="¥199"
        img-url="/headphones.jpg" />
    </div>
  );
}

踩坑实录:别信“开箱即用”

理想很丰满,现实嘛……全是坑。

坑1:属性传参 vs 对象传参

Web Components 只支持字符串属性。想传复杂对象?得自己序列化。

// ❌ 不能这么写
// <x-rec-card product="{ title: 'xxx', price: 299 }"></x-rec-card>

// ✅ 正确姿势:JSON 字符串 + 解析
this.product = JSON.parse(this.getAttribute('product') || '{}');

但这样性能差、易出错。后来我们封装了一个工具函数,自动监听属性变化并反序列化。

坑2:Shadow DOM 里的事件冒泡被阻断

我在 .card 上绑了 click 事件,但在父组件里监听不到!查文档才知道:Shadow DOM 默认阻止事件冒泡到外部

解决方案:手动派发自定义事件。

setupEventListeners() {
  this.shadowRoot.querySelector('.card').addEventListener('click', () => {
    // 派发可冒泡的自定义事件
    this.dispatchEvent(new CustomEvent('rec-click', {
      detail: { title: this.getAttribute('title') },
      bubbles: true,
      composed: true // 关键!允许穿越 Shadow DOM 边界
    }));
  });
}

现在父级可以正常监听了:

document.querySelector('x-rec-card').addEventListener('rec-click', (e) => {
  console.log('用户点击了:', e.detail.title);
});

坑3:样式定制太痛苦

产品经理说:“这个卡片在首页要圆角,在活动页要方角!”
我:???

Shadow DOM 的样式默认是封闭的。解决方案有几种:

  • 用 CSS 变量暴露可配置项
  • 通过 ::part() 选择器(需浏览器支持)
  • 或者……干脆不用 Shadow DOM(mode: 'closed' 改成不 attach)

我们最终采用 CSS 变量:

/* 组件内部 */
.card {
  border-radius: var(--rec-card-radius, 8px); /* 默认8px,可外部覆盖 */
}
<!-- 外部使用时 -->
<style>
  x-rec-card {
    --rec-card-radius: 0px;
  }
</style>

性能 & 兼容性:真能上生产?

我们做了简单压测:在低端安卓机(Redmi 9A)上渲染 50 个 <x-rec-card>,FPS 稳定在 55+,内存占用比 React 版低 30%。毕竟没有虚拟 DOM diff,原生操作就是快。

兼容性方面,Can I Use 数据如下:

浏览器 支持情况
Chrome ✅ 54+
Firefox ✅ 63+
Safari ✅ 10.1+
Edge ✅ 79+
iOS WebView ✅ 10.3+

国内主流 App 内置 WebView(微信、QQ、抖音)基本都基于 Chromium 70+,问题不大。实在要兼容老机型?上 webcomponents/polyfills,但体积会增加 ~15KB。


面试题挑战:Web Components vs 框架组件

最近面试被问到:“你们为什么不用微前端或者 iframe?”
我的回答是:

方案 优点 缺点
Web Components 原生、轻量、跨框架 生态弱、调试工具少
微前端(qiankun) 功能强、隔离彻底 重、通信复杂、SEO 差
iframe 完全隔离 性能差、通信难、体验割裂

对我们这种“轻量、高频、多端嵌入”的场景,Web Components 是甜点区。


总结:值不值得投入?

上线两周,0 故障。产品夸我“这次终于没出 bug”,测试同学给我点了奶茶(感动哭)。

Web Components 不是银弹,但在特定场景下,它真的香

  • 跨技术栈复用 UI 组件
  • 第三方嵌入式 widget(比如客服浮窗、分享按钮)
  • 渐进式重构老项目

当然,复杂交互、状态管理还是交给 React/Vue 更靠谱。但作为一个推荐工程师,能把一个组件做到“一次开发,全平台跑”,省下的时间够我多调两个模型参数了(笑)。

最后,如果你也在被“多端一致”折磨,不妨试试 Web Components。代码可读性高、维护成本低,还能在面试时秀一把“原生 API 掌握度”——毕竟,综合能力才是涨薪的关键嘛!

本文代码已整理到 GitHub,欢迎 star:github.com/yourname/x-rec-card
成都的冬天又湿又冷,写完这篇我去煮碗面,大家保重!

评论 0

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