Web Components:原生组件化开发新趋势
上周五晚上十一点半,我合上笔记本电脑,泡了杯枸杞茶,终于把新项目里那个“神奇的悬浮反馈按钮”搞定了。没加班、没拉会、没被产品经理半夜钉钉轰炸——这在我们国企研发部简直是奢侈。坐在我对面的老王探头问:“你这按钮咋做到跨框架还能动画丝滑的?”我嘿嘿一笑:“Web Components,原生组件,香得很。”
是的,我就是那个在北京通勤一小时、白天喝茶看报、深夜写代码效率爆表的国企程序员。别笑,我们部门虽然节奏慢点,但技术热情可没落下。去年双11期间,隔壁电商组被 React 升级搞到崩溃,而我们却悄悄用 Web Components 把几个老系统里的交互模块重构了一遍——没动一行业务逻辑,UI 却焕然一新。
为啥要折腾这个“古董”?
说起来有点尴尬。我们系统里既有 Vue 2 的遗产,又有 AngularJS 的“活化石”,最近还掺了点 React 新模块。产品经理美其名曰“技术多元化”,实则让前端维护成了“缝合怪”。最离谱的是,同一个 Toast 组件,在三个框架里写了三遍,样式还不统一。测试同事每次提 Bug 都要备注“仅限 React 页面复现”,运维大哥看到 bundle size 直接血压飙升。
更惨的是,有一次我想复用一个带 Lottie 动画的加载指示器,结果光是处理框架差异就熬了两个通宵。当时真的想砸电脑——不是因为代码难写,而是明明功能一样,却要在不同地方重复造轮子。
就在那晚,我在 Stack Overflow 刷到一篇关于 Web Components 的旧帖,突然灵光一闪:浏览器原生支持的组件,不就是跨框架的终极解法吗?
原生组件真能打?
先说结论:能,而且打得挺稳。
Web Components 不是什么新技术,它其实由三部分组成:
- Custom Elements:自定义 HTML 标签
- Shadow DOM:封装样式和 DOM,避免污染
- HTML Templates:声明式模板
听起来很底层?但正因为“原生”,它不需要任何构建工具就能跑。Chrome、Edge、Firefox、Safari 全家桶都支持(IE?拜拜了您内)。更重要的是——它和 React、Vue 完全不冲突。
举个例子,我写了个 <fancy-tooltip> 组件:
class FancyTooltip extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
.tooltip {
background: #333;
color: white;
padding: 8px 12px;
border-radius: 4px;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-5px); }
to { opacity: 1; transform: translateY(0); }
}
</style>
<div class="tooltip">
<slot></slot>
</div>
`;
}
}
customElements.define('fancy-tooltip', FancyTooltip);
然后在 React 项目里直接用:
function App() {
return (
<div>
<button>
悬浮提示
<fancy-tooltip>我是跨框架 tooltip!</fancy-tooltip>
</button>
</div>
);
}
不需要 import,不需要 HOC,不需要 context bridge。React 只把它当普通 DOM 节点渲染,内部逻辑完全由 Web Component 自己管理。这意味着:一次编写,到处使用。
和 React 到底啥关系?
很多人一听“组件化”,第一反应就是 React。确实,React 的组件模型影响了一代人。但 React 是“运行时组件”,依赖虚拟 DOM 和 reconciliation;而 Web Components 是“平台级组件”,直接由浏览器理解。
| 对比项 | React 组件 | Web Components |
|---|---|---|
| 依赖 | 需要 React 运行时 | 浏览器原生支持 |
| 跨框架 | ❌(需额外桥接) | ✅(天然支持) |
| 样式隔离 | 靠 CSS-in-JS 或命名约定 | Shadow DOM 天然隔离 |
| 性能 | 虚拟 DOM diff 开销 | 直接操作真实 DOM |
| 学习成本 | 高(JSX、hooks 等) | 中(需理解 DOM API) |
我不是说 React 不好——我们内部新项目还在用 React。但当你需要共享 UI 原子能力(比如按钮、弹窗、动画控件)时,Web Components 更像是“基础设施”。
实战踩坑记
当然,理想很丰满,现实有坑。
第一个坑:属性传递。React 默认把 props 当作 attributes 传给 DOM,但 Web Components 期望的是 properties。比如你想传一个对象 {theme: 'dark'},直接写 <my-comp config={{theme:'dark'}} /> 是不行的,因为 attribute 只能是字符串。
解决办法?要么用 ref 手动赋值:
const ref = useRef();
useEffect(() => {
ref.current.config = { theme: 'dark' };
}, []);
return <my-comp ref={ref} />;
要么封装一层 React Wrapper,把 props 转成 properties。我们团队现在用 reactify-wc 这类小工具自动处理,省心不少。
第二个坑:事件冒泡。Web Components 内部 dispatch 的事件,默认不会穿透 Shadow DOM。如果你在 React 里监听 <my-button onClick={...} />,可能收不到 click。
解决方案是在组件内部显式设置 composed: true:
this.dispatchEvent(new CustomEvent('myclick', {
bubbles: true,
composed: true // 关键!
}));
第三个坑:开发体验。没有热更新、没有 TypeScript 智能提示、调试时 Shadow DOM 要手动展开……这些确实不如现代框架舒服。但我们用 Lit(一个轻量级 Web Components 库)+ Vite 搞定大部分问题。Lit 提供类似 React 的响应式更新,Vite 支持原生 ES Module,开发体验瞬间拉满。
效果如何?
上线三个月,成果喜人:
- 公共组件库从 3 套减为 1 套
- 新员工接入 UI 组件时间从 2 天缩短到 2 小时
- Bundle size 减少 18%(因为去掉了重复的 UI 逻辑)
- 最重要的是——再也不用跟产品经理解释“为什么这个按钮在 A 页面圆角、B 页面方角”了
上周团建,测试组长敬我一杯:“你们那个 tooltip 终于不闪退了!” 我笑笑,心里清楚:不是我多牛,是选对了技术路径。
写在最后:代码人生的另一种可能
很多人觉得 Web Components 是“过时技术”,是“React/Vue 的替代品”。但我觉得,它更像是前端生态的胶水——不争不抢,默默连接一切。
在我们这种节奏慢、系统杂、人员流动低的国企,追求最新框架反而容易翻车。而 Web Components 这种“稳如老狗”的方案,既能满足交互需求(比如我最爱的微交互动画),又不会给运维添堵,简直是天选之子。
如果你也在维护“祖传代码”,或者被多框架折磨得睡不着觉,不妨试试 Web Components。不用全盘重构,就从一个按钮、一个 loading 开始。你会发现,原生的力量,往往最持久。
对了,教程?网上一堆,但我建议直接看 MDN。别信那些“三天精通”的速成课——真正的代码人生,从来都是边踩坑边成长。
(写完这篇,已经凌晨一点。关电脑,回家。明天还要早起挤地铁呢。)

评论 0