从零搭建到落地:Web Components 带给我的组件化新体验

如虎添翼
2025-06-14 03:14
阅读 590

作为一名前端开发者,经历过 jQuery 时代、 Backbone/AngularJS 的 MVC 潮流、再到 React/Vue 这类现代框架的洗礼之后,我对“组件化开发”这个概念可以说是再熟悉不过了。

然而,在一个项目中,我们遇到了前所未有的挑战:多个团队共用一套 UI 组件,但各自使用的技术栈不同,维护成本极高。

在尝试各种方案(包括跨技术栈封装、共享 NPM 包、微前端等)后,我们决定回到原点——Web Components,因为它足够原生、足够通用、足够灵活,且天生支持现代浏览器。

这篇文章会围绕我亲身经历的一个项目展开,分享我们是如何通过 Web Components 把看似不可能统一的 UI 层统一起来的。希望你也能从中看到它在未来可能扮演的角色。


背景:多技术栈下的 UI 碎片化难题

背景:多技术栈下的 UI 碎片化难题

事情要从两年前说起。当时我在一家中大型互联网公司负责一个企业级系统架构的升级。我们的产品矩阵中有多个子系统,分别由不同的团队负责:

  • 营销后台是用 Vue.js 开发的
  • 数据分析平台是 Angular 的遗产项目
  • 客服工单系统则是一个轻量级的 React SPA
  • 更不用提还有一些老的 PHP 页面直接嵌套在主站里

这些系统虽然功能不同,却都需要使用一致的 UI 设计语言和交互规范。更关键的是,有几组核心组件(比如按钮、表单、模态框)是每个系统都在重复实现的。

最头疼的问题来了:UI 不一致 + 维护困难 + 功能更新同步滞后
每次设计师说“按钮圆角换一下”,我们需要同步改四个地方……这显然是不合理的。

于是我们开始寻找一个能跨越技术栈的通用解决方案。


尝试过的路:不是不够好,就是太重或太脆弱

尝试过的路:不是不够好,就是太重或太脆弱

我们尝试过几种常见的做法:

1. 用 npm 包 + 各框架适配器

简单来说,把公共组件抽成 npm 包,然后为每个框架写一个 adapter。比如一个 <BaseButton> 在 Vue 中是个 .vue 文件,在 React 里是 .jsx 组件。

问题在于:

  • 每次改动都要同时维护多个分支
  • 需要额外处理样式隔离和依赖冲突
  • 团队间协作效率低,尤其对非主流技术栈(如旧版 Angular)

2. 使用 Webpack Module Federation 微前端方案

我们想借助微前端的思想,让主应用加载其他子系统的组件资源。听起来不错,但在实际中出现很多兼容性问题,特别是在生命周期管理、状态隔离方面,复杂度陡增。

而且,并不是所有团队都准备好迁移到微前端结构,这种“为了复用而重构”的代价太高了。


转折点:Web Components 浮出水面

转折点:Web Components 浮出水面

最终让我们眼前一亮的,是那个一直被提及却鲜有人真正在生产环境中使用的方案:Web Components

你可能会问:“Web Components?这不是 HTML5 刚出来时候的概念吗?”
确实如此,但它的几个特性刚好解决了我们的痛点:

  • 原生支持:不需要依赖任何框架
  • 真正的封装性:样式隔离、DOM 封装
  • 自定义标签:可以像普通 HTML 标签一样使用,语义清晰
  • 可与任意框架集成:无论你是 Vue、React、Angular 甚至原生 JS,都能自由使用

这简直是最适合我们当前场景的解耦方式!


实践:从一个 Button 组件开始

为了验证可行性,我们先从小处入手:从零开始打造一个 <ui-button> Web Component。

Step 1:基本结构搭建

我们使用原生 JavaScript 创建了一个简单的按钮组件:

class UIButton extends HTMLElement {
  constructor() {
    super();
    
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.innerHTML = `
      <style>
        button {
          padding: 8px 16px;
          border-radius: 4px;
          background-color: #007BFF;
          color: white;
          cursor: pointer;
        }
        button:hover {
          background-color: #0056b3;
        }
      </style>
      <button><slot>Default</slot></button>
    `;
  }
}

customElements.define('ui-button', UIButton);

然后直接在 HTML 或其他框架中使用它:

<ui-button>点击我</ui-button>

这一步非常顺利,也证明了它可以脱离框架运行。

Step 2:加上属性控制(props)

我们希望它支持一些常见的属性,比如 type="primary"disabledloading 等。

static get observedAttributes() {
  return ['type', 'disabled', 'loading'];
}

attributeChangedCallback(name, oldValue, newValue) {
  switch (name) {
    case 'type':
      this._setType(newValue);
      break;
    case 'disabled':
      this._setDisabled(newValue !== null);
      break;
    case 'loading':
      this._setLoading(newValue !== null);
      break;
  }
}

用户交互流程图-1

这部分花了些时间调试属性变更逻辑,但最后效果很自然,使用时就像操作普通 DOM 元素一样:

<ui-button type="success" disabled>提交</ui-button>

Step 3:事件绑定 & 行为封装

为了让组件具备交互能力,我们还加入了 click 事件触发机制:

this.shadowRoot.querySelector('button').addEventListener('click', () => {
  const event = new CustomEvent('click', {
    bubbles: true,
    composed: true,
    detail: { value: this.textContent.trim() }
  });
  this.dispatchEvent(event);
});

这样,外部就可以监听组件行为:

<ui-button id="my-button">提交</ui-button>
<script>
  document.getElementById('my-button').addEventListener('click', e => {
    console.log('按钮被点击啦');
  });
</script>

深入实战:如何把它用进真实项目?

有了原型之后,我们开始将它整合进各个系统,过程中遇到一些坑,也摸索出了一些经验。

1. 如何在 Vue 中使用 Web Component

Vue 对于原生 Custom Elements 支持良好,只需要注册忽略列表即可:

// vue.config.js
module.exports = {
  chainWebpack: config => {
    config.module
      .rule('js')
      .test(/\.js$/)
      .use('babel-loader')
      .loader('babel-loader');
  },
  devServer: {
    headers: {
      "Access-Control-Allow-Origin": "*"
    }
  }
};

// main.js
import './components/ui-button';

const app = new Vue({
  // ...
}).$mount('#app');

然后直接在模板里使用:

<template>
  <ui-button @click="submitForm">提交</ui-button>
</template>

需要注意的是:Vue 的 v-model 和事件命名需要稍微调整,因为 Web Components 发布的事件通常小写加中划线形式,而 Vue 默认推荐大写驼峰,建议使用事件映射:

<ui-button :value.sync="formValue" />
<!-- 需改为 -->
<ui-button :value="formValue" @update-value="val => formValue = val" />

2. 在 React 中使用 Web Component

React 对 Custom Elements 的支持也不错,但需要安装 polyfill 来应对较老版本浏览器:

npm install @webcomponents/webcomponentsjs

入口文件添加:

import '@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js';
import '@webcomponents/webcomponentsjs/webcomponents-bundle.js';

然后在组件中引入并使用:

import './components/ui-button';

function App() {
  return (
    <div>
      <ui-button onClick={() => alert('React 也能搞定!')}>React按钮</ui-button>
    </div>
  );
}

React 对大小写敏感,所以要注意属性命名尽量避免混淆。例如:

<ui-button type='success'>成功</ui-button>

3. 样式隔离 & 全局污染防御

我们发现一个问题:有些项目的全局 CSS 会影响到组件内部元素,导致样式错乱。

解决方法:

  • 所有样式必须通过 shadow DOM 注入,不要使用 external stylesheets。
  • 复杂组件可以考虑使用 CSS-in-JS 方案注入内部样式。
  • 外部传入的主题变量可通过 CSS 变量传递,保持一定的灵活性。
:host {
  --button-bg-color: #007bff;
}

挑战与优化:踩过的那些坑

当然,Web Components 并不是银弹,我们也遇到不少现实问题。

1. 浏览器兼容性问题

尽管主流浏览器已经广泛支持 Custom Elements V1,但在某些内核较低的环境下(比如微信小程序里的 WebView)仍存在兼容问题。

解决办法是引入官方 polyfill:

<script src="https://unpkg.com/@webcomponents/webcomponentsjs@2.4.3/webcomponents-bundle.js"></script>

如果你使用构建工具,也可以按需加载适配器:

import '@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js';
import '@webcomponents/webcomponentsjs/webcomponents-bundle.js';

2. 构建工具配置麻烦

刚开始的时候,我们尝试用 Rollup 来打包组件库,但遇到很多关于 ES Modules、Tree Shaking、打包格式的问题。

后来我们统一采用 Rollup + TypeScript + Babel 的组合:

  • Rollup 负责打包模块
  • TypeScript 提供类型安全
  • Babel 负责向下兼容

最终输出两个版本:ESM(用于现代浏览器)和 UMD(用于传统环境),通过 package.json 的 modulemain 字段进行区分。

3. 缺乏热更新调试能力

由于 Web Components 是原生 JS 类,不像 Vue/React 有 Hot Reload 工具链。我们在开发初期很痛苦,每次改完代码都得刷新页面看效果。

解决方案是在本地起一个小 demo 页,结合 Webpack Dev Server 实时预览组件变化。


最终成果:组件库上线 & 总体收益

经过两个月的时间,我们打造了一个基础 UI 库,包含按钮、输入框、表格、对话框等常用组件。

最终的使用情况如下:

团队 技术栈 是否接入 Web Components
A 组 Vue 2.x
B 组 Angular 9+
C 组 React 17
D 组 Django 模板

最大的收益就是:

  • UI 一致性得到显著提升
  • 维护成本大幅下降
  • 设计变更只需一次发布,全系统生效
  • 各团队不必再关心组件实现细节,专注业务开发

心得与建议:给准备上手 Web Components 的你

如果你也在面临类似的组件复用问题,或者只是好奇 Web Components 是否值得投入学习,我想给你几点真诚的建议:

✅ 推荐使用的场景

  • 多技术栈并存的大型项目
  • 需要对外暴露 SDK 或插件接口的产品
  • 内部共享 UI 组件库建设
  • 想减少第三方依赖、追求更轻量级的封装

⚠️ 需要谨慎的地方

  • 不适合复杂的业务逻辑封装:Web Components 更偏向 UI 组件层面,业务逻辑还是交由上层框架处理更合适。
  • 初期学习曲线较高:尤其是 Shadow DOM 的操作和样式限制,需要适应。
  • 社区生态不如主流框架丰富:很多现成的库还没完全拥抱 Web Components。

写在最后:未来属于真正开放的标准

回顾这段 Web Components 的实践之路,其实最让我感动的,是一种久违了的“回归本质”的感觉。

曾经我们被各种框架、DSL、抽象层包围着,而现在,用原生的方式写出可以在任何地方工作的组件,反而让我觉得更加踏实。

Web Components 不完美,但它代表了一种趋势——用标准化的方式解决通用问题,而不是用封闭生态圈制造壁垒。

也许有一天,它不会取代 React 或 Vue,但它一定会成为每一个现代前端系统中不可或缺的一环。


最后送大家一句我很喜欢的话:

“好的技术,不在花哨,在于能不能让你轻松地做正确的事。”

希望你也能在这条路上找到属于自己的答案。

如有疑问,欢迎留言交流~

评论 0

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