Web Components:原生组件化开发新趋势,以及我在上海租房屋里踩过的那些坑

链表断了
2025-12-17 12:08
阅读 1510

上周五晚上十点半,我瘫在出租屋的破沙发上,盯着屏幕里又一个因为 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(如 attachShadowmode: '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

最热最新
暂无评论
链表断了Lv.1
0
影响力
0
文章
0
粉丝