Web Components:原生组件化开发新趋势

后端便利贴
2025-06-17 13:37
阅读 333

Web Components:在真实项目中踩过的坑与收获

Web Components:在真实项目中踩过的坑与收获

这篇文章的灵感源于去年我们团队接手的一个内部工具开发任务。当时我们面对一个比较典型的需求:为公司的多个业务系统开发一套统一、可复用的 UI 组件库,目标是减少重复开发、提升维护效率,并能在不同技术栈(React、Vue、甚至传统 jQuery 项目)中自由复用。

起初,大家很自然地倾向于使用 React 或 Vue 来构建组件库。然而现实并不总那么理想——我们的项目涉及太多历史遗留代码,技术栈五花八门,很多老系统甚至不支持现代 JS 模块化规范。在这种情况下,直接使用 React/Vue 开发的组件根本无法被直接引用到这些环境中。

就在大家都一筹莫展的时候,我突然想到了 Web Components 这个东西。虽然之前只是零星了解过一点概念,真正实践得不多,但它“原生”、“跨框架兼容”的特点正好契合当前项目的技术诉求。于是,我决定尝试用它来解这个局。

接下来就是一段从零开始的学习和探索旅程。这篇文章,我想以第一人称的角度分享一下那次真实的实战经历——包括我们遇到的问题、怎么一步步尝试解决、踩了哪些坑,以及最终的效果如何。希望能给正在考虑使用 Web Components 的朋友一些参考。


项目背景和问题描述

我们这套组件库的目标是实现如下能力:

  • 支持主流浏览器(包括 IE11)
  • 可在任意前端框架下使用,也可以脱离框架单独引入
  • 具备良好的性能表现和可维护性
  • 保持一定的灵活性,允许通过属性配置行为和样式

初期尝试用 React 构建组件库,但在集成到其他非 React 项目时遇到了严重依赖问题。比如,在一个基于 jQuery 的后台管理系统中,我们试图通过 Webpack 配出独立文件供 HTML 直接 <script> 引入,结果不仅体积膨胀(React + ReactDOM 超过 400KB),还因为加载顺序、模块未定义等问题频频报错,维护起来非常痛苦。

后来试过将组件封装成 Web Components 格式。第一次实验其实失败得很惨——组件渲染不出来,事件传不过去,CSS 样式还总是漏掉……但我知道这条路是对的,只是细节处理不到位。于是我重新梳理思路,调整方案,逐步完善了整个工程结构。


解决方案和技术选型

Web Components 是一组浏览器原生 API,主要包括 Custom Elements、Shadow DOM 和 HTML Templates,它们让我们能够创建可复用的、自定义的 HTML 元素。它的优势非常明显:

  • 不依赖任何框架
  • 封装性强(特别是 Shadow DOM 提供样式隔离)
  • 天然支持现代模块化开发方式(ES Modules)

为了更高效开发,我们选择了 Lit(前身是 Polymer 的 lit-html + lit-element)作为核心开发工具。Lit 提供了轻量级的模板语法、响应式状态管理,同时保留对 Web Components 原始特性的支持,非常适合构建可跨项目复用的组件。

为什么不是 React/Vue?

虽然这两个框架生态庞大、功能丰富,但在这种跨项目、多技术栈的场景下,它们更像是阻碍而非助力。我们要的是能嵌入到任何一个页面中的“小而精”的组件,而不是捆绑了运行时的一整套体系。


项目搭建与代码实践

首先,我们搭建了一个基于 Vite 的基础开发环境。Vite 对 ES Modules 支持非常好,可以做到热更新快速编译,开发体验很棒。

npm create vite@latest my-component-lib --template lit

生成出来的目录结构很清晰,其中关键点是我们会把每个组件写成 Lit 的类组件形式,然后通过 customElements.define() 注册为 Web Component。

下面是一个简单的按钮组件示例:

// components/my-button.js
import { html, css, LitElement } from 'lit';

export class MyButton extends LitElement {
  static styles = css`
    button {
      background-color: #007bff;
      color: white;
      border: none;
      padding: 8px 16px;
      border-radius: 4px;
      cursor: pointer;
    }
  `;

  render() {
    return html`<button @click="${this.handleClick}">Click me</button>`;
  }

  handleClick(e) {
    this.dispatchEvent(new CustomEvent('click', { detail: {} }));
  }
}

customElements.define('my-button', MyButton);

然后在 HTML 中可以直接使用:

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>My Web Component Demo</title>
</head>
<body>

  <my-button></my-button>

  <script type="module" src="/components/my-button.js"></script>
</body>
</html>

看起来是不是很简单?没错。但别高兴太早,后面还有不少坑等着你……


踩坑经验与解决过程

💥 Shadow DOM 中的样式丢失

最开始我们以为只要写了 CSS 样式,组件就能正确显示。但实际上,在 Lit 中声明的 styles 默认会被注入到 Shadow DOM 中,对外部不会产生污染,这当然是好事。

但如果你期望全局样式也能影响到组件内部,或者你需要让组件支持用户自定义主题变量(比如通过 CSS Variables 控制颜色),就会发现某些样式设置无效。

解决方法:

  • 使用 CSS Variables 实现主题化:
:host {
  --btn-bg: #007bff;
}
button {
  background-color: var(--btn-bg);
}
  • 在 Shadow DOM 外部定义样式并应用:
<style>
  .container my-button button {
    background: red !important;
  }
</style>
<div class="container">
  <my-button></my-button>
</div>

虽然不推荐滥用 !important,但在 Web Components 中有时候确实需要这样控制优先级。

📦 打包与发布问题

早期我们在使用 Rollup 做打包时遇到了一个问题:某些旧项目要求引入 UMD 版本,而 Web Components 通常依赖 ES Modules。

解决方法:

  • 使用 Rollup 分别构建 ESM、CJS 和 UMD 版本
  • 利用 package.json"module""main" 字段做区分
  • 提供全局变量挂载的方式(如 window.MyComponents = {...}

🧪 兼容性测试:IE11 的噩梦

尽管 Web Components 标准已经很成熟了,但想兼容 IE11 就必须引入 polyfill。

解决方案:

  • 添加 webcomponents.js 官方 polyfill:
<script src="https://unpkg.com/@webcomponents/webcomponentsjs/webcomponents-bundle.js"></script>
  • 注意某些特性如 adoptedStyleSheets 在 IE 上完全不支持,只能退而求其次采用动态插入 <style> 的方式

🔄 事件通信问题

由于 Web Components 本质上是原生 DOM 元素,所以其与父容器的通信要依靠自定义事件。但我们一开始在 Vue/React 中监听事件时,发现有些平台绑定不上。

解决办法:

  • 确保事件名为 kebab-case(如 my-event 而不是 myEvent
  • 在 React 中用 addEventListener 代替 JSX 的 onXxx 方式
useEffect(() => {
  const elem = document.querySelector('my-button');
  elem.addEventListener('my-event', handleMyEvent);
  return () => elem.removeEventListener('my-event', handleMyEvent);
}, []);

最终效果与收益总结

经过大约两个月的时间,我们成功完成了一套包含十余个高频使用的 UI 组件(如 Button、Input、Modal、Table 等)的组件库,所有组件均通过 Web Components 技术标准实现,支持多框架复用。

成果主要体现在以下几个方面:

  • 复用率显著提高:原本每个系统都要重复写的表单控件、弹窗等组件,现在只需简单引用即可。
  • 维护成本下降:一次修复,多处生效。
  • 技术债减轻:老系统不再需要引入大量 React 运行时就可以使用新组件。
  • 团队协作顺畅:不同小组可以各自负责不同组件模块,统一接口后组合使用。

更重要的是,团队成员在这个过程中都深入了解了 Web Components 和现代浏览器的能力边界,对原生 DOM 编程有了更深的认识。


我的经验分享与建议

如果你也在考虑使用 Web Components,不妨听听我的几点建议:

  1. 明确使用场景:不是所有项目都适合用 Web Components。如果你的项目本身就运行在一个现代框架内(如 Vue3+Composition API),那么没必要绕回原生;但如果涉及跨技术栈共享或需要长期维护的底层组件库,就值得投入。

  2. 从小处着手:不要一开始就追求完整组件库,先挑几个简单的 UI 控件练手,熟悉 Shadow DOM、事件通信、样式注入这些基本机制。

  3. 重视文档与命名规范:Web Components 是真正的“自定义标签”,名字要语义清晰,API 文档要详细说明属性、事件、插槽等内容。

  4. 考虑渐进升级策略:尤其是需要兼容旧项目时,务必提供 Polyfill,确保最低限度的功能可用性。

  5. 调试技巧很重要:在 Chrome DevTools 中查看 Shadow DOM 内容,可以用 “Right-click → Reveal in Elements Panel” 快速定位节点。另外记得开启开发者模式下的 Shadow DOM 显示。

  6. 性能优化不能忽视:Web Components 虽然是原生,但如果过度使用复杂渲染逻辑或频繁操作 DOM,同样会导致性能下降。合理使用生命周期钩子,避免不必要的重绘。

  7. 拥抱现代模块化:坚持使用 ES Modules 和 Tree-shaking 机制,让你的组件库保持轻盈。


结语:回到初心

回想那次开发经历,说实话并不轻松。特别是在项目中期遇到各种奇葩兼容性问题时,也一度怀疑过是否该继续折腾 Web Components。但当第一个组件在各个项目里跑通那一刻,还是很有成就感的。

Web Components 并非银弹,但它提供了一种简洁、稳定、持久的组件化路径。尤其在如今前后端分离越来越深、微前端架构大行其道的时代,它的价值显得愈加突出。

希望这篇真实经历对你有所帮助。如果你已经在用 Web Components 或者打算试试看,欢迎留言交流,一起聊聊那些踩过的坑!


作者简介:一名热爱开源、关注性能优化的前端开发者,有多年大型企业级前端架构设计经验,擅长解决复杂系统的组件复用问题。目前专注于 Web Components 在企业级项目中的落地实践。

评论 0

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