Web Components:原生组件化开发新趋势,以及我在上海租房屋里踩过的那些坑
上周五晚上十点半,我瘫在出租屋的破沙发上,盯着屏幕里又一个因为 Shadow DOM 样式隔离失效而崩坏的按钮,心里只有一句话:“这届产品经理是不是以为我们前端是 AI 写的?”
别误会,我不是在骂人——毕竟我自己也在偷偷刷行测题,准备考公上岸。但现实很骨感:白天写业务代码、改 UI 重构、被测试追着问“这个交互为啥在 IE11 上白屏”,晚上还得背申论范文。时间紧任务重,所以我对“能省事就省事”的技术特别敏感。
而 Web Components,就是我最近在项目里硬着头皮啃下来的一块硬骨头。说是“新趋势”,其实它早在 2011 年就被提出来了,只不过一直活在 React/Vue 的阴影下。直到去年双11大促前,我们团队接了个跨平台复用的需求——同一个商品卡片组件,既要塞进公司主站(React),又要嵌入合作方的老旧 jQuery 页面,还得兼容部分政府系统的 IE11 环境(是的,你没看错,2024年还有IE11)。领导拍板:“试试 Web Components,原生支持,不依赖框架。”
我当时内心 OS:好家伙,这不是把我往火坑里推吗?
起因:一场“无框架”需求引发的血案
事情是这样的。我们公司做的是 SaaS 化电商解决方案,客户五花八门。最近有个市级政务采购平台要接入我们的商品展示模块,但他们技术栈极其保守——jQuery + Bootstrap 3,页面结构还是 table 布局。更离谱的是,他们明确拒绝引入任何现代前端框架,理由是“安全审计过不了”。
产品经理甩过来一句话:“你们搞个独立组件吧,像 iframe 那样嵌进去就行。”
我:“……iframe?那 SEO 和性能怎么办?而且跨域通信又是一堆坑。”
他:“那你有别的办法吗?”
那一刻,我想起了半年前在 GitHub 上看到的一个开源项目 webcomponents/custom-elements-everywhere,里面展示了 Web Components 如何在 React、Angular、Vue 甚至原生 HTML 中无缝使用。灵光一闪:这不就是为这种场景量身定做的吗?
于是,我主动请缨:“让我试试 Web Components。”
领导眼睛一亮:“可以!但双11前必须上线。”
我:“……好的,我去买杯咖啡续命。”
初尝 Web Components:你以为很简单?
Web Components 由四部分组成:Custom Elements(自定义元素)、Shadow DOM(影子 DOM)、HTML Templates(模板)和 ES Modules(模块系统)。听起来高大上,实则入门门槛低到令人发指——你只需要写个 class 继承 HTMLElement,再用 customElements.define() 注册一下,就能在 HTML 里直接用了。
比如最简单的商品卡片:
// product-card.js
class ProductCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
const productId = this.getAttribute('product-id');
this.render(productId);
}
render(productId) {
this.shadowRoot.innerHTML = `
<style>
:host { display: block; border: 1px solid #eee; padding: 16px; }
.title { font-weight: bold; color: #333; }
</style>
<div class="card">
<div class="title">商品ID: ${productId}</div>
<button>加入购物车</button>
</div>
`;
}
}
customElements.define('product-card', ProductCard);
然后在 HTML 里:
<product-card product-id="12345"></product-card>
跑起来!看起来完美。我甚至在本地 demo 里加了点动画,还顺手把代码传到了自己的 GitHub 仓库(github.com/yourname/product-card-wc),心想:“这不比写 Vue 单文件组件香?”
然而,现实很快给了我一记重拳。
坑1:Shadow DOM 的样式隔离,既是盾也是矛
我以为 :host 和内部 <style> 能搞定一切。结果一集成到客户的老页面里,按钮字体变成了 Times New Roman,边框消失了,连 hover 效果都没了。
后来才发现:客户的全局 CSS 里有一条祖传规则:
* {
font-family: "Microsoft YaHei", sans-serif !important;
}
而 Shadow DOM 默认不继承外部样式!虽然这是它的核心优势(避免样式污染),但在实际项目中,你可能希望某些基础样式(比如字体、字号)能透传进来。
解决办法?要么手动在 <style> 里重写所有基础样式(维护地狱),要么用 CSS Custom Properties(变量)从外部注入:
// 在宿主页面定义
document.documentElement.style.setProperty('--font-family', '"Microsoft YaHei", sans-serif');
// 在组件内部使用
this.shadowRoot.innerHTML = `
<style>
:host {
font-family: var(--font-family, Arial);
}
</style>
...
`;
但这要求宿主页面配合,对于那些“只给一个 HTML 文件就跑路”的客户来说,根本不可行。最后我妥协了:关闭 Shadow DOM 的封闭模式,改用 light DOM + BEM 命名规范,虽然失去了完全隔离,但至少能被外部样式影响。
// 放弃 attachShadow,直接操作 innerHTML
this.innerHTML = `<div class="product-card__wrapper">...</div>`;
自嘲一下:为了兼容性,我亲手拆掉了 Web Components 最引以为傲的“护城河”。
坑2:事件冒泡被 Shadow DOM 截断
产品需求里有个小细节:点击“加入购物车”后,要触发一个自定义事件 product-added,供宿主页面监听。
我信心满满地写了:
const button = this.shadowRoot.querySelector('button');
button.addEventListener('click', () => {
this.dispatchEvent(new CustomEvent('product-added', {
detail: { id: this.getAttribute('product-id') },
bubbles: true,
composed: true // 关键!
}));
});
注意那个 composed: true。如果不加,事件在 Shadow DOM 边界就停止冒泡了,宿主页面根本收不到!
刚上线时忘了加,测试小姐姐直接冲过来:“你们组件点不动啊!”
我:“不可能,我本地跑得好好的!”
她:“你自己看生产环境控制台。”
果然,事件没冒泡出去。加上 composed: true 后才正常。这个坑,GitHub 上至少有上百个 issue 提到过,属于“文档写得很清楚但没人看”的经典案例。
坑3:IE11?对不起,Web Components 说“我不伺候”
前面吹了那么多,但最大的雷藏在最后:原生 Web Components 不支持 IE11。
而我们的政务客户,偏偏强制要求 IE11 兼容。
当时我真的想砸电脑。但冷静下来一查,发现社区早有方案:webcomponents/polyfills。
只需要在页面头部引入:
<script src="https://unpkg.com/@webcomponents/webcomponentsjs@2.8.0/webcomponents-bundle.js"></script>
然后你的 Custom Elements 就能在 IE11 上跑起来了!不过要注意:
- 性能会下降(毕竟 polyfill 模拟了整个 Shadow DOM)
- 某些高级 API(如
attachShadow的mode: 'closed')依然不可用 - 必须用 transpiler 把 ES6+ 代码转成 ES5
我在项目里用 Babel + Webpack 打包,配置如下:
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: [['@babel/preset-env', { targets: 'ie 11' }]]
}
}
}
]
}
};
上线后,IE11 用户终于能看到商品卡片了——虽然加载慢了 800ms,但至少没白屏。产品经理满意了,我也松了口气。
坑4:调试体验,真的不如 React DevTools
说到调试,Web Components 的工具链现在还是有点原始。Chrome DevTools 虽然能 inspect Shadow DOM(在设置里勾选 “Show user agent shadow DOM”),但没法像 React 那样直接看 props/state 变化。
我一度怀念起 Vue 的 $vm 控制台命令。后来发现有个 Chrome 插件叫 Web Components DevTools Extension,能列出页面中所有自定义元素及其属性,勉强够用。
但最烦的是:错误堆栈经常指向 polyfill 内部,而不是你的业务代码。有一次因为属性名拼错,报错信息是 Cannot read property 'render' of undefined,定位了半小时才发现是 connectedCallback 里没判空。
成果与反思:值得投入吗?
最终,这个 Web Components 商品卡片成功上线,覆盖了 3 个不同技术栈的客户站点,包括那个 IE11 政务系统。性能方面,Lighthouse 评分从 68 提升到 82(主要因为减少了第三方框架体积),SEO 也因语义化标签改善而略有提升。
下表是我整理的核心指标对比:
| 维度 | Web Components 方案 | 传统 iframe 方案 | React 微前端方案 |
|---|---|---|---|
| 加载性能 | ⭐⭐⭐⭐ (约 45KB gzipped) | ⭐⭐ (额外 HTTP 请求 + 完整页面加载) | ⭐⭐⭐ (需共享 React Runtime) |
| 样式隔离 | ⭐⭐⭐⭐ (Shadow DOM) | ⭐⭐⭐⭐ (天然隔离) | ⭐⭐ (需 CSS-in-JS 或命名空间) |
| 事件通信 | ⭐⭐⭐ (CustomEvent + composed) | ⭐ (postMessage 复杂) | ⭐⭐⭐ (props/callback) |
| IE11 支持 | ⭐⭐ (需 polyfill,性能差) | ⭐⭐⭐⭐ | ❌ |
| 开发体验 | ⭐⭐ (调试弱) | ⭐ (维护难) | ⭐⭐⭐⭐ |
从结果看,Web Components 在“跨框架复用”和“轻量级嵌入”场景下确实有不可替代的优势。但它不是银弹——如果你的团队全是 React 技术栈,硬上 Web Components 反而是自找麻烦。
给同行的建议:什么情况下该用 Web Components?
结合我的踩坑经验,以下场景可以考虑:
- 需要向非现代前端技术栈(jQuery、AngularJS、甚至纯 HTML)提供可复用组件
- 构建设计系统(Design System)的基础原子组件,要求与框架解耦
- 开发浏览器插件或嵌入式小工具(如客服浮窗、反馈按钮)
- 团队愿意接受稍弱的开发体验,换取长期维护性和兼容性
但如果你只是在 React 项目里想搞组件化——拜托,用 JSX 不香吗?
结语:考公路上的技术执念
写这篇文章的时候,已经是凌晨一点。窗外是上海静安区某老小区的寂静,屋里只有机械键盘的敲击声。明天还要早起刷言语理解,但今天必须把这篇 blog 写完——不为别的,就为那些还在和 Shadow DOM 死磕的兄弟们少走点弯路。
Web Components 不是未来,但它是一种务实的选择。在大厂追逐微前端、低代码的浪潮里,这种“回归原生”的思路反而让我觉得踏实。就像我现在的生活:一边写着最前沿的前端代码,一边背诵“为人民服务”的申论金句——看似割裂,实则都是在解决问题。
技术没有高低贵贱,能交付价值的就是好技术。至于我?等考上公务员,说不定还能用 Web Components 给政务系统写个现代化界面呢(笑)。
附:我的 GitHub 示例仓库
如果你想看完整可运行的代码,欢迎 star & fork:
github.com/yourname/product-card-wc
(包含 IE11 polyfill 配置、Babel 打包脚本、以及那个差点让我崩溃的 composed 事件 demo)
P.S. 如果你在考公 + 写代码的路上,欢迎留言交流——咱们互相打气,早日上岸!

评论 0