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

SystemArchitect
2025-06-25 11:09
阅读 225

开篇:背景与缘由

开篇:背景与缘由

去年年底,我们团队接手了一个大型的前端重构项目。项目的前身是一个用了七八年的旧系统,代码结构混乱、组件复用性差,维护成本非常高。更麻烦的是,这套系统已经被部署到多个客户环境里,不同客户间还存在部分定制化需求。

面对这种情况,团队内部对于技术选型产生了分歧:有人主张继续使用现有的 Vue,也有人想试试 Angular 的封装能力。而我提出了一个看似“保守”的建议——要不要尝试一下 Web Components?

起初大家对这个提议有些犹豫,毕竟当时主流框架如 React 和 Vue 都已经做得非常成熟,Web Components 听起来就像是浏览器原生的“冷门玩法”。但经过深入讨论和验证后,我们最终决定采用 Web Components 来作为这次重构的核心方案。

现在回过头来看,这不仅是一次技术上的小突破,更是让我重新认识了组件化开发的本质。


问题描述:我们在老项目中遇到了什么挑战?

问题描述:我们在老项目中遇到了什么挑战?

复杂度高、难以维护

原来的项目使用的是 jQuery + 原生 JavaScript 混合搭建的一套 SPA 架构,里面充斥着大量全局变量、DOM 直接操作、状态管理混乱。每次修改一个功能点,都像在拆地雷,稍有不慎就会影响其他模块。

而且由于历史原因,很多 UI 组件是直接复制粘贴写的,虽然看起来一样,但实际逻辑完全不同。比如一个“表格”组件,在不同的页面中竟然有不同的样式、不同的数据绑定方式,甚至还有不同的事件触发机制。这种“伪复用”反而成了负担。

客户定制难统一

由于我们的产品部署在多个客户环境中,每个客户都有自己的一些个性化需求。这些定制需求往往只涉及某个 UI 元素的显示逻辑或交互方式,但由于所有组件都是紧耦合的,想要做微调就必须 fork 整个组件甚至整个页面结构,导致后续升级困难重重。

技术栈锁定严重

原来的老项目基于某版本 Vue(还是 options API),但我们内部其实已经有新项目开始使用 React 和 TypeScript。这就造成一个问题:如果我们要做一个通用组件库,应该基于哪个框架写?不同技术栈之间的兼容性如何保证?

这也是我最初提出 Web Components 的关键原因之一——它是一种真正的跨框架通信语言


解决方案:为什么选择 Web Components?

Web Components 是一组标准规范,主要包括:

前端性能优化图表-2

  • Custom Elements:定义 HTML 标签
  • Shadow DOM:实现样式的封装和隔离
  • HTML Templates:声明式模板支持
  • ES Modules:模块化打包基础

它们并不属于某个具体的框架,而是原生浏览器的能力,只要你的项目能跑 JS,就能运行 Web Components。

我之所以推荐 Web Components,有几个关键点打动了我:

  1. 天然跨框架:你可以把它嵌入到 Vue、React、Angular,甚至是 jQuery 页面中。
  2. 可封装性强:通过 Shadow DOM 实现样式和行为的隔离,避免全局污染。
  3. 渐进式集成:可以逐步将老项目中的模块替换为 Web Components,而不必全盘重写。
  4. 轻量级无依赖:不需要引入庞大框架,适合构建小型工具类组件库。
  5. 未来趋势明确:越来越多的大厂开始重视,并构建基于 WC 的 UI 库(比如 GitHub Primer、Salesforce Lightning Web Components)。

我们是怎么做的?

第一步:选好脚手架工具

Web Components 虽然是原生特性,但真要从头开发也会遇到不少坑,比如:

  • 如何组织目录结构?
  • 如何调试?
  • 如何支持现代语法(TypeScript、CSS 变量、ES6 模块)?
  • 如何打包输出并集成到各种项目中?

我们调研了几种方式,最终选择了 OpenWC 提供的脚手架。它提供了一整套开箱即用的构建流程,包括:

  • 使用 Rollup 打包
  • 支持 ES Module 和 SystemJS
  • 内置 LitElement 和一些优秀的 Web Components 辅助库
  • 单元测试、lint 工具一应俱全

此外,我们还结合 Storybook for Web Components 来构建我们的组件文档系统,这对于后期交接和客户沟通至关重要。

第二步:从核心组件入手

我们决定先从几个高频使用的组件开始封装:按钮、对话框、分页器、表格等。目标不是追求大而全,而是确保这些组件在多种场景下都能稳定运行。

举个简单的例子,我们封装了一个 <my-button> 组件:

class MyButton extends HTMLElement {
  constructor() {
    super();
    this._shadowRoot = this.attachShadow({ mode: 'open' });
    this._shadowRoot.innerHTML = `
      <style>
        :host {
          display: inline-block;
          background-color: var(--button-bg, #007bff);
          color: white;
          padding: 8px 16px;
          border-radius: 4px;
          cursor: pointer;
        }
        :host([disabled]) {
          opacity: 0.5;
          pointer-events: none;
        }
      </style>
      <slot></slot>
    `;
    
    this.addEventListener('click', () => {
      if (!this.disabled) {
        this.dispatchEvent(new CustomEvent('my-click', { detail: {} }));
      }
    });
  }

  get disabled() {
    return this.hasAttribute('disabled');
  }

  set disabled(val) {
    val ? this.setAttribute('disabled', '') : this.removeAttribute('disabled');
  }
}

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

这段代码虽然不复杂,但它展示了 Web Components 的几个特点:

  • 封装了内联样式,防止外部干扰
  • 支持通过 CSS Variables 自定义样式
  • slot 实现内容透传
  • 用自定义事件进行通信

我们还给这个组件写了 Storybook 的 Demo:

export const Primary = () => html`
  <my-button @my-click=${() => alert('clicked!')}>提交</my-button>
`;

这样不仅能展示基本样式,还能模拟真实业务场景下的交互行为。

第三步:处理样式冲突与性能优化

Web Components 虽然自带 Shadow DOM,但也不是万能药。我们在实际开发过程中遇到了几个典型问题:

1. 主题样式穿透问题

有时候我们需要让 Web Components 支持父级的主题变量(比如颜色、字体大小)。这时候我们会使用 CSS 变量,配合 ::part 伪元素来暴露子元素样式接口:

:host {
  --inner-text-color: red;
}
<span part="text">${this.text}</span>

在外部样式表中就可以:

my-button::part(text) {
  color: var(--inner-text-color);
}

2. 性能瓶颈分析

我们发现当页面上同时渲染几十个 Web Components 时,会有轻微卡顿。于是我们用 Chrome DevTools 的 Performance 面板做了分析,发现是因为某些组件频繁触发 reflow。

解决办法是对组件生命周期进行优化,例如使用 debounce 控制更新频率,或者通过 MutationObserver 进行响应式更新控制。

3. 跨框架集成细节

在 Vue 或 React 中使用 Web Components 时需要注意以下几点:

  • 属性绑定需使用 :attrsetAttribute 方式传递非字符串类型
  • 事件需要手动监听并绑定回调函数
  • 使用 JSX/TSX 时可能需要额外 polyfill 或配置 Babel 插件(如 @babel/plugin-transform-react-jsx

比如在 React 中使用:

function App() {
  const onMyClick = useCallback(() => {
    console.log('Web Component Clicked!');
  }, []);

  return (
    <div>
      <my-button onClick={onMyClick}>Submit</my-button>
    </div>
  );
}

如果事件不能正常触发,可以尝试加 dangerouslySetInnerHTML 或用 addEventListener 动态绑定。


成果与收益

前端性能优化图表-1

经过半年多的努力,我们成功将原有系统的大部分通用 UI 组件替换为 Web Components。整体效果出乎意料的好:

指标 替换前 替换后
代码冗余率 40%+ <10%
组件加载时间 600ms+ ~180ms
团队协作效率 显著提升
定制化交付周期 1-2周 3天内

最重要的是,我们终于建立了一个真正意义上的跨项目、跨框架、跨客户的 UI 组件库,并且这套体系几乎不依赖任何第三方框架,极大降低了后期维护成本。

更难得的是,Web Components 的封装性和灵活性让我们在应对客户个性化需求时更加从容。我们可以快速构建新分支,修改少量样式和逻辑即可适配,而不必大规模改动代码结构。


我的经验分享

如果你也在考虑是否尝试 Web Components,我想分享几个心得体会:

✅ 不要用 Web Components 解决所有问题

它并不是银弹,更适合用于构建UI 组件层的基础设施,而不是用来替代 Vue/React 等应用层框架。

如果你要做的是复杂的单页应用、状态管理、服务端渲染……那它肯定不是首选。但在构建组件库、设计系统、微前端通信等领域,它的优势非常明显。

✅ 从简单组件开始,循序渐进

不要一开始就追求完美。先封装几个简单控件(比如按钮、输入框),熟悉生命周期管理和 Shadow DOM 的特性,再逐步封装复杂组件(如表格、树形菜单)。

✅ 注意浏览器兼容性

目前大多数主流浏览器都已支持 Web Components,但如果你的产品还需要支持 IE11,那就需要引入 polyfill(比如 @webcomponents/webcomponentsjs)。不过这类项目一般也比较有限,多数情况我们可以忽略。

✅ 配套工具链不能少

Web Components 本身是标准化规范,但它并不等于开箱即用。你需要一套完整的工程化体系:编译、打包、文档生成、单元测试,以及调试工具。

我们用到的工具有:

  • Storybook:文档 & 示例中心
  • Rollup:打包构建
  • ESLint + Prettier:代码规范
  • Karma/Mocha:单元测试
  • Chrome DevTools:调试利器

✅ 调试技巧:善用 DevTools

你可以直接在 Elements 面板中查看 Web Components 的 Shadow DOM 结构,也可以右键点击组件节点选择 “Reveal in Elements Panel”,快速定位到具体元素。

如果你想查看组件注册情况,可以在 Console 输入:

window.customElements.get('my-component')

结语:为什么说它是未来的趋势?

说实话,在刚接触 Web Components 时,我也曾质疑它的实用性。毕竟主流框架生态太强大了,Vue 也好,React 也罢,各自都有庞大的社区和丰富的生态。但经历过这次项目实战后,我才真正意识到它背后所代表的意义:

Web Components 是 Web 标准的延伸,而不是某种“框架的附属品”。

它的价值在于,它让我们不再受限于技术栈的边界,而是回归 Web 原本的设计哲学:开放、标准化、跨平台、互操作性。

如果你问我未来还会不会继续使用它?我的答案是肯定的。

当然,我不会盲目推广它去取代一切现有方案。但如果哪天你面临类似的问题:组件复用难、框架不统一、维护成本高……不妨试着打开 DevTools,写下你的第一个 Web Component,亲自感受下“原生组件化”的魅力。

相信我,那种“简洁而强大”的感觉,真的很爽。

评论 0

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