Web Components:一次彻底解放组件复用的尝试

轻舟开发记
2025-06-15 05:39
阅读 358

开篇:为什么我要写这篇文章?

作为前端开发工程师,我们每天都在和“组件”打交道。React、Vue、Angular 这些框架早已深入人心,甚至成为了行业标配。然而在实际工作中,我逐渐发现了一个问题:当我们试图跨项目、跨团队、甚至跨技术栈共享 UI 组件时,这些框架的优势反而变成了掣肘。

有没有一种更轻量、更原生、更具通用性的组件化方式?直到有一次,我在一个大型微前端项目中接触到 Web Components,才发现原来 HTML 原生就支持这种能力。它不需要你绑定任何一个框架,也不需要引入庞大的构建流程。

这篇文章想跟你聊聊我亲身实践 Web Components 的经历——从最初的质疑到后来的坚定使用,以及过程中踩过的坑。如果你也在思考组件复用、跨团队协作、微前端集成等问题,或许这篇文章能给你带来一些启发。


问题描述:我们的组件,真的复用了吗?

事情发生在去年的一个企业级项目上,当时我们部门负责搭建一个中台系统,这个中台系统要为多个子业务线提供统一的 UI 界面、表单控件和数据展示组件。

原本我们是基于 Vue 开发的,也封装了若干组件,但随着不同业务线接入,麻烦来了:

  1. UI 组件无法跨团队使用:其他组有的用 React,有的还用 jQuery,组件根本没法直接搬过去。
  2. 样式冲突频繁:虽然用了 scoped 样式,但由于组件是通过 npm 包发布的,引入后样式依然相互影响。
  3. 版本管理困难:每次更新一个 bug,都要通知所有引用方更新依赖。
  4. 开发调试成本高:不同项目的环境配置差异大,本地测试很麻烦。

这时候我们就在想,有没有一种“原生”的方式,可以脱离任何框架进行组件定义和使用?


解决方案:Web Components,原生组件化的新思路

经过调研,我们决定尝试使用 Web Components 来重新封装核心组件。

什么是 Web Components?

简单来说,Web Components 是一组浏览器原生标准,主要包括以下几个关键技术点:

  • Custom Elements(自定义元素)
  • Shadow DOM(影子 DOM)
  • HTML Templates(HTML 模板)
  • ES Modules(ECMAScript 模块)

它的优势在于:

  • 不依赖任何框架,纯 HTML/CSS/JS 实现
  • 组件样式隔离(Shadow DOM)
  • 可直接嵌入任意网页或应用
  • 可发布为 NPM 包供多端使用
  • 未来兼容性更好(因为它是原生标准)

我们的目标

  • 将中台的核心 UI 组件(比如按钮、表格、弹窗等)重构为 Web Components
  • 支持多种框架项目使用(Vue、React、jQuery 都行)
  • 所有组件风格统一,便于维护
  • 发布为独立 NPM 包,降低依赖耦合度

代码实践:一步步搭建你的第一个 Web Component

下面是一个简化版的按钮组件示例,演示如何用 Web Components 实现一个可复用的 <custom-button>

HTML 模板

<!-- template.html -->
<template id="button-template">
  <style>
    .btn {
      padding: 8px 16px;
      border-radius: 4px;
      font-size: 14px;
      background-color: #007bff;
      color: white;
      border: none;
      cursor: pointer;
    }
    .btn:hover {
      background-color: #0056b3;
    }
  </style>
  <button class="btn"><slot>Click Me</slot></button>
</template>

JavaScript 定义组件

// custom-button.js
class CustomButton extends HTMLElement {
  constructor() {
    super();

    // 创建 Shadow DOM
    const shadow = this.attachShadow({ mode: 'open' });

    // 加载模板
    const template = document.getElementById('button-template');
    const clone = template.content.cloneNode(true);

    shadow.appendChild(clone);
  }

  connectedCallback() {
    const button = this.shadowRoot.querySelector('button');

    button.addEventListener('click', () => {
      this.dispatchEvent(new CustomEvent('custom-click', {
        detail: { message: '按钮被点击啦!' },
        bubbles: true,
        composed: true
      }));
    });
  }
}

// 注册组件
customElements.define('custom-button', CustomButton);

页面使用方式

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Custom Button Demo</title>
  <script type="module" src="./custom-button.js"></script>
</head>
<body>
  <custom-button oncustom-click="handleClick()">Submit</custom-button>

  <script>
    function handleClick(event) {
      console.log('Custom event:', event.detail.message);
    }
  </script>
</body>
</html>

响应式布局概念图-2

是不是很简单?但这只是开始。


踩坑经验:那些年我们一起趟过的雷

刚开始的时候我们也是信心满满,结果实际落地过程中还是遇到不少问题,分享几个印象深刻的:

✅ 1. Shadow DOM 与全局样式冲突处理

一开始我们以为用了 Shadow DOM 就万无一失了,结果上线后发现某些字体没有继承过来。原来是有些公共 CSS 字体设置是在全局 <html> 上定义的,而 Shadow DOM 里并没有自动继承。

解决方案:

  • 在组件内部单独引入字体文件(@font-face),或者将字体变量抽离为 CSS 变量注入
  • 使用 ::part()::theme() 来允许外部定制特定区域的样式(现代浏览器已支持)

✅ 2. 表单组件无法提交的问题

我们在封装 <custom-input> 的时候发现,在父页面用 <form> 提交时,并不会触发事件。这是因为组件本身不是 native input 元素。

解决方法:

  • 在组件内部监听用户输入,手动同步 value 到 <input> 元素并暴露接口供外部获取
  • 或者让组件包裹原生 input,同时封装其行为

✅ 3. 浏览器兼容性挑战

虽然大多数现代浏览器都支持 Web Components,但在一些旧客户现场仍然存在兼容性问题(IE11、老版本 Chrome)。

应对策略:

  • 引入 webcomponents.js Polyfill
  • 构建时区分目标浏览器环境,输出对应的 polyfilled 包
  • 不再支持 IE11 后,移除 polyfill 减少包体积

✅ 4. 调试困难

Web Components 的 Shadow DOM 默认隐藏在 DevTools 中,调试起来不方便。

小技巧:

  • 在 Chrome DevTools 设置里开启 "Show user agent shadow DOM"
  • 使用 this.shadowRoot 打印调试节点
  • 组件内部加个 debug 标志,控制是否显示 Shadow DOM 结构

效果总结:这套组件体系带来了什么?

经过几个月的迭代,我们终于完成了第一期重构。效果还是挺明显的:

评估维度 之前 之后
组件复用难度 需适配不同框架 所有项目一键引用
样式冲突 高频发生 几乎零干扰
性能开销 较高(框架加载) 轻量级组件 + 按需加载
调试便利性 靠 console.log 可视化 Shadow DOM,配合 DevTools 得心应手
构建部署效率 依赖复杂构建流程 简单打包即可发布

CSS动画效果展示-1

另外,我们还将组件库打包上传到了私有 NPM 私服,各子项目只需:

npm install @company/shared-components

然后在入口 JS 中导入所需组件,就可以在任意 HTML 页面直接使用。


经验分享:给正在考虑 Web Components 的你

结合我这几年使用 Web Components 的经验,这里给你几点实用建议:

🧭 1. Web Components 是工具,不是银弹

它适合用于封装 高度稳定、交互逻辑相对固定、样式隔离要求高 的组件,例如:

  • UI 控件(按钮、输入框、下拉菜单)
  • 数据可视化图表
  • 公共布局模块(Header、Footer)
  • 外部嵌入脚本(比如广告位、统计埋点)

但对于交互复杂、状态管理频繁的组件,还是推荐使用成熟的框架来实现。

💡 2. 推荐搭配 ES Module + Rollup/Webpack 构建

虽然 Web Components 可以原生运行,但如果要跨项目使用并打包成 NPM 包,还是建议使用主流构建工具:

  • 使用 Rollup 更加轻量,适合发布纯组件库
  • 使用 Webpack 可整合 TypeScript、Babel、CSS modules 等高级功能
  • 构建出 UMD、ESM、CommonJS 等多种格式,增强兼容性

🔍 3. 关注性能与可访问性

  • Shadow DOM 虽好,但也可能导致渲染性能下降,尤其是嵌套过深
  • 注意语义化标签和 Aria 属性的使用,提升无障碍体验
  • 小组件可以考虑懒加载,用 IntersectionObserver 监听首次可视再加载

🔒 4. 安全性也要关注

  • Shadow DOM 隔离了样式,但不能完全防止 XSS
  • 动态插入 HTML 内容要小心 Sanitize(消毒),避免执行恶意脚本
  • 如果是开放平台组件,建议对属性传参做白名单校验

最后的感想

说实话,在用 Web Components 之前我也怀疑:“这玩意儿都出来这么多年了,为啥大家还在用 React/Vue?”
直到真正投入实战后,才意识到它填补了前端生态中的一个空白:一种真正跨框架、低耦合、可移植的组件形态

它并不意味着取代现有的框架,而是一种补充和融合的机会。尤其是在微前端架构日益普及的今天,如果想要打造一套真正“通用”的 UI 组件库,Web Components 无疑是一个值得尝试的方向。

希望这篇来自我实际工作场景的文章能帮你少走弯路,也欢迎你在评论区留言交流经验。毕竟技术这条路,就是在不断尝试中前行的!


📌 文章源码仓库地址:github.com/dreamapplelee/web-components-demo
如有帮助,请给个小⭐️~

评论 0

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