CJH_BLOG

Web Components 回来了——你的项目真的需要 React/Vue 吗?

C
cjh
·2026年06月29日·👁 1 次阅读

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/VueWeb 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 ComponentsReactVue
创建constructor()constructor()created()
挂载connectedCallback()useEffect([], [])mounted()
更新attributeChangedCallback()useEffect()watch/computed
销毁disconnectedCallback()useEffect(() => fn, [])unmounted()
属性监听observedAttributes 静态 getterprops 声明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 年,当你准备新建一个项目时,先问自己两个问题:

  1. 我真的需要那 300KB 吗?
  2. 如果不需要,为什么还要引入框架?

如果决定试试

product-card.js 保存到你的项目里,新建一个 HTML 文件引用它。或者直接在你现有的 React 项目里用:

function App() {
  return (
    <div>
      <product-card name="商品" price="99" image="/img.jpg" />
    </div>
  );
}

React 不认识的 HTML 标签会被当作普通 DOM 元素渲染——而 Web Components 恰好就是 DOM 元素。兼容性不是问题,它是标准。

登录后可为文章点赞


#评论区

登录 后参与讨论
💬

暂无评论,成为第一个留言的人吧