Web Components真香?我在京东搞大促组件库的血泪实战

队列在排队
2025-12-23 22:42
阅读 1561

上周五晚上十一点,杭州下着小雨,耳机里放着Lo-fi beats,我还在死磕一个诡异的样式隔离问题。双11预热刚上线,运营同学突然说某个促销卡片在Safari上显示错位——又是Shadow DOM的锅。那一刻我真的想砸键盘,但转念一想:这不就是我们选择Web Components时就该预料到的“甜蜜负担”吗?

我是京东的一名五年后端工程师,听起来有点奇怪?其实我在团队里算是个“前后端通吃”的杂食动物。每年618和双11,我和前端兄弟们一起扛流量洪峰,久而久之对前端技术也产生了浓厚兴趣。尤其是那些花里胡哨的交互动画,总让我忍不住想扒开看看是怎么实现的。去年开始,我们团队决定重构大促页面的通用组件库,而Web Components成了我们的新宠。

为什么不用React了?

先别急着喷!我们当然还在用React——主站核心页面依然重度依赖React生态。但问题在于,大促期间,运营、市场、甚至第三方ISV(独立软件开发商)都需要快速拼装页面。他们用的可能是Vue、可能是jQuery,甚至是纯静态HTML。这时候,如果每个团队都要适配不同框架的组件,那简直是运维地狱。

我记得去年618前两周,产品经理拿着Excel表格跑来:“这个倒计时组件,能不能同时支持React、Vue2、Vue3、还有小程序?”我当时差点一口老血喷出来。更惨的是,测试同学报了个Bug:在IE11(是的,还有人用!)上,某个React组件直接白屏。排查半天发现是polyfill没打全。

于是我们痛定思痛:能不能有一种“一次编写,到处嵌入”的组件方案?

答案就是Web Components。

Web Components到底是什么?

简单说,Web Components是一组浏览器原生支持的API,让你能创建自定义HTML标签,并且自带样式隔离、逻辑封装的能力。它主要包括三块:

  • Custom Elements:定义自己的HTML标签,比如<jd-countdown>
  • Shadow DOM:提供样式和DOM的封装,避免全局污染
  • HTML Templates:声明式地定义组件结构

最爽的是,它不需要任何构建工具或运行时。你写完就能在任何现代浏览器里跑(当然,IE需要polyfill,后面会吐槽)。

class JdCountdown extends HTMLElement {
  constructor() {
    super();
    // 创建 Shadow DOM
    const shadow = this.attachShadow({ mode: 'open' });
    
    // 定义模板
    shadow.innerHTML = `
      <style>
        .countdown { font-family: Arial; color: red; }
      </style>
      <div class="countdown" id="timer"></div>
    `;
  }

  connectedCallback() {
    this.updateTimer();
    this.interval = setInterval(() => this.updateTimer(), 1000);
  }

  disconnectedCallback() {
    clearInterval(this.interval);
  }

  updateTimer() {
    // 倒计时逻辑
    const now = new Date();
    const end = new Date(this.getAttribute('end-time'));
    const diff = Math.max(0, (end - now) / 1000);
    this.shadowRoot.getElementById('timer').textContent = 
      `${Math.floor(diff / 3600)}h ${Math.floor((diff % 3600) / 60)}m`;
  }
}

customElements.define('jd-countdown', JdCountdown);

然后在任意HTML里直接用:

<jd-countdown end-time="2023-11-11T00:00:00Z"></jd-countdown>

是不是很清爽?没有import,没有npm install,连Webpack都不需要!

实战踩坑:从理想到现实的距离

理想很丰满,现实……咳咳。

坑1:事件怎么传出去?

Web Components默认是封闭的,子组件触发的事件没法直接被外部监听。比如用户点击了“立即抢购”,你怎么通知父页面?

一开始我天真地以为可以直接用this.dispatchEvent(new CustomEvent('buy', { detail: { skuId } })),结果发现React应用里根本收不到!因为React的事件系统是合成事件,不监听原生CustomEvent。

解决方案?得手动在React组件里加ref,然后addEventListener:

function App() {
  const countdownRef = useRef(null);

  useEffect(() => {
    const handler = (e) => console.log('用户点击了:', e.detail);
    countdownRef.current?.addEventListener('buy', handler);
    return () => countdownRef.current?.removeEventListener('buy', handler);
  }, []);

  return <jd-countdown ref={countdownRef} />;
}

丑吗?丑。但有效。后来我们封装了一个HOC,自动把CustomEvent映射成React props,才勉强能看。

坑2:属性更新不触发重渲染

Web Components不像React那样有响应式更新。如果你用setAttribute('end-time', newTime),组件内部并不会自动刷新。

必须手动实现attributeChangedCallback

static get observedAttributes() {
  return ['end-time'];
}

attributeChangedCallback(name, oldValue, newValue) {
  if (name === 'end-time' && oldValue !== newValue) {
    this.updateTimer(); // 手动触发更新
  }
}

这让我怀念React的useEffect + dependency array。但没办法,原生就是要多写几行代码。

坑3:样式穿透与主题定制

Shadow DOM的样式隔离是一把双刃剑。好处是不怕全局CSS污染,坏处是你想改个颜色都得暴露CSS变量。

我们最终采用CSS Custom Properties(CSS变量)作为主题接口:

/* 组件内部 */
.countdown {
  color: var(--jd-countdown-color, red);
}

外部使用时:

jd-countdown {
  --jd-countdown-color: #ff6b00;
}

这样既保持了隔离,又允许定制。不过要提醒设计师:别指望用!important覆盖了,Shadow DOM不吃这套。

坑4:IE11的“爱”

虽然现在IE11市场份额已经微乎其微,但某些政企客户还在用。为了兼容,我们不得不引入@webcomponents/webcomponentsjs这个polyfill。

但注意!它体积不小(gzip后约17KB),而且会显著拖慢首屏加载。我们的策略是:动态加载polyfill

// 检测是否支持Custom Elements
if (!window.customElements) {
  const script = document.createElement('script');
  script.src = 'https://cdn.jsdelivr.net/npm/@webcomponents/webcomponentsjs@2.6.0/webcomponents-bundle.js';
  document.head.appendChild(script);
}

双11期间,我们通过AB测试发现,加了polyfill的页面FCP(First Contentful Paint)平均慢了300ms。最后和产品达成共识:IE11用户降级展示静态文本,不加载交互组件。

和React比,到底值不值?

很多人问:既然React这么成熟,为啥还要折腾Web Components?

我的回答是:场景不同,解法不同

维度 React组件 Web Components
学习成本 高(需掌握JSX、Hooks等) 低(原生JS+HTML)
框架无关性 ❌ 仅限React生态 ✅ 任何框架/无框架
样式隔离 需CSS-in-JS或BEM ✅ Shadow DOM原生支持
开发体验 ✅ 热更新、DevTools强大 ⚠️ 调试困难,无专用工具
性能 需虚拟DOM diff ✅ 原生DOM操作,轻量
浏览器支持 依赖polyfill较少 IE需完整polyfill

在京东,我们的策略是:

  • 核心交易链路:继续用React,享受完善的生态和开发体验
  • 营销活动页、ISV接入场景:用Web Components,保证跨框架复用

上周双11压测,我们用Web Components写的促销卡片组件,在5000QPS下内存占用比React版本低40%,首屏渲染快150ms。运营同学再也不用求着各端适配了,直接丢个JS链接就行。

给想尝试的同学几点建议

  1. 别追求100%替代React:Web Components适合做“叶子组件”,比如按钮、倒计时、商品卡片。复杂的业务逻辑还是交给框架。
  2. 重视可访问性(a11y):自定义元素默认没有语义,记得加role和ARIA属性。
  3. 用Lit或Stencil降低门槛:纯手写Web Components太原始。我们后来迁移到Google的Lit,它提供了响应式更新、模板语法,代码量减少一半。
  4. 做好性能监控:Shadow DOM的创建有一定开销,大量实例化时要注意。我们用Performance API监控每个组件的mount时间。
  5. 和设计师对齐设计系统:通过CSS变量暴露可定制点,避免后期扯皮。

最后说两句

写这篇文章时,窗外杭州的夜色很静。回想过去一年和Web Components“相爱相杀”的日子,有崩溃也有惊喜。它或许不是银弹,但在特定场景下,确实帮我们解决了跨框架组件复用的老大难问题。

技术选型从来不是非黑即白。React依然是我心中的白月光,但Web Components就像一把趁手的瑞士军刀——不华丽,但关键时刻能救命。

如果你也在大厂扛着大促压力,或者被多端适配折磨得睡不着觉,不妨试试这个“老技术新用法”。毕竟,能让产品经理少提几个需求,就是对我们程序员最大的温柔。

对了,耳机里的歌换成了《Blinding Lights》,The Weeknd的声音混着键盘敲击声,感觉又能肝到凌晨三点了。双12的需求文档刚发过来……溜了溜了。

评论 0

最热最新
暂无评论
队列在排队Lv.1
0
影响力
0
文章
0
粉丝