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

低代码旁观者
2025-06-24 04:34
阅读 410

引言:为什么我会开始关注Web Components?

引言:为什么我会开始关注Web Components?

记得两年前,我加入了一个中型前端项目,项目初期采用的是React + Redux的经典架构,整个团队对React的生态体系也相当熟悉。然而随着业务快速扩张,组件复用性、协作效率和性能问题逐渐暴露出来。尤其是当我们需要将某个通用功能模块迁移到另一个非React项目时,才发现共享代码的复杂度远超预期。

当时我们尝试过各种方式,比如构建一个npm包导出React组件,但这显然限制了技术栈,不利于跨项目复用。而当我们想把这些模块嵌入到一些传统网站时,更是束手无策——对方的技术栈可能连React都不存在。

这个问题让我开始思考:有没有一种不依赖特定框架、真正可插拔、开箱即用的组件化方案?后来,在一次技术分享会上,我第一次听到了关于 Web Components 的介绍。起初觉得它有点“古老”,毕竟浏览器原生支持已经好几年了,但在实际试水之后,我发现这可能是解决我们问题的一个关键突破口。

这篇文章就从我亲身参与的项目出发,聊聊我是怎么一步步引入 Web Components 并获得良好收益的。


一、项目背景与挑战:跨平台、高性能、易维护的诉求

一、项目背景与挑战:跨平台、高性能、易维护的诉求

项目的背景其实挺典型的:我们为一个教育类产品开发了一套课程播放器组件,包含视频播放、字幕切换、笔记记录等功能。这个播放器最初是写在 React 内部的一个高阶组件,但随着产品线扩展,我们需要:

  • 在多个子系统中使用同一个播放器(有的用Vue,有的用纯HTML)
  • 在第三方站点进行内嵌(如合作伙伴官网、H5页面等)
  • 实现懒加载、低内存占用以提升加载速度
  • 不希望播放器样式或JS逻辑影响宿主环境

这时候问题就出现了:

  1. React 组件太重,打包体积大且不能脱离React运行;
  2. 样式隔离困难,播放器内部样式容易被全局污染;
  3. 通信机制复杂,不同技术栈之间的数据交互成本高;
  4. 维护成本上升,每个平台都要单独维护一套相似的功能逻辑。

说白了,我们在重复造轮子,而且每造一次,都得小心翼翼地避开坑。


二、解决方案:为什么选择 Web Components?

Web Components 技术栈包括三个核心标准:

  • Custom Elements:自定义 HTML 元素
  • Shadow DOM:创建独立 DOM 和样式作用域
  • HTML Templates:模板声明式编写组件结构

再加上 ES Module 的加持,我们可以完全摆脱对任何框架的依赖,写出真正的“原生”组件。

为什么说它适合我们的场景?

  1. 跨框架使用:无论是 Vue、React 还是裸 HTML,都可以直接通过 <custom-player> 这种方式使用。
  2. 样式隔离:借助 Shadow DOM,组件内部样式不会影响外部,避免冲突。
  3. 轻量高效:没有复杂的虚拟 DOM diff 算法,启动速度快。
  4. 可封装性强:可以暴露清晰的 API 接口,便于与外界通信。
  5. 无需额外构建工具:ES Module 直接支持现代浏览器加载。

于是我们决定,把原来那个 React 组件重构为 Web Component,目标就是“一次开发,到处运行”。


三、代码实践:从零到有打造自己的组件

我选择用 TypeScript + ES Module + Shadow DOM 来搭建整个组件框架。下面是一个简化版的播放器组件实现过程。

1. 定义自定义元素

class CustomPlayer extends HTMLElement {
  private shadowRoot: ShadowRoot;
  private videoElement!: HTMLVideoElement;

  constructor() {
    super();
    this.shadowRoot = this.attachShadow({ mode: 'open' });
    
    const template = document.createElement('template');
    template.innerHTML = `
      <style>
        :host {
          display: block;
          width: 100%;
          background-color: #000;
          border-radius: 8px;
        }
        .player-container {
          position: relative;
        }
        video {
          width: 100%;
          display: block;
        }
      </style>
      <div class="player-container">
        <video controls></video>
      </div>
    `;

    this.shadowRoot.appendChild(template.content.cloneNode(true));
    this.videoElement = this.shadowRoot.querySelector('video')!;
  }

  connectedCallback() {
    const src = this.getAttribute('src');
    if (src) {
      this.videoElement.src = src;
    }
  }

  static get observedAttributes() {
    return ['src'];
  }

  attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {
    if (name === 'src' && newValue !== oldValue) {
      this.videoElement.src = newValue!;
    }
  }
}

customElements.define('custom-player', CustomPlayer);

这个组件非常简单,只实现了基本的视频播放功能,但你已经可以看到几个关键点:

  • 使用 Shadow DOM 隔离样式
  • 对外暴露 src 属性作为配置
  • 使用 connectedCallback 控制生命周期

2. 使用组件

无论你在什么环境里,只要浏览器支持 ES Module,都可以这样使用它:

<!DOCTYPE html>
<html lang="en">
<head>
  <script type="module" src="./dist/custom-player.js"></script>
</head>
<body>
  <custom-player src="https://your-domain.com/video.mp4"></custom-player>
</body>
</html>

是不是比你想象得要简单?


四、踩过的坑和解决方法

虽然 Web Components 听起来很理想,但在实际落地过程中,也遇到了不少让人头疼的问题。

1. 样式隔离不如预期?

有时候我们会发现,某些 CSS 框架(比如 Tailwind 或 Bootstrap)还是会影响 Shadow DOM 中的内容。原因在于,如果你在 Shadow Root 外部定义了一些全局字体、颜色变量,这些可能会穿透进来。

解决办法:

  • 明确重置所有基础样式(如使用 reset.css)放入 Shadow DOM 内部
  • 避免使用全局 CSS 变量,或自己手动定义一份隔离后的变量
  • 使用 BEM 或更具体的类名来减少冲突

2. 浏览器兼容性如何?

现代浏览器大部分都已原生支持 Web Components,但在IE11上肯定是不行的。为此我们做了一个判断机制:

if (!window.customElements) {
  // 动态加载 polyfill
  import('https://unpkg.com/@webcomponents/webcomponentsjs@2.4.7/webcomponents-bundle.js')
}

当然,这只是个折中方案。如果项目不强制要求兼容老版本浏览器,完全可以跳过这个步骤。

3. 如何与父页面通信?

Web Components 是封闭的黑盒,想要与外部通信怎么办?答案是使用事件。

// 触发自定义事件
this.dispatchEvent(new CustomEvent('play', { detail: { time: new Date() } }));

// 外部监听
document.querySelector('custom-player').addEventListener('play', (e) => {
  console.log(e.detail.time);
});

这种方式足够灵活,也可以很好地控制组件间通信的粒度。

4. 如何调试?

刚开始的时候我总是不知道组件是否真的被正确挂载,或者 Shadow DOM 中的结构是否符合预期。后来我总结了几点经验:

  • 在 Chrome DevTools 中,打开“设置 -> Preferences -> Elements -> Show user agent shadow DOM”可以看到完整的结构
  • 给组件添加一个 debug 属性,用于输出日志到控制台
  • 利用浏览器断点查看执行流程,特别注意组件的生命周期回调函数

五、实施效果与收获

重构完成后,我们将新的播放器部署到了多个项目中,并逐步替代原有的 React 版本。经过几个月的运行,取得了以下成效:

指标 原React组件 Web Components
包体积 ~600KB ~90KB
加载时间 ≈800ms ≈150ms
组件复用率 低(需适配不同框架) 几乎100%通用
开发维护成本 显著降低

最重要的是:不再需要针对不同技术栈写不同的组件实现,我们只需要专注做好组件本身,然后像“积木”一样在任何地方拼装即可。

而且因为使用了 Shadow DOM,用户反馈说样式混乱、样式冲突的现象几乎消失了,用户体验也更好了。


六、我的经验分享与建议

作为一个在前端一线摸爬滚打多年的人,我想给正在考虑使用 Web Components 的同学几点建议:

✅ 优势明显的场景:

  • 需要在多个异构项目之间共享 UI 组件(React/Vue/裸HTML)
  • 组件需要嵌入到第三方网页中(如广告组件、客服入口)
  • 要求样式完全隔离,避免污染宿主环境
  • 希望组件尽可能轻量级,加快初始加载速度

❌ 不太推荐的场景:

  • 需要大量状态管理、数据绑定或动画的复杂交互组件
  • 团队对现代原生 JS/TypeScript 掌握程度不高
  • 仍需强兼容 IE 浏览器的项目(除非愿意加Polyfill)

🧪 推荐的开发工具链:

  • 编译打包:Rollup / Webpack + TypeScript 支持
  • 本地调试:Vite 是个不错的选择,支持 ES Module 热更新
  • 单元测试:用 Jest 或 Vitest 写单元测试没问题
  • 文档说明:配合 Storybook 构建组件文档库

结语:不是万能,但确实够用

现代网页界面设计示例-1

Web Components 并不是银弹,它也有自己的局限性,但它提供了一种真正意义上“技术无关、平台无关”的组件化能力。尤其当你面对多技术栈协作、模块共享或微前端架构时,它的价值会越发体现出来。

我个人的经历告诉我:组件化的核心不是写得多漂亮,而是能不能用得好、用得广。Web Components 虽然看起来有些“朴素”,但它确实是目前最接近原生、最通用的一种方式。

如果你也在寻找一个能够“一处开发、处处可用”的方案,不妨试试 Web Components。说不定你会发现,回到原生也能玩得很快乐 😄


文章完。如果你也有类似实践经验,欢迎留言交流~

评论 0

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