Web Components:我在项目中亲历的原生组件化尝试

吴浩天
2025-06-27 04:34
阅读 529

开篇背景:从一个重构需求说起

开篇背景:从一个重构需求说起

大概一年前,我所在的团队接手了一个内部工具系统的重构任务。这套系统已经运行了三年多,最初的版本是使用 jQuery + Handlebars 搭建的,后来逐步加入了 Vue.js 来替换部分老旧代码。但问题也出在这:前端技术栈混用、页面结构不统一、重复代码高、维护成本越来越高。

我们希望能统一整个项目的 UI 组件体系,并在多个项目中复用这些组件。当时摆在面前的有几个选择:

  1. 继续用 Vue 或 React 构建可复用组件
  2. 引入第三方组件库(如 ElementUI、Ant Design)
  3. 试一试 Web Components —— 这个一直听说但没实际用过的原生方案

出于对未来技术演进和跨框架能力的考量,我们决定挑战一下——用 Web Components 打造一套跨框架的组件库,先做几个关键组件验证可行性。接下来的故事,就是我在其中的一些经历与思考。


问题描述:老系统带来的三大痛点

问题描述:老系统带来的三大痛点

1. 技术栈分散,协作混乱

由于历史原因,不同模块由不同的人开发,有的用 Vue,有的还保留着原始的 JavaScript 写法,甚至有些地方用了 AngularJS!这就导致每次新功能开发都要先判断“在哪块代码里写”,沟通成本很高。

2. UI 样式不一致,视觉体验差

虽然有统一的设计规范文档,但由于缺乏实际的组件封装和复用机制,开发者基本靠手动复制 HTML 和样式。长此以往,相同类型的控件在不同页面上的表现千差万别,用户反馈也很频繁。

3. 维护复杂,升级困难

一旦某个基础交互方式或布局方式需要变更,就不得不在各个页面上逐一修改,容易遗漏、风险大、测试难覆盖。比如改一个按钮的 class 名称,往往要找遍几十个文件。


解决方案:为什么是 Web Components?

解决方案:为什么是 Web Components?

Web Components 是一组 Web 平台标准特性,包括 Custom Elements、Shadow DOM、HTML Templates 以及 ES Modules 等,它允许你创建自定义元素并封装其行为和样式。这恰恰是我们想要的:与框架无关的、真正可复用的组件单元

我们想打造的是一种“任何项目、任何人只要引入就能直接用”的组件模式。最终目标是让其他部门也能无缝接入我们的组件库,不管是用 Vue、React、Angular 还是纯 JS 的项目。


实战项目:从 Button 到 FormKit

实战项目:从 Button 到 FormKit

我们决定从小处入手,先实现几个核心组件,看看是否能在真实项目中落地。

第一个组件:<custom-button>

目标非常明确:替代项目中所有的 <button> 元素,统一外观、支持 loading 状态、点击反馈动效等。

class CustomButton extends HTMLElement {
  constructor() {
    super();
    
    const shadow = this.attachShadow({ mode: 'open' });
    
    const button = document.createElement('button');
    button.style.backgroundColor = '#4CAF50';
    button.style.color = 'white';
    button.style.border = 'none';
    button.style.padding = '10px 20px';
    button.style.fontSize = '16px';
    button.textContent = this.getAttribute('label') || 'Submit';

    const loading = document.createElement('span');
    loading.style.display = 'none';
    loading.innerHTML = 'Loading...';

    button.addEventListener('click', () => {
      if (this.loading) return;
      this.dispatchEvent(new CustomEvent('clicked'));
    });

    // 提供外部设置 loading 的接口
    Object.defineProperty(this, 'loading', {
      get: () => this.hasAttribute('loading'),
      set(val) {
        if (val) {
          this.setAttribute('loading', '');
          loading.style.display = 'inline-block';
          button.disabled = true;
        } else {
          this.removeAttribute('loading');
          loading.style.display = 'none';
          button.disabled = false;
        }
      },
    });

    shadow.appendChild(button);
    shadow.appendChild(loading);
  }
}

customElements.define('custom-button', CustomButton);

这段代码实现了:

  • 自定义标签名:<custom-button>
  • 支持通过 label 属性控制显示文字
  • 封装样式到 Shadow DOM 中避免冲突
  • 可控制 loading 状态和事件通信

接着我们在 Vue 页面中这样使用它:

<template>
  <div>
    <custom-button label="提交" @clicked="handleSubmit"></custom-button>
  </div>
</template>

<script>
export default {
  methods: {
    handleSubmit() {
      this.$el.querySelector('custom-button').loading = true;
      setTimeout(() => {
        this.$el.querySelector('custom-button').loading = false;
      }, 1500);
    },
  },
};
</script>

Vue 和 Web Component 同时存在居然很和谐!


持续扩展:构建更复杂的组件

尝到了甜头后,我们开始构建更复杂的组件,例如:

  • 表单输入组件组合 <form-input>,内置验证逻辑
  • 分页器 <pagination>
  • 数据表格 <data-table>,支持异步数据加载和排序

在这个过程中,我们也遇到了不少坑,以下是一些典型问题和解决方法。


踩坑经验分享:从入门到踩雷

🧱 Shadow DOM 风格隔离 vs 外部样式穿透

Shadow DOM 的最大优势就是样式隔离,但这也有副作用:有时候我们希望某些全局样式能作用到组件内(比如字体、主题色)。

解决方案:使用 CSS 自定义属性(CSS Variables)

:host {
  --main-color: #4CAF50;
}

然后在组件内部:

button.style.backgroundColor = 'var(--main-color)';

这样即使组件嵌套在外部样式环境下,也可以灵活控制主题色,而不会被污染。


🔄 与主流框架的绑定问题

尽管 Web Components 是框架中立的,但像 Vue 和 React 这类框架对动态属性和事件处理有自己的一套机制。

比如 React 中,默认是不会自动识别 Web Components 的自定义属性。你需要告诉 React 不要用驼峰命名转换,而是原样传递属性:

<custom-button label="提交" onClicked={handleClick}></custom-button>

在 React 组件中需要用如下方式注册属性白名单:

import { attributesToProps } from 'html-react-parser';

// 或者你可以显式声明允许的 props
const allowedProps = ['label', 'on-clicked'];

function registerCustomElement(tagName, Component) {
  customElements.define(
    tagName,
    class extends HTMLElement {
      connectedCallback() {
        const props = {};
        for (let attr of this.attributes) {
          if (allowedProps.includes(attr.name)) {
            props[attr.name] = attr.value;
          }
        }
        const reactElement = React.createElement(Component, props);
        ReactDOM.render(reactElement, this);
      }
    }
  );
}

前端性能优化图表-1

这种适配过程确实有点繁琐,但在 Vue 中会友好很多,因为 Vue 对属性的映射比较宽松。


🔍 开发调试小技巧

刚开始写 Web Components 的时候,我发现浏览器的 DevTools 显示的只有 <custom-button> 这么个黑盒子,里面啥都看不见,非常难受。

解法一:使用 console.dir() 输出 Shadow Root

const btn = document.querySelector('custom-button');
console.dir(btn.shadowRoot);

解法二:DevTools 的 “Computed” 面板 + “Show all computed styles”

这个面板可以帮你看到 Shadow DOM 中的元素和样式应用情况。


💥 性能优化与懒加载

对于大型组件库来说,一次性加载所有组件肯定是不可取的。我们采用的是基于路由的按需加载策略:

// 动态 import 加载
async function lazyLoadComponent(name, path) {
  await import(path);
  console.log(`Component ${name} registered`);
}

lazyLoadComponent('custom-button', './components/button.js');

此外,我们还使用了 vite-plugin-web-components-rollup 这样的插件,结合 Rollup 对组件进行打包压缩和代码拆分。


效果总结:我们得到了什么?

经过三个月的摸索和迭代,我们成功将 Web Components 应用于生产环境,以下是具体的收益点:

✅ 组件复用率提升 70%

过去每个页面都要 copy-paste 的按钮、表单、分页组件现在都变成了 <custom-button><form-field> 这种标准化组件。无论哪个工程师开发新功能,都能快速调用现成组件。

✅ 样式一致性大大提高

得益于 Shadow DOM 的封装,所有组件的 UI 样式不再受全局样式干扰。设计文档中的变量和规范也更容易落地。

✅ 渐进迁移友好

最让人惊喜的是,在已有 Vue/React/Angular 项目中引入 Web Components 几乎没有障碍,反而成为了我们统一各业务线 UI 风格的桥梁。

✅ 团队协作效率提升

大家不用再争论“该不该用某个框架组件”、“要不要重新封装”,只需要关注怎么高效地使用现有组件库,大大降低了沟通成本。


我的一些体会和建议

从这次实践中,我也积累了不少经验和感悟,如果你也在考虑是否使用 Web Components,这里有一些真心建议:

🧱 不要追求“一次封装处处通用”

Web Components 虽然框架中立,但它本质上是一种低级抽象。它适合那些需要高度稳定、不易变化的 UI 基础组件。对于复杂的交互逻辑或业务组件,还是推荐用 Vue/React 这样的框架来实现。

🛡️ 做好渐进演进的准备

迁移到 Web Components 是一个渐进的过程,不要指望一夜之间全部组件化。我们可以像搭积木一样,一边开发一边打磨,逐步丰富自己的组件库。

🧪 测试不能少,自动化才是王道

建议为 Web Components 编写单元测试和集成测试。我们可以用 Jest + Puppeteer 或 Playwright 来模拟真实渲染场景,确保组件在不同上下文下表现一致。

📦 发布方式也要注意

如果你打算将组件库对外发布,可以通过 npm 包的形式提供。配合 Rollup/Webpack 打包,让用户可以通过 ES Module 方式导入:

npm install my-components
import 'my-components/dist/custom-button';

结语:未来的组件化方向值得期待

其实写这篇文章的时候,我还回想起当初刚听到 Web Components 的时候那种疑惑:“真的有人用吗?”、“性能会不会很差?”、“兼容性怎么办?”

但实践之后发现,只要你了解它的特点、合理使用,它不仅能解决我们实际遇到的问题,还能带来前所未有的灵活性和可维护性。

在我看来,Web Components 正在悄悄成为前端组件化的“新大陆”。它不代表放弃现代框架,而是让我们在更高维度上建立统一的基础能力。

如果你正在寻找一种能让团队协作更顺畅、项目结构更清晰、未来更可维护的技术路径,不妨试试 Web Components,也许会有意想不到的收获。


最后,如果你喜欢这样的实战分享,欢迎留言交流,说不定下次我会讲讲我们是如何用 LitElement 快速构建组件的 😄

评论 0

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