Web Components:被React“统治”多年后,我决定试试原生组件化
大家好,我是小林,一个双非学校的大二学生。别看我还在读大二,其实我已经在现在的前端组干了快两年了——从高中毕业那年暑假开始实习,到现在几乎成了“老人”。我们团队不大,但节奏贼快,上周五晚上又因为产品经理临时改需求加班到十一点,运维还在群里@我说“别把测试环境搞崩了”,我只能苦笑。
我一直是个底层原理爱好者,喜欢扒一扒浏览器到底怎么渲染、JS引擎怎么执行。虽然现在 React 几乎成了前端标配,但最近我越来越觉得,有些场景下它有点“杀鸡用牛刀”了。比如上个月,我们接到一个任务:给公司内部的多个老系统(有 jQuery 的、有 Vue 1 的、甚至还有纯原生 JS 的)统一嵌入一个数据采集模块,用于抓取页面上的关键信息——说白了就是个轻量级爬虫组件,要能跨框架复用。
这时候,React 就显得有点“重”了。你总不能让每个老系统都装一套 React 运行时吧?包体积、兼容性、依赖冲突……想想就头大。于是,我翻出了尘封已久的 Web Components 文档,决定亲自试试这个“原生组件化”的新(其实也不新)方案。
为什么是 Web Components?
Web Components 是一套由 W3C 标准定义的浏览器原生技术,包含四个核心部分:
- Custom Elements:自定义 HTML 标签
- Shadow DOM:封装样式和 DOM,避免污染
- HTML Templates:声明式模板
- ES Modules:模块化加载
最大的优势?不用任何框架,浏览器原生支持! 现代浏览器(Chrome、Edge、Firefox、Safari)基本都支持了,连 IE 都不用考虑(感谢公司去年彻底淘汰了 IE)。
更重要的是,它天生跨框架。你可以在 React 项目里用,在 Vue 里用,甚至在纯静态 HTML 里直接 <my-crawler></my-crawler> 插入,完全无感。
动手写一个“爬虫组件”
我们的需求很简单:用户点击按钮,组件自动扫描页面中符合特定 class 的元素(比如 .price, .title),提取文本并上报。
先看最终使用方式:
<!-- 在任何页面里,只要引入这个组件 -->
<script type="module" src="./crawler-component.js"></script>
<page-crawler
target-classes="price,title,description"
report-url="https://api.example.com/collect"
></page-crawler>
是不是清爽?不需要 npm install,不需要 build,直接 script 引入就能用。
第一步:定义 Custom Element
// crawler-component.js
class PageCrawler extends HTMLElement {
constructor() {
super();
// 创建 Shadow DOM,隔离样式
this.attachShadow({ mode: 'open' });
// 初始化配置
this.targetClasses = this.getAttribute('target-classes')?.split(',') || [];
this.reportUrl = this.getAttribute('report-url') || '';
}
connectedCallback() {
// 组件挂载时创建按钮
this.render();
this.bindEvents();
}
render() {
this.shadowRoot.innerHTML = `
<style>
.crawler-btn {
padding: 8px 16px;
background: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.crawler-btn:hover {
background: #45a049;
}
</style>
<button class="crawler-btn">开始采集</button>
`;
}
bindEvents() {
const button = this.shadowRoot.querySelector('.crawler-btn');
button.addEventListener('click', () => {
this.scrapeAndReport();
});
}
scrapeAndReport() {
const data = {};
this.targetClasses.forEach(cls => {
const elements = document.querySelectorAll(`.${cls.trim()}`);
data[cls] = Array.from(elements).map(el => el.innerText.trim());
});
// 模拟上报
fetch(this.reportUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
.then(() => {
alert('采集成功!');
})
.catch(err => {
console.error('上报失败:', err);
alert('采集失败,请检查网络');
});
}
}
// 注册自定义元素
customElements.define('page-crawler', PageCrawler);
踩坑记录
Shadow DOM 里的事件怎么冒泡?
最初我以为 Shadow DOM 完全隔离,结果发现click事件其实会穿透到主文档。但如果你在 Shadow 内部用了stopPropagation(),外部就收不到了。这点要小心,不过对我们这种独立组件影响不大。属性更新没反应?
Web Components 的属性默认不会自动响应变化。如果需要动态修改target-classes,得实现attributeChangedCallback并声明observedAttributes:static get observedAttributes() { return ['target-classes', 'report-url']; }IE?不存在的
公司早就放弃 IE 了,所以 polyfill 也没加。但如果你的用户还在用 IE,得用 webcomponents/polyfills。不过说实话,2024 年还支持 IE 的项目,建议直接重构。
和 React 组件对比:谁更香?
我知道很多人会问:既然我们组主要用 React,为啥不用 React 写个组件然后用 ReactDOM.render 挂载到老系统里?
理论上可行,但实际很麻烦。比如:
- 每个老系统都要引入 React + ReactDOM(~40KB gzipped)
- 如果老系统本身有 React,版本冲突怎么办?
- 构建流程复杂,得单独打包一份 UMD 格式的 bundle
- 调试困难,React DevTools 在老系统里可能不生效
而 Web Components:
- 无依赖,纯原生
- 体积小(上面那个组件压缩后不到 2KB)
- 开箱即用,一行 script 搞定
- 调试直接看 Elements 面板,Shadow DOM 也能展开
为了验证,我做了个简单对比:
| 特性 | Web Components | React 组件 |
|---|---|---|
| 体积 | ~2KB | ~40KB+ |
| 跨框架 | ✅ 原生支持 | ❌ 需额外处理 |
| 样式隔离 | ✅ Shadow DOM | ❌ 需 CSS Modules / styled-components |
| 学习成本 | 中(需理解 Shadow DOM) | 低(团队熟悉) |
| 开发体验 | 一般(无热更新) | 优秀(HMR、DevTools) |
| 浏览器支持 | 现代浏览器✅ | 依赖 Babel/构建 |
结论:简单、独立、跨框架的 UI 功能,Web Components 更合适;复杂交互、状态管理强的,还是 React 香。
实战:在 React 项目里用 Web Components
有意思的是,Web Components 也能在 React 里用!虽然 React 对自定义元素的支持有点“别扭”(比如属性要用 camelCase 转 kebab-case),但配合一点封装,完全可行。
// React 中使用
import './crawler-component.js'; // 引入
function App() {
return (
<div>
<h1>我的 React 应用</h1>
{/* 注意:React 不识别 kebab-case 属性,得用 dataset 或 ref */}
<page-crawler
ref={el => {
if (el) {
el.setAttribute('target-classes', 'price,title');
el.setAttribute('report-url', 'https://api.example.com/collect');
}
}}
/>
</div>
);
}
或者更优雅的方式:用一个 wrapper 组件:
const CrawlerWrapper = ({ targetClasses, reportUrl }) => {
const ref = useRef(null);
useEffect(() => {
if (ref.current) {
ref.current.setAttribute('target-classes', targetClasses.join(','));
ref.current.setAttribute('report-url', reportUrl);
}
}, [targetClasses, reportUrl]);
return <page-crawler ref={ref} />;
};
这样,同一个组件,既能用在 React 项目,也能用在 jQuery 项目,真正做到“一次编写,到处运行”。
性能和兼容性
我们在测试环境跑了性能对比:
- 首次加载时间:Web Components 快 300ms(因为没 React 初始化)
- 内存占用:Web Components 低约 15%
- 交互响应:几乎无差别
兼容性方面,Can I Use 显示:
- Chrome 67+ ✅
- Firefox 63+ ✅
- Safari 10.1+ ✅
- Edge 79+ ✅
国内主流浏览器(基于 Chromium)基本没问题。唯一要注意的是,某些国产浏览器的“极速模式”可能有问题,但我们的内部系统只允许用 Chrome,所以无忧。
总结:小而美的解决方案
这次实践让我对 Web Components 刮目相看。它不是要取代 React,而是填补了轻量级、跨框架、原生集成的空白。
尤其适合以下场景:
- 内部工具组件(如埋点、监控、爬虫)
- 微前端中的子应用共享 UI
- 第三方 SDK(比如客服插件、支付按钮)
- 老系统渐进式改造
作为双非出身、靠自学走到现在的人,我特别喜欢这种“回归原生”的思路。不需要花里胡哨的框架,用浏览器自带的能力解决问题,既高效又可控。
当然,Web Components 也有短板:开发体验不如 React/Vue、生态弱、调试工具少。但对特定场景,它真的是“刚刚好”。
最后,分享一句我常对自己说的:“不要为了用新技术而用,要为了解决问题而用。” 这次,Web Components 解决了我们的痛点,就够了。
下周,我打算用它重构公司的统一登录浮层。要是成功了,再来分享!
P.S. 今天又是 8 点起床写代码的一天。虽然双非,但代码不会骗人。加油,打工人!

评论 0