Web Components:原生组件化开发新趋势
引言:为什么我会开始关注Web Components?

记得两年前,我加入了一个中型前端项目,项目初期采用的是React + Redux的经典架构,整个团队对React的生态体系也相当熟悉。然而随着业务快速扩张,组件复用性、协作效率和性能问题逐渐暴露出来。尤其是当我们需要将某个通用功能模块迁移到另一个非React项目时,才发现共享代码的复杂度远超预期。
当时我们尝试过各种方式,比如构建一个npm包导出React组件,但这显然限制了技术栈,不利于跨项目复用。而当我们想把这些模块嵌入到一些传统网站时,更是束手无策——对方的技术栈可能连React都不存在。
这个问题让我开始思考:有没有一种不依赖特定框架、真正可插拔、开箱即用的组件化方案?后来,在一次技术分享会上,我第一次听到了关于 Web Components 的介绍。起初觉得它有点“古老”,毕竟浏览器原生支持已经好几年了,但在实际试水之后,我发现这可能是解决我们问题的一个关键突破口。
这篇文章就从我亲身参与的项目出发,聊聊我是怎么一步步引入 Web Components 并获得良好收益的。
一、项目背景与挑战:跨平台、高性能、易维护的诉求

项目的背景其实挺典型的:我们为一个教育类产品开发了一套课程播放器组件,包含视频播放、字幕切换、笔记记录等功能。这个播放器最初是写在 React 内部的一个高阶组件,但随着产品线扩展,我们需要:
- 在多个子系统中使用同一个播放器(有的用Vue,有的用纯HTML)
- 在第三方站点进行内嵌(如合作伙伴官网、H5页面等)
- 实现懒加载、低内存占用以提升加载速度
- 不希望播放器样式或JS逻辑影响宿主环境
这时候问题就出现了:
- React 组件太重,打包体积大且不能脱离React运行;
- 样式隔离困难,播放器内部样式容易被全局污染;
- 通信机制复杂,不同技术栈之间的数据交互成本高;
- 维护成本上升,每个平台都要单独维护一套相似的功能逻辑。
说白了,我们在重复造轮子,而且每造一次,都得小心翼翼地避开坑。
二、解决方案:为什么选择 Web Components?
Web Components 技术栈包括三个核心标准:
- Custom Elements:自定义 HTML 元素
- Shadow DOM:创建独立 DOM 和样式作用域
- HTML Templates:模板声明式编写组件结构
再加上 ES Module 的加持,我们可以完全摆脱对任何框架的依赖,写出真正的“原生”组件。
为什么说它适合我们的场景?
- 跨框架使用:无论是 Vue、React 还是裸 HTML,都可以直接通过
<custom-player>这种方式使用。 - 样式隔离:借助 Shadow DOM,组件内部样式不会影响外部,避免冲突。
- 轻量高效:没有复杂的虚拟 DOM diff 算法,启动速度快。
- 可封装性强:可以暴露清晰的 API 接口,便于与外界通信。
- 无需额外构建工具: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 构建组件文档库
结语:不是万能,但确实够用

Web Components 并不是银弹,它也有自己的局限性,但它提供了一种真正意义上“技术无关、平台无关”的组件化能力。尤其当你面对多技术栈协作、模块共享或微前端架构时,它的价值会越发体现出来。
我个人的经历告诉我:组件化的核心不是写得多漂亮,而是能不能用得好、用得广。Web Components 虽然看起来有些“朴素”,但它确实是目前最接近原生、最通用的一种方式。
如果你也在寻找一个能够“一处开发、处处可用”的方案,不妨试试 Web Components。说不定你会发现,回到原生也能玩得很快乐 😄
文章完。如果你也有类似实践经验,欢迎留言交流~

评论 0