一次组件化演进的真实实战:Web Components 的探索与落地

接口字段消失术
2025-06-16 22:47
阅读 314

开篇背景:为什么选择 Web Components?

开篇背景:为什么选择 Web Components?

去年,我负责的项目是一个大型的企业级后台管理系统,需要支持多个业务线的产品模块开发。每个业务线都有自己的前端团队,但大家都面临一个共同的问题:组件复用难、样式冲突频繁、协作成本高。我们试过使用 React 和 Vue 的组件库,也尝试过基于 CSS-in-JS 方案来隔离样式,但始终没找到一个真正“跨框架”的通用方案。

就在这个时候,我在一个技术会议上听到有人提到 Web Components,起初觉得这是一个“听起来很酷但没人真用的技术”,不过当深入研究后,我发现它不仅解决了我们很多痛点,而且浏览器支持也在不断完善,甚至主流框架(如 Vue、React)也开始拥抱它。

于是,我们决定在新一期重构中尝试用 Web Components 来做基础组件库的统一封装和分发。这篇文章就是我在这次项目中的真实经历分享。


问题描述:项目中的具体痛点

问题描述:项目中的具体痛点

先说说当时我们面临的几个核心问题:

  1. 组件重复开发严重
    不同团队都在重复造轮子,同一个“按钮”组件可能有三四种实现方式,功能相似却风格迥异。

  2. 样式污染频繁
    由于各业务线使用不同技术栈,CSS 冲突特别严重。有时候一个组件引入后整个页面样式就变了。

  3. 协作效率低
    需要维护多个版本的组件库,文档不一致,接口设计混乱。沟通成本极高。

  4. 框架绑定明显
    用 React 实现的组件无法在 Vue 或 Angular 项目中直接使用,必须重新实现,导致资源浪费。

  5. 升级维护困难
    每次主流程组件更新,都要同步多个仓库,修复多个依赖项,稍有不慎就会崩溃。

这些痛点让我们意识到:我们需要一种更底层、更原生、更标准的方式来构建可复用、独立、自洽的组件


解决方案:Web Components 到底是什么?

解决方案:Web Components 到底是什么?

如果你不了解 Web Components,我可以简单介绍一下它的核心技术点:

  • Custom Elements(自定义元素):你可以注册一个新的 HTML 标签,比如 <my-button>
  • Shadow DOM(影子DOM):提供一个隔离的作用域,确保样式和结构不会影响全局。
  • HTML Templates(模板标签):可以在 HTML 中声明一段不立即渲染的内容,方便复用。
  • ES Modules 支持:现代浏览器支持通过原生 import 加载 JS 组件。

这几个特性的组合,让 Web Components 成为了一种真正的“组件化原生解决方案”。

我们的目标:

  1. 用 Web Components 构建统一的 UI 组件库
  2. 支持多框架环境下的快速接入
  3. 提供稳定 API 接口,避免样式污染
  4. 确保性能表现良好,兼容主流浏览器

代码实践:从零开始写一个 Web Component

为了验证可行性,我先写了一个最简单的 Web Component:一个带 icon 的按钮组件。这个组件后来演化成我们整个组件库的基础。

第一步:定义一个 Custom Element

class MyButton extends HTMLElement {
  constructor() {
    super();
    
    // 创建 Shadow DOM
    this.attachShadow({ mode: 'open' });
    
    // 定义按钮内容
    const button = document.createElement('button');
    button.textContent = this.getAttribute('label') || '点击';
    button.style.backgroundColor = this.getAttribute('color') || '#007bff';
    button.style.color = '#fff';
    button.style.padding = '8px 16px';
    button.style.border = 'none';
    button.style.borderRadius = '4px';
    button.style.cursor = 'pointer';

    // 添加 click 事件
    button.addEventListener('click', () => {
      this.dispatchEvent(new Event('click'));
    });

    // 将按钮插入 shadow DOM
    this.shadowRoot.appendChild(button);
  }
}

// 注册组件
customElements.define('my-button', MyButton);

使用方法

<!-- 在任何项目中使用 -->
<my-button label="提交" color="#28a745"></my-button>

<script>
  const btn = document.querySelector('my-button');
  btn.addEventListener('click', () => {
    alert('点击了按钮!');
  });
</script>

这个例子虽然简单,但它展示了 Web Components 的核心特性:自定义标签、封装逻辑、隔离样式,以及跨项目复用。


踩坑经验:遇到的那些事儿

说实话,在实际推进过程中,并不是一帆风顺的,我们踩了不少坑。

坑1:Shadow DOM 样式穿透问题

刚开始我们以为 Shadow DOM 是完全封闭的样式作用域,但实际上某些 CSS 属性还是会“漏出去”。比如:

  • font-familycolor 会继承自外层
  • 外层样式也能影响到组件内部

解决办法:

  • 手动重置所有继承属性
  • 给根节点加一些默认样式以防止继承污染
:host {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}

坑2:IE11 兼容性限制

虽然我们主要面向的是现代化浏览器,但有些客户还是坚持使用 IE11,这就带来了麻烦。

  • Web Components 在 IE11 上是不支持的
  • 需要使用 polyfill,比如 webcomponents.js

我们最终做了以下处理:

  • 对于旧浏览器降级显示基础样式
  • 使用 feature detection 自动加载 polyfill
  • 并设置白名单,只对必要组件启用 polyfill

坑3:动态属性传递与响应性不足

Web Components 本身不具备响应式能力,像属性变更并不会自动触发界面更新。

解决办法:

  • 使用 attributeChangedCallback
  • 监控属性变化并手动更新组件状态
static get observedAttributes() {
  return ['label', 'color'];
}

attributeChangedCallback(name, oldValue, newValue) {
  if (name === 'label' && this.button) {
    this.button.textContent = newValue;
  }
  if (name === 'color' && this.button) {
    this.button.style.backgroundColor = newValue;
  }
}

坑4:打包和部署流程复杂

最初我们是手动写 ES Module 文件,然后在各个项目中引用,但后来发现这种方式难以维护。

解决方案:

  • 用 Rollup 打包 Web Components
  • 输出 ES5 + ESM 版本供不同环境使用
  • 发布 NPM 包(私有 + 公共)
  • 支持按需加载(结合动态 import)

实际项目整合:如何与主流框架共存?

我们并没有要求项目全部切换成 Web Components,而是让它们成为通用基础库的一部分。

以下是我们在不同框架中的集成方式:

在 Vue 项目中使用

Vue 默认支持自定义元素,只需在配置中忽略前缀即可:

Vue.config.ignoredElements = ['my-button'];

然后就能正常用了:

<template>
  <div>
    <my-button label="保存" @click="handleSubmit" />
  </div>
</template>

在 React 项目中使用

React 不太喜欢小驼峰命名,所以你要用全小写的 tag 名:

function App() {
  const handleButtonClick = () => {
    console.log('clicked!');
  };

  return (
    <div>
      <my-button label="提交" color="#dc3545" onClick={handleButtonClick} />
    </div>
  );
}

需要注意的是,React 的合成事件不能直接绑在 Web Components 上,需要使用 addEventListener

useEffect(() => {
  const btn = document.querySelector('my-button');
  btn.addEventListener('click', handleButtonClick);
  return () => btn.removeEventListener('click', handleButtonClick);
}, []);

性能优化:别忘了用户体验

虽然 Web Components 本身轻量,但我们依然做了不少性能优化:

  1. 延迟加载策略:只有在元素首次出现在视口时才进行初始化。
  2. 减少不必要的重绘:组件内尽可能减少 DOM 操作。
  3. 使用虚拟滚动/可视区域渲染机制:对于列表类组件尤其重要。
  4. 静态资源懒加载:图片、字体等大文件都做了 lazy load。
  5. 构建压缩:Rollup + terser,体积控制在极小范围。
  6. 监控指标:用 Lighthouse 检查加载速度和交互时间。

另外,我们还开发了一个内部工具来可视化组件加载性能,帮助前端同学快速定位瓶颈。


效果总结:带来了哪些实质收益?

经过几个月的推进,我们取得了不错的效果:

项目 改进点 结果
组件一致性 统一组件样式和行为 各业务线视觉风格趋于统一
协作效率 组件库集中管理 新需求响应速度提高约40%
技术债务 减少重复开发 组件数量减少了近一半
兼容性 支持多种框架 多个项目无缝接入
性能 优化加载和渲染 页面加载速度提升,Lighthouse 得分平均增加15分

最有意思的是,有位实习生说:“原来不用框架也能写出这么漂亮的组件!” ——这让我意识到,Web Components 让更多人看到了原生的力量


我的几点建议与注意事项

如果你正在考虑是否采用 Web Components,或者已经在尝试的路上,我想给你以下几个建议:

✅ 适用场景推荐

  • 企业级项目,涉及多个团队协作
  • 需要跨框架复用的 UI 组件
  • 对样式隔离有严格要求的项目
  • 长期维护且希望降低技术债的系统

❌ 什么时候不推荐?

  • 项目生命周期较短,追求快速上线
  • 需要大量状态管理和动画交互(这时候更适合 React/Vue)
  • 团队缺乏标准化意识或工程化能力较低

🛠 开发与调试小技巧

  • 使用 Chrome DevTools 的“Elements”面板查看 Shadow DOM
  • 在组件中加入 console.debug() 日志辅助排查
  • 利用 Storybook + WC 构建组件文档演示平台
  • 用 ESLint 插件规范组件命名规则(如 kebab-case
  • 设置合理的语义化标签名,方便语义化和 SEO

尾声:回到原点,看到未来

移动端适配方案-1

其实我一直觉得,技术的本质是解决问题,而不是追求酷炫的新名词。这次 Web Components 的尝试,让我们找到了一个既能满足现有需求又能面向未来的解决方案。

虽然它不是万能药,也不是银弹,但在我们项目的特定阶段,它确实带来了很多意想不到的价值。

如果你也正面临组件复用、协作困难、样式污染等问题,不妨试试 Web Components。它不一定适合所有人,但对于需要长期维护、注重架构设计和跨团队协作的项目来说,它真的值得一试。

或许有一天,你会发现:原来最好的组件,就是原生写出来的那一行 <my-component>


写于深夜咖啡厅,窗外雨声沥沥,耳机里放着 Coldplay 的《The Scientist》——

「我们走得太远,是不是该回头看看?」
但有时候,回头看看,正是为了走得更远。

评论 0

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