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

算法曹建国导师
2025-06-20 01:33
阅读 253

开篇

开篇

作为前端开发者,我们每天都在与“复用”打交道。从早期的jQuery插件,到后来的React、Vue等现代框架,大家都在尝试更高效地组织代码、提高团队协作效率。但不管技术如何演进,组件化开发始终是不变的主题

而近年来,随着浏览器对Web Components规范的支持不断完善,越来越多团队开始尝试这一“原生”的组件化方案。我也在去年参与的一个项目中首次引入了Web Components,从最初的怀疑到后期的依赖,整个过程让我对它的理解发生了翻天覆地的变化。

今天我就想和大家分享一下这段经历,说说我为什么选择它,遇到了哪些坑,最后取得了什么样的成果,以及给正在考虑是否采用Web Components的你一些实用建议。


问题描述:项目背景与挑战

问题描述:项目背景与挑战

项目背景

我们的项目是一个大型的跨部门系统集成平台。前端由多个独立模块组成,分别由不同团队负责开发维护。这些模块之间有很多UI控件需要复用,比如按钮、输入框、弹窗、表单验证组件等等。

最初的技术选型是使用Vue + Vuex + Vue Router,配合微前端架构(qiankun)实现模块之间的通信和整合。但随着项目的推进,我们发现:

  • 组件复用成本高:每个团队都在重复造轮子,很多组件功能相似但风格不一
  • 版本管理复杂:由于各个模块使用不同版本的公共组件库,出现样式/行为不一致的问题
  • 性能负担加重:由于每个模块都自带一份框架副本,整体包体积增大,加载时间变长

此外还有一个比较棘手的问题:部分老旧系统必须运行在IE11环境下,这就导致我们不能轻易升级或引入较新的框架特性。

于是,我们在一次技术评审会上开始讨论一个可能的替代方案——是否可以利用原生的Web Components来解决这些问题?


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

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

其实我一开始对Web Components并不感冒,觉得它只是一个玩具级的API,真正开发还是得靠成熟的框架。但在实际调研后我发现,Web Components并不是取代框架,而是提供了一个更通用、更底层的组件封装机制

它有几个关键优势打动了我:

✅ 原生支持,无需框架依赖

这可能是最吸引人的地方。Web Components基于Custom Elements API构建,不需要任何框架即可运行。这意味着我们未来可以轻松将这些组件嵌入到React、Vue甚至Angular项目中,极大提高了可复用性。

✅ 风格隔离,避免CSS污染

Shadow DOM的引入让每一个组件都有自己的样式作用域,不会被外部全局样式污染,也避免了内部样式影响其他模块。这个特性对于多团队协同开发特别重要。

✅ 跨项目共享,统一版本控制

通过NPM发布自定义元素的方式,我们可以统一所有项目使用的组件库版本,从而保证视觉和行为的一致性。

✅ 性能优化潜力大

相比每个模块都引入一个完整的框架,纯HTML/CSS/JS构建的Web Component显然更加轻量,尤其适合那些只关心视图层的小型模块。

于是我们决定:以一个小的功能模块为试点,尝试迁移到Web Components技术栈,并评估其可行性。


实践落地:搭建第一个可复用组件

实践落地:搭建第一个可复用组件

我们选了一个相对独立的功能模块作为试点:用户反馈评分组件。这是一个用于让用户打星评价的产品反馈收集控件,具备交互性和一定的业务逻辑,适合作为迁移目标。

技术栈选择

  • 核心框架:原生Web Components(customElements + ShadowDOM)
  • 构建工具:Rollup.js
  • 样式预处理器:SCSS
  • 测试框架:Jest + Puppeteer

为了兼容旧系统,我们还做了Babel降级处理(转译ES6+语法),并加入了polyfill支持。

组件结构设计

class FeedbackRating extends HTMLElement {
  constructor() {
    super();

    this._rating = 0;
    this.shadow = this.attachShadow({ mode: 'open' });
  }

  connectedCallback() {
    this.render();
    this.setupEventListeners();
  }

  render() {
    const style = document.createElement('style');
    style.textContent = `
      .stars {
        display: flex;
        gap: 5px;
        cursor: pointer;
      }
      .star {
        font-size: 24px;
      }
      .filled {
        color: gold;
      }
    `;

    const container = document.createElement('div');
    container.className = 'stars';
    for (let i = 1; i <= 5; i++) {
      const span = document.createElement('span');
      span.className = 'star';
      span.setAttribute('data-rating', i);
      span.innerHTML = '★';
      container.appendChild(span);
    }

    this.shadow.innerHTML = '';
    this.shadow.appendChild(style);
    this.shadow.appendChild(container);
  }

  setupEventListeners() {
    this.shadow.querySelectorAll('.star').forEach(star => {
      star.addEventListener('click', (e) => {
        this._rating = parseInt(e.target.getAttribute('data-rating'), 10);
        this.dispatchRatingChange();
        this.updateStars();
      });

      star.addEventListener('mouseover', () => {
        this.highlightStars(parseInt(star.getAttribute('data-rating'), 10));
      });

      star.addEventListener('mouseleave', () => {
        this.updateStars();
      });
    });
  }

  updateStars() {
    this.shadow.querySelectorAll('.star').forEach(star => {
      const value = parseInt(star.getAttribute('data-rating'), 10);
      star.classList.toggle('filled', value <= this._rating);
    });
  }

  highlightStars(rating) {
    this.shadow.querySelectorAll('.star').forEach(star => {
      const value = parseInt(star.getAttribute('data-rating'), 10);
      star.classList.toggle('highlighted', value <= rating);
    });
  }


![移动端适配方案-1](https://code-guide.oss.shanghai.autogptai.club/common/file/download?name=date2025062001/f06e9f03-8302-4c71-ad58-23271d037687.jpg)


  dispatchRatingChange() {
    const event = new CustomEvent('rating-change', {
      detail: { rating: this._rating },
      bubbles: true,
      composed: true
    });
    this.dispatchEvent(event);
  }
}

customElements.define('feedback-rating', FeedbackRating);

使用方式

<feedback-rating></feedback-rating>

<script>
  document.querySelector('feedback-rating').addEventListener('rating-change', (e) => {
    console.log('用户评分:', e.detail.rating);
  });
</script>

看起来是不是很简单?没错,这就是Web Components的魅力所在:高度封装、便于使用、无需引入复杂框架


踩坑经验:从兴奋到崩溃再到豁然开朗

虽然整体体验还不错,但在开发过程中我们也踩了不少坑,这里我挑几个印象最深的分享给大家。

🚨 Shadow DOM样式穿透问题

我们一开始在外部页面写了一些CSS样式,试图修改组件的内部样式,结果完全不起作用——这是因为Shadow DOM默认是封闭的。

解决办法有两种:

  1. 在Shadow DOM内直接写样式(推荐)
  2. 使用::part()暴露指定节点供外部访问

比如我们在组件里这样写:

container.setAttribute('part', 'stars-container');

然后在父页面中可以通过以下方式覆盖样式:

feedback-rating::part(stars-container) {
  gap: 10px;
}

⚠️ 浏览器兼容性问题

虽然Chrome等现代浏览器已经较好支持Web Components v1规范,但我们还需要兼容IE11,这时候就必须加polyfill。

我们最终选择了SkateJS/web-component-shim这个社区维护较好的方案,效果不错。

使用方式如下:

<script src="path/to/webcomponents-bundle.js"></script>

需要注意的是,引入之后要添加defer属性,否则某些DOM操作会报错。

🐞 开发调试不够友好

相比于Vue DevTools那种所见即所得的调试方式,Web Components的调试确实有点原始。不过我还是总结了几个小技巧:

  • 使用浏览器的Elements面板查看Shadow DOM结构
  • 在组件构造函数中打印this,看看生命周期有没有正确触发
  • 利用console.table()输出对象数据,更清晰
  • 使用Jest做单元测试时,模拟事件触发判断状态是否变化

效果总结:迁移后的收益

经过大约一个月的重构和灰度上线,我们观察到了以下几个显著提升:

指标 迁移前 迁移后
页面首次加载速度 3.8s 2.6s
包体积大小 2.4MB 1.6MB
公共组件版本一致性
多团队协作难度 较高 显著降低
可维护性 中等

更重要的是,当我们需要把这个组件集成到老系统的时候,不再需要引入整个Vue环境,只需要一个简单的脚本标签就能搞定:

<script src="/dist/feedback-rating.min.js" defer></script>
<feedback-rating></feedback-rating>

这种“零门槛”接入方式受到了运维团队和产品组的高度认可。


经验分享:写给准备上车的你

如果你也在考虑使用Web Components,或者已经在试用了,那我可以把我踩过的坑和收获的经验总结成以下几点建议:

🎯 适用场景

  • 多项目间需要共享组件
  • 存在低配设备或旧浏览器需求
  • 不想被框架绑架,希望保持灵活性
  • 对性能敏感,追求更快的加载速度

🧱 技术建议

  • 合理使用Shadow DOM和CSS Part,不要过度封闭,适当保留样式可定制能力
  • 构建流程要自动化,用Rollup/Babel打包,结合TypeScript可以获得更好的类型保障
  • Polyfill策略要灵活,按需加载而非全部引入,减少性能损耗
  • 组件状态管理要小心,如果是纯展示组件还好,如果有复杂状态,建议搭配简单状态管理方案(如Redux-lite)

💡 心态调整

刚开始接触Web Components会觉得“怎么又回到手写DOM的时代了”,但慢慢你会发现,它其实是一种更高层次的抽象。你不再受限于某个特定的生态,而是拥有了一种可以在任何前端体系中工作的“元能力”。


结语:原生的力量不容忽视

如今回顾这次技术转型,我觉得最大的收获不是提升了多少性能,也不是节省了多少内存,而是让我重新认识到原生的力量。很多时候我们习惯性依赖框架,却忽略了浏览器本身提供的能力。

Web Components并不是银弹,但它提供了一个干净、简洁、跨框架的组件构建方式。特别是在当前前端生态百花齐放的情况下,能够有一套统一的组件通信语言,是非常有价值的事情。

如果你正苦于组件难以复用、框架绑定过重、性能优化乏力,不妨试试Web Components,也许它正是你需要的那一把钥匙。

别忘了,最好的工具永远是那个既能解决问题,又让人舒服的家伙。


作者注:文章中提到的完整示例代码已上传至GitHub仓库,欢迎Star交流:https://github.com/yourname/web-components-examples

评论 0

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