2026 年,越来越多的项目开始"去框架化",直接用 Web Components + 原生 JavaScript 构建应用。这不是极客的偏执,而是浏览器原生能力成熟之后的理性回归。
Web Components 不是一个新的框架或库。它是一组浏览器原生支持的 Web 标准 API,让你能创建可复用的自定义 HTML 标签——就像
<video>、<input>一样,但由你定义。
一、Web Components 到底是什么?
1. Custom Elements(自定义元素)
让你能注册自己的 HTML 标签。比如 <product-card>、<my-button>、<app-header>。
// 定义一个标签
class ProductCard extends HTMLElement {
// ...组件逻辑
}
// 注册到浏览器——以后就能在 HTML 里用了
customElements.define('product-card', ProductCard);
注册完成后,你在 HTML 里这样用:
<!-- 这是合法的 HTML——浏览器认识它 -->
<product-card name="耳机" price="299"></product-card>
⚡ 关键理解:
customElements.define()不是在"安装"一个库,而是在通知浏览器——"嘿,当遇到<product-card>这个标签时,请执行这段构造函数"。这是和 React 的React.createElement完全不同的思路——后者需要引入整个 React runtime。
2. Shadow DOM(影子 DOM)
解决了前端开发中最头疼的问题——样式冲突。
想象你在页面里引入了一个第三方日期选择器,结果它的 .input 和你的 .input 冲突了,样式乱掉——这是所有前端开发者的噩梦。
Shadow DOM 就是浏览器的自带隔离机制:
class ProductCard extends HTMLElement {
constructor() {
super();
// 创建一个"影子树"——这里的样式和 DOM 对外部完全隔离
this.attachShadow({ mode: 'open' });
}
}
就像 <video> 标签的播放控件——你从来看不到它的内部 DOM 结构,也影响不了它的样式。Shadow DOM 就是给自定义组件赋予了同样的能力。
这意味着你写的组件样式天然不会污染全局,也不怕被全局样式污染——不需要 CSS Module、Styled Components、scoped、BEM 这些"补丁方案"。
3. HTML Templates(HTML 模板)
提供一个惰性的 HTML 片段,解析了但不渲染,等到需要时再克隆使用:
<!-- 定义模板(不会渲染,只是"存着") -->
<template id="card-template">
<style>/* 组件的样式 */</style>
<div class="card">
<slot name="title"></slot>
<slot name="content"></slot>
</div>
</template>
<script>
// 需要时再克隆出来
const template = document.getElementById('card-template');
const clone = template.content.cloneNode(true);
document.body.appendChild(clone);
</script>
三大 API 的关系
这三个 API 可以单独使用,但组合起来才是完整的 Web Components。Custom Elements 定义行为,Shadow DOM 提供隔离,Templates 优化性能。
与 React/Vue 的思维差异
| 维度 | React/Vue | Web Components |
|---|---|---|
| 核心 | 声明式框架——你用 JSX/模板描述 UI,框架运行时将它转换为真实 DOM | 浏览器原生标准——你直接操作真实的 DOM 元素,不需要额外的运行时 |
| 渲染机制 | 虚拟 DOM —— 内存中的 DOM 副本 + Diff 算法 | 真实 DOM —— 直接操作浏览器 DOM API |
| 样式方案 | CSS Module、Styled Components、scoped(都需要工具链支持) | Shadow DOM —— 浏览器的原生隔离,零开销 |
| 依赖体积 | React 约 120KB + ReactDOM 约 130KB + 你的代码 | 零 KB —— 这是浏览器内置能力 |
| 学习成本 | 需要学 JSX、hooks、编译工具链、路由、状态管理 | 需要学 3 个 Web 标准 API(Custom Elements + Shadow DOM + Templates) |
一个非常形象的类比:学习 React 就像学开车——你需要考驾照、熟悉仪表盘、适应各种辅助系统。而 Web Components 就像你本来就会的自行车——只是现在给你的自行车加了个电动马达。因为你本来就懂 HTML、CSS、JavaScript,Web Components 80% 的知识你已经会了。
Web Components ≠ 不用工具
虽然 Web Components 是原生标准,但你可以(不是必须)用工具提升开发体验:
选哪个取决于项目规模,但核心不变:无论用不用工具,最终产出的都是标准的 Web Components,在任何框架中都能直接使用。
二、先写一个 Demo
与其空谈,不如动手。我们用原生 Web Components 写一个产品卡片组件,包含图片、标题、价格、收藏按钮。
第 1 步:定义组件
// product-card.js
class ProductCard extends HTMLElement {
// 声明需要监听的属性
static get observedAttributes() {
return ['name', 'price', 'image', 'currency'];
}
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
}
attributeChangedCallback(name, oldVal, newVal) {
if (oldVal !== newVal) this.render();
}
render() {
const name = this.getAttribute('name') || '商品名称';
const price = this.getAttribute('price') || '0';
const currency = this.getAttribute('currency') || '¥';
const image = this.getAttribute('image') || '';
this.shadowRoot.innerHTML = `
<style>
:host { display: inline-block; width: 280px; border-radius: 12px; overflow: hidden; background: #fff; box-shadow: 0 2px 12px rgba(0,0,0,0.08); font-family: -apple-system, system-ui, sans-serif; }
:host(:hover) { transform: translateY(-4px); box-shadow: 0 8px 24px rgba(0,0,0,0.12); }
.image-wrap { width: 100%; height: 200px; overflow: hidden; background: #f5f5f5; }
.image-wrap img { width: 100%; height: 100%; object-fit: cover; }
.info { padding: 16px; }
.name { font-size: 16px; font-weight: 600; color: #1a1a1a; }
.price { font-size: 20px; font-weight: 700; color: #e74c3c; }
</style>
<div class="image-wrap"><img src="${image}" alt="${name}" loading="lazy"></div>
<div class="info">
<div class="name">${name}</div>
<div class="price"><span class="currency">${currency}</span>${price}</div>
</div>
`;
}
}
customElements.define('product-card', ProductCard);
第 2 步:在页面中使用
<!DOCTYPE html>
<html>
<head>
<title>Web Components Demo</title>
<script src="product-card.js"></script>
</head>
<body>
<h2>商品推荐</h2>
<product-card name="真无线降噪耳机" price="899" currency="¥" image="https://picsum.photos/seed/1/400/300"></product-card>
<product-card name="智能手表 Pro" price="1999" currency="¥" image="https://picsum.photos/seed/2/400/300"></product-card>
</body>
</html>
关键体验:你不需要安装 npm 包,不需要配置 webpack/vite,不需要学 JSX——写好一个 .js 文件,在 HTML 里直接引用就能用。
第 3 步:添加交互——收藏按钮
class ProductCard extends HTMLElement {
static get observedAttributes() {
return ['name', 'price', 'image', 'currency', 'liked'];
}
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._toggleLike = this._toggleLike.bind(this);
}
connectedCallback() {
this.render();
this.shadowRoot.querySelector('.like-btn')?.addEventListener('click', this._toggleLike);
}
disconnectedCallback() {
this.shadowRoot.querySelector('.like-btn')?.removeEventListener('click', this._toggleLike);
}
_toggleLike() {
const isLiked = this.getAttribute('liked') === 'true';
this.setAttribute('liked', isLiked ? 'false' : 'true');
this.dispatchEvent(new CustomEvent('like-change', { detail: { liked: !isLiked }, bubbles: true }));
}
attributeChangedCallback(name, oldVal, newVal) {
if (oldVal !== newVal) this.render();
}
render() {
const liked = this.getAttribute('liked') === 'true';
this.shadowRoot.innerHTML = `
<style>
:host { /* ...同上... */ }
.like-btn { background: none; border: 1px solid #ddd; border-radius: 8px; padding: 6px 12px; cursor: pointer; }
.like-btn.liked { background: #e74c3c; color: white; border-color: #e74c3c; }
</style>
<div class="info">
<div class="name">${this.getAttribute('name')}</div>
<div class="price">¥${this.getAttribute('price')}</div>
<button class="like-btn ${liked ? 'liked' : ''}">${liked ? '❤️ 已收藏' : '🤍 收藏'}</button>
</div>
`;
this.shadowRoot.querySelector('.like-btn')?.addEventListener('click', this._toggleLike);
}
}
看到区别了吗?没有 useState、没有 useEffect、没有虚拟 DOM。就是最朴素的属性驱动 + DOM API。而 Shadow DOM 自动做了样式隔离——你不用担心 .info 或 .price 的样式被全局 CSS 污染。
三、生命周期对照
| 阶段 | Web Components | React | Vue |
|---|---|---|---|
| 创建 | constructor() | constructor() | created() |
| 挂载 | connectedCallback() | useEffect([], []) | mounted() |
| 更新 | attributeChangedCallback() | useEffect() | watch/computed |
| 销毁 | disconnectedCallback() | useEffect(() => fn, []) | unmounted() |
| 属性监听 | observedAttributes 静态 getter | props 声明 | props 声明 |
四、用 Lit 让开发效率翻倍
如果觉得原生太啰嗦,可以用 Google 的 Lit 库——只有 4KB,但提供了声明式模板和响应式更新:
import { LitElement, html, css } from 'lit';
class ProductCard extends LitElement {
static properties = {
name: { type: String },
price: { type: String },
image: { type: String },
liked: { type: Boolean, reflect: true },
};
static styles = css`
:host { /* 同样样式隔离 */ }
.card { border-radius: 12px; overflow: hidden; }
`;
render() {
return html`
<div class="card">
<img src="${this.image}" alt="${this.name}" />
<h3>${this.name}</h3>
<p>¥${this.price}</p>
<button @click=${this._toggle}>${this.liked ? '❤️' : '🤍'}</button>
</div>
`;
}
_toggle() { this.liked = !this.liked; }
}
customElements.define('product-card', ProductCard);
Lit 版本的可读性和开发体验已经非常接近 React/Vue,但打包只有 4KB。而且它生成的依然是标准 Web Components,可以在任何框架中使用。
五、真实项目对比
案例 1:某电商商品详情页重构
重构前(React):首屏加载 3.2 秒,JS 体积 1.1MB
重构后(Web Components):首屏加载 0.8 秒,JS 体积 120KB
转化率提升约 10%。对于百万日活的平台,这就是千万级别的营收提升。
案例 2:企业内部组件库
某公司维护 React + Vue 两套组件库,版本升级经常出现不同步。统一为 Lit + Web Components 后:
- 一套组件,React 和 Vue 项目都能用
- 维护工作量 减少 60%
- 升级再也不需要"两边改"
六、生态成熟度(2026 年 6 月)
| 工具/库 | 定位 | 适合场景 |
|---|---|---|
| Lit (Google) | 4KB 轻量封装,声明式模板 | 复杂组件库,替代 React/Vue |
| Shoelace | 完整设计系统,60+ 组件 | 企业应用 UI 套件 |
| OpenWC | 开发工具链(测试/Lint/构建) | 大型项目基建 |
| Stencil (Ionic) | 编译时生成,优化性能 | 跨框架组件分发 |
| FAST (Microsoft) | Fluent UI 设计系统 | 微软生态项目 |
七、选型决策逻辑
不要一刀切。正确的决策依赖具体场景。
优先选 Web Components
| 场景 | 原因 |
|---|---|
| 官网/营销页 | 首屏速度是 KPI,不需要框架 |
| 设计系统/组件库 | 一套组件覆盖所有框架 |
| 电商内容页 | 包体积每小 100KB,转化率就涨 1-2% |
| 长期维护项目 | 不受框架 breaking changes 绑架 |
| 小团队/个人项目 | 不搭复杂脚手架,一个 HTML 就能跑 |
仍然用框架
| 场景 | 原因 |
|---|---|
| 复杂 SPA(Figma/Google Docs 类) | 重度状态管理需求 |
| 已有大团队 | 人员流动、技术积累 |
| 框架生态依赖 | React Native、Next.js SSR、VueUse |
| SSR/SSG 强需求 | Next.js/Nuxt 这些元框架仍有优势 |
Web Components 赢:官网/营销页、设计系统/组件库、电商内容页、长期维护项目、微前端子应用、跨框架团队
框架赢:复杂 SPA 应用、现有大团队项目、SSR/SSG 强需求、重度状态管理、移动端跨平台、框架特有生态
八、Web Components 在 React/Vue 中怎么用?
⚡ 最关键的卖点:Web Components 写一次,直接在任何框架中使用。但实际集成时有几个"坑"需要注意——特别是在 React 中。
在 React 中使用
React 从 19 版本开始对 Web Components 的兼容性有了显著提升,但仍有几个细节需要知道。
基础用法——直接拿来用
function ProductPage() {
return (
<div>
<product-card name="真无线耳机" price="899" image="https://picsum.photos/seed/1/400/300" />
</div>
);
}
React 不认识的 HTML 标签会被当作自定义元素渲染到 DOM 中——Web Components 恰好就是自定义元素,所以基础使用完全没问题。
属性 vs 特性——第一个"坑"
// ❌ 错误方式:React 会把 liked 转成字符串属性
<product-card liked={true} />
// 在 DOM 中变成:<product-card liked="true" /> — 字符串,不是布尔!
原因:React 的 JSX 只设置 HTML attribute(字符串),不会设置 DOM property(真正的 JavaScript 值)。
解决方案:使用 ref 来直接设置 property:
import { useRef, useEffect } from 'react';
function ProductPage() {
const cardRef = useRef(null);
useEffect(() => {
if (cardRef.current) {
cardRef.current.liked = true; // 布尔值 ✅
cardRef.current.price = 899; // 数字 ✅
}
}, []);
return <product-card ref={cardRef} name="真无线耳机" image="https://picsum.photos/seed/1/400/300" />;
}
监听自定义事件——第二个"坑"
// ❌ React 无法直接监听 Web Components 的自定义事件
<product-card onlike-change={handleChange} />
原因:React 的合成事件系统只处理标准的 DOM 事件。Web Components 通过 dispatchEvent(new CustomEvent(...)) 触发的是自定义事件。
解决方案:通过 ref 手动绑定原生 addEventListener:
function ProductPage() {
const cardRef = useRef(null);
useEffect(() => {
const card = cardRef.current;
if (!card) return;
const handleLike = (e) => console.log('收藏状态变化:', e.detail.liked);
card.addEventListener('like-change', handleLike);
return () => card.removeEventListener('like-change', handleLike);
}, []);
return <product-card ref={cardRef} />;
}
封装成通用 hook,一劳永逸:
function useWebComponent(ref, properties, events) {
useEffect(() => {
const el = ref.current;
if (!el) return;
Object.entries(properties || {}).forEach(([key, value]) => { el[key] = value; });
const handlers = [];
Object.entries(events || {}).forEach(([eventName, handler]) => {
el.addEventListener(eventName, handler);
handlers.push([eventName, handler]);
});
return () => handlers.forEach(([name, fn]) => el.removeEventListener(name, fn));
}, [properties, events]);
}
在 Vue 中使用
Vue 对 Web Components 的支持比 React 好得多——几乎所有场景都能直接使用:
<template>
<product-card :name="product.name" :price="product.price" :liked="true" @like-change="handleLike" />
</template>
<script setup>
const handleLike = (e) => console.log('Like changed:', e.detail.liked);
</script>
跨框架体验对比
| 框架 | 基础渲染 | 属性传递 | 自定义事件 | 综合体验 |
|---|---|---|---|---|
| Vue 3 | ✅ 原生支持 | ✅ : 绑定自动处理 | ✅ @ 原生支持自定义事件 | ⭐ 开箱即用 |
| React 19 | ✅ 基础渲染 | ⚠️ attribute 转字符串,需 ref | ⚠️ 需手动 addEventListener | 🔧 需封装 hook |
| Svelte | ✅ 原生支持 | ✅ 直接绑定 | ✅ 直接绑定 | ⭐ 开箱即用 |
| Angular | ✅ 原生支持 | ✅ 直接绑定 | ✅ 直接绑定 | ⭐ 开箱即用 |
最终总结——Web Components 的价值"铁三角"
这就是为什么越来越多的设计系统(Shoelace、Fluent UI、Adobe Spectrum)转向 Web Components 的根本原因——一套组件库,不再需要维护 React 版 + Vue 版 + Angular 版。
写在最后
五年前我们不得不使用框架,因为浏览器 API 太弱。现在浏览器终于足够强大了。
2026 年,当你准备新建一个项目时,先问自己两个问题:
- 我真的需要那 300KB 吗?
- 如果不需要,为什么还要引入框架?
如果决定试试
把 product-card.js 保存到你的项目里,新建一个 HTML 文件引用它。或者直接在你现有的 React 项目里用:
function App() {
return (
<div>
<product-card name="商品" price="99" image="/img.jpg" />
</div>
);
}
React 不认识的 HTML 标签会被当作普通 DOM 元素渲染——而 Web Components 恰好就是 DOM 元素。兼容性不是问题,它是标准。
