Web Components真香?我在京东搞大促组件库的血泪实战
上周五晚上十一点,杭州下着小雨,耳机里放着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链接就行。
给想尝试的同学几点建议
- 别追求100%替代React:Web Components适合做“叶子组件”,比如按钮、倒计时、商品卡片。复杂的业务逻辑还是交给框架。
- 重视可访问性(a11y):自定义元素默认没有语义,记得加
role和ARIA属性。 - 用Lit或Stencil降低门槛:纯手写Web Components太原始。我们后来迁移到Google的Lit,它提供了响应式更新、模板语法,代码量减少一半。
- 做好性能监控:Shadow DOM的创建有一定开销,大量实例化时要注意。我们用Performance API监控每个组件的mount时间。
- 和设计师对齐设计系统:通过CSS变量暴露可定制点,避免后期扯皮。
最后说两句
写这篇文章时,窗外杭州的夜色很静。回想过去一年和Web Components“相爱相杀”的日子,有崩溃也有惊喜。它或许不是银弹,但在特定场景下,确实帮我们解决了跨框架组件复用的老大难问题。
技术选型从来不是非黑即白。React依然是我心中的白月光,但Web Components就像一把趁手的瑞士军刀——不华丽,但关键时刻能救命。
如果你也在大厂扛着大促压力,或者被多端适配折磨得睡不着觉,不妨试试这个“老技术新用法”。毕竟,能让产品经理少提几个需求,就是对我们程序员最大的温柔。
对了,耳机里的歌换成了《Blinding Lights》,The Weeknd的声音混着键盘敲击声,感觉又能肝到凌晨三点了。双12的需求文档刚发过来……溜了溜了。

评论 0