Web Components:我在大型前端项目中的原生组件化实战探索

NullPointer青年
2025-06-27 17:00
阅读 585

引言:为什么我又一次把目光投向了原生?

引言:为什么我又一次把目光投向了原生?

在一家用户量过千万的互联网公司,我作为前端团队的核心成员之一,主要负责产品中后台系统的搭建与维护。在过去几年中,我们经历了从 jQuery 到 React 再到 Vue 的技术栈变迁,也见证了组件化开发理念在前端领域的全面普及。

但最近,在一个跨平台需求繁重的新项目中,我重新将目光聚焦到了 Web Components 上。这不是一时兴起的技术尝鲜,而是经过深思熟虑后的选择。我想通过这篇笔记式的分享,讲讲我这段时间用 Web Components 搞定复杂组件复用、实现跨框架通信的真实经历。


问题描述:跨框架复用和组件耦合成了瓶颈

问题描述:跨框架复用和组件耦合成了瓶颈

我们的业务场景其实很典型:一个中后台系统需要接入多个不同技术栈的产品模块。比如:

  • 用户中心用了 Vue;
  • 数据看板用的是 React;
  • 一些老的页面仍然依赖 jQuery 插件;
  • 还有一些公共组件(弹窗、按钮、数据表格)需要统一风格。

这导致几个明显的问题:

  1. 样式不一致:每个模块自己管理一套 UI 基础组件,视觉差异严重;
  2. 功能重复开发:同一个“带搜索的下拉菜单”,三个小组各自开发了一套;
  3. 跨框架通信困难:React 组件想传个事件给 Vue 得绕三道弯;
  4. 维护成本高:改一个通用按钮样式要同步三个仓库,还得小心别出兼容性问题。

我们尝试过很多方式来解决这些问题:封装 npm 包、使用 Shadow DOM 封装组件样式、引入设计系统(Design System),但始终没能根治这个问题的根本矛盾:组件是基于某个框架的,而不是原生的 Web 规范

于是,我决定试试我一直没敢上生产环境的——Web Components。


解决方案:用 Web Components 构建真正的跨框架组件库

Step 1:选型调研

我们在内部组建了一个小团队进行技术选型,最终确定以 Web Components 为核心构建通用组件库,配合 Lit 作为开发框架。

Lit 是 Google 团队维护的库,轻量级、响应式更新机制清晰,而且对 TypeScript 友好。比起原生写 Custom Elements,它大大降低了开发门槛,还保留了足够的自由度。

我们评估过其他方案,比如 Stencil.js、Svelte 的 Custom Element 支持,但考虑团队学习成本和构建工具链的适配,最终选择了 Lit + Rollup 的组合。


Step 2:设计统一的组件 API 和生命周期

为了让组件真正“通用”,我们为所有 Web Components 设计了一套规范化的接口:

interface BaseComponent {
  // 属性
  disabled: boolean;
  label: string;
  options: Array<{ value: string; label: string }>;
  
  // 事件
  change: CustomEvent;
  click: CustomEvent;

  // 方法
  focus(): void;
  openModal(): void;
}

同时,我们遵循标准的 Custom Element 生命周期钩子(connectedCallback, disconnectedCallback 等),并在 Lit 中封装了一些通用逻辑:

class MyButton extends LitElement {
  @property() label = '点击我';
  @property({ type: Boolean }) disabled = false;

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

  private handleClick() {
    this.dispatchEvent(new CustomEvent('click'));
  }
}

这样就能在任何框架里直接使用:

<!-- Vue -->
<my-button label="提交" @click="handleSubmit"></my-button>

<!-- React -->
<MyButton label="提交" onClick={handleSubmit}></MyButton>

<!-- jQuery -->
<my-button id="submitButton"></my-button>
<script>
  document.getElementById('submitButton').addEventListener('click', () => {});
</script>

Step 3:封装样式与 Shadow DOM 的使用技巧

Web Components 非常适合封装隔离的样式。我们一开始用的是内置的 shadowRoot

createRenderRoot() {
  const root = this.attachShadow({ mode: 'open' });
  const style = document.createElement('style');
  style.textContent = baseStyles;
  root.appendChild(style);
  return root;
}

但很快我们就遇到一个问题:有些外部样式穿透进了 shadow DOM!

移动端适配方案-2

解决方案有两个方向:

  1. 严格控制 CSS 作用域:使用 BEM 或 CSS Modules 样式命名方式,尽量避免全局样式污染。
  2. 提供主题变量支持:使用 CSS 自定义属性(variables)让组件可以在运行时被“定制化”。

例如:

:host {
  --button-bg: #007bff;
}

button {
  background-color: var(--button-bg);
}

这样即使组件被嵌入不同的应用,也可以通过设置 --button-bg 来统一主题色。


Step 4:处理浏览器兼容性和性能优化

虽然现在主流浏览器都支持 Web Components,但为了兼容旧版本 Safari 和部分企业内网 IE(是的,还有人在用...),我们也做了一些兜底方案:

  • 使用 Polyfill 加载器按需注入;
  • 对于低性能设备,采用懒加载策略,只在首次使用时注册组件;
  • 使用 lit-element performance tips 做了一些微调,比如减少不必要的渲染、合理使用 @property() 装饰器等。

性能方面我们进行了几轮测试,发现 Web Components 的首屏加载速度比纯 JS 组件略慢,但在二次访问或缓存命中后,体验差距几乎可以忽略。


Step 5:跨框架协作与组件通信机制

Web Components 最大的优势之一就是它的通用性。我们借助它实现了跨框架的统一通信机制:

父子通信

使用属性和自定义事件即可完成基本交互,如:

document.querySelector('my-component').addEventListener('select', (e) => {
  console.log(e.detail);
});

全局状态共享

为了支持更复杂的场景,我们引入了类似 Redux 的小型状态容器,并通过自定义事件向外广播变更,供各 Web Component 订阅监听。

比如某个“用户信息组件”会监听 user-updated 事件:

connectedCallback() {
  super.connectedCallback();
  window.addEventListener('user-updated', this.handleUserChange);
}

disconnectedCallback() {
  window.removeEventListener('user-updated', this.handleUserChange);
}

虽然不是最完美的解法,但在没有全局状态管理的前提下,这种做法足够稳定有效。


效果总结:组件统一带来的效率提升

项目上线三个月后,我们总结了一下效果:

指标 结果
组件复用率 提升了约 70%
UI 风格一致性 视觉验收一次性通过,设计师不再频繁打回来
开发人员协作成本 下降了约 40%,特别是新同事上手更快
构建体积 相比之前多个组件库叠加,整体减少了 12%
性能表现 Lighthouse 分数保持在 90+,加载速度无显著下降

当然也有挑战,比如:

  • 某些框架对 Custom Elements 支持不太友好(React 特别明显);
  • 编写 TypeScript 类型时需要手动维护类型定义;
  • 测试起来不如普通组件方便。

但我们觉得收益远大于代价。


经验分享:Web Components 真的是下一个趋势吗?

CSS动画效果展示-1

作为一个在一线摸爬滚打多年的前端开发者,我的体会是:

Web Components 不是用来取代 React/Vue 的,而是用来填补它们之间缝隙的。

下面是我整理的一些经验建议,希望对你有帮助:

✅ 哪些场景适合用 Web Components?

  • 需要在多个不同技术栈间共享的组件;
  • 想要做插件市场或嵌入式 SDK 的场景;
  • 希望彻底隔离组件样式的组件;
  • 需要长期维护、跨技术演进周期使用的组件。

⚠️ 哪些坑需要注意?

  • 事件绑定语法在不同框架中的差异:React 默认不会自动绑定事件名(需要用 onXXX),而 Vue 更自然。
  • TypeScript 支持需要额外配置:最好自己写 .d.ts 文件或者用 custom-elements-manifest 自动生成类型定义。
  • 不要过度追求“完全原生”:适当结合像 Lit 这样的小而美的库会事半功倍。

💡 我的小技巧 & 开发心得

  • 调试 Shadow DOM 的时候可以用 Chrome DevTools 的 "Elements" 面板展开查看结构
  • window.customElements.get('xxx') 检查组件是否已正确注册
  • 利用 @web/test-runner 编写单元测试,模拟不同宿主环境下的行为
  • 对于复杂的交互,提前设计好接口边界,防止后期扩展失控

结语:前端生态百花齐放,我们也要多一把武器

Web Components 曾经被认为“未来可期却难落地”。但现在随着工具链成熟、浏览器厂商支持到位,它确实已经在越来越多的大厂项目中崭露头角。

在我参与的这个项目中,Web Components 成为了连接不同技术栈的一座桥梁,也成为我们统一设计语言的基石。

技术没有银弹,只有合适与否。我希望通过这次真实项目的分享,能帮你看到 Web Components 更实用的一面——它不仅仅是又一项炫酷的浏览器特性,而是真正能解决问题的工程实践。

如果你也在做跨框架组件复用、或者想打造自己的 UI 库,不妨试着迈出第一步。或许你会发现,原来原生开发也没那么难。

评论 0

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