#React#TypeScript

Web Components:框架无关的组件标准

React/Vue 各自有组件,但换框架就白写。Web Components 是浏览器原生的组件标准——Custom Elements + Shadow DOM,跨框架复用。本文讲清 WC 的核心 API、和 React/Vue 组件的区别、什么时候值得用(组件库/微前端),什么时候别用。

本文要点

  • 主题聚焦:React、TypeScript
  • 适合人群:独立开发者、全栈工程师、AI 应用开发者
  • 核心价值:快速理解背景、实践路径、常见误区和工程建议
作者 @admin·发布于 2026年3月21日·更新于 2026年7月23日

Web Components:框架无关的组件标准

每个前端都经历过这种痛:用 React 写了套组件库,业务要迁移到 Vue,组件库全部重写。再迁移到原生?再重写一遍。

问题不在你,在框架。React 组件是 React 的,Vue 组件是 Vue 的,它们的"组件"都跑在各自的运行时里,换一个宿主就失效。

Web Components(WC)是另一条路:浏览器原生支持的组件标准。它不依赖任何框架,Chrome/Safari/Firefox/Edge 全部原生支持,写一次,React、Vue、Angular、原生 JS 都能用。

这篇文章讲清楚:WC 的三大核心 API、它和 React/Vue 组件到底差在哪、什么时候该上、什么时候别碰,以及 Google 的 Lit 怎么把 WC 的开发体验拉到接近 React。


一、Web Components 到底是什么

一句话定位:Web Components 是 W3C/WHATWG 制定的浏览器原生组件标准,由三个原生 API 组合而成。

它不是一个框架,而是一组浏览器能力:

  • customElements.define() —— 注册自定义 HTML 标签
  • Element.attachShadow() —— 创建样式和 DOM 隔离的影子树
  • <template> 标签 —— 定义惰性、可复用的 DOM 片段

这三个 API 都是浏览器内置的,不需要 import 任何库,不需要打包器。你写一个 <my-button>,它就是 HTML 的一等公民,和 <button>、<div> 同级。

这正是 WC 最锋利的卖点:永恒且跨框架。只要 HTML 标准不废除它(几十年内不会),你的组件就永远能跑。


二、三大核心技术,挨个拆

1. Custom Elements:自定义 HTML 标签

Custom Elements 让你能注册一个全新的 HTML 标签,并定义它的行为。这是 WC 的灵魂。

typescript
class MyButton extends HTMLElement {
  connectedCallback() {
    // 元素插入 DOM 时触发
    this.render();
  }

  // 监听属性变化(必须先在 observedAttributes 里声明)
  static get observedAttributes() {
    return ['disabled'];
  }

  attributeChangedCallback(name: string, _old: string, newVal: string) {
    if (name === 'disabled') {
      this.classList.toggle('is-disabled', newVal !== null);
    }
  }

  private render() {
    this.innerHTML = `<button>点我</button>`;
  }
}

// 注册自定义元素,标签名必须带连字符,防止和原生标签冲突
customElements.define('my-button', MyButton);

用起来跟原生标签一模一样:

html
<my-button disabled></my-button>

几个关键点:

  • 标签名必须带连字符(my-button、ui-card),这是标准规定,为了不和未来的原生标签冲突。
  • 生命周期:connectedCallback(插入 DOM)、disconnectedCallback(移除)、attributeChangedCallback(属性变化),对应 React 的 useEffect / useEffect cleanup / 依赖变化。
  • 子类化:可以继承原生元素(class MyButton extends HTMLButtonElement),用法是 <button is="my-button">。

2. Shadow DOM:样式和 DOM 的隔离墙

Shadow DOM 是 WC 的第二大杀器。它给元素挂一棵"影子 DOM 树",这棵树内部的样式和结构对外完全隔离——内部 CSS 不泄漏到外部,外部 CSS 也进不来。

typescript
class MyCard extends HTMLElement {
  connectedCallback() {
    // 开启 Shadow DOM,mode: 'open' 表示外部能通过 element.shadowRoot 访问
    const shadow = this.attachShadow({ mode: 'open' });

    shadow.innerHTML = `
      <style>
        /* 这里的样式只作用于 Shadow DOM 内部,绝不污染全局 */
        .card {
          padding: 16px;
          border: 1px solid #e5e7eb;
          border-radius: 8px;
          font-family: system-ui;
        }
        ::slotted(*) { /* 把外部传进来的子节点样式化 */
          color: #374151;
        }
      </style>
      <div class="card">
        <slot></slot>
      </div>
    `;
  }
}

customElements.define('my-card', MyCard);
html
<my-card>
  <p>这段文字通过 &lt;slot&gt; 投影进 Shadow DOM</p>
</my-card>

<slot> 是 Shadow DOM 的插槽机制,功能类似 Vue 的 <slot> 或 React 的 children,把外部子节点"投影"到组件内部指定位置。

Shadow DOM 的实战价值非常具体:

  • 写组件库不用纠结 BEM 命名空间,样式天然隔离,不用担心用户全局 CSS 把你的按钮改得稀烂。
  • 封装严格:外部 document.querySelector 进不来 Shadow DOM,内部节点不会泄漏到全局。

代价也是有的:Shadow DOM 内的内容搜索引擎默认看不到,做 SEO 站要慎重(下文会展开)。

3. HTML Templates:可复用的惰性 DOM 模板

<template> 标签定义一段 DOM 结构,但浏览器不会渲染它,直到你 clone 一份塞进文档。配合 <slot> 用很顺手。

html
<template id="row-template">
  <tr>
    <td class="name"></td>
    <td class="age"></td>
  </tr>
</template>

<script>
  const tpl = document.getElementById('row-template');
  const row = tpl.content.cloneNode(true); // 深拷贝模板
  row.querySelector('.name').textContent = '张三';
  row.querySelector('.age').textContent = '28';
  document.querySelector('tbody').appendChild(row);
</script>

<template> 内部是惰性的:图片不加载、脚本不执行、样式不生效。这种"声明结构但不立即渲染"的特性,在写复杂 WC 时是个很好的模板载体。

实战中,更多人直接用模板字符串(shadowRoot.innerHTML = '...'),<template> 反而用得少。但当你需要预定义静态结构、动态克隆时,它依然是最干净的方案。


三、和 React/Vue 组件的本质区别

把 WC 和 React/Vue 组件放一起对比,差异非常清晰:

维度Web ComponentsReact/Vue 组件
运行依赖浏览器原生,零依赖必须引入对应框架运行时
跨框架复用一个组件,所有框架通用换框架=重写
生命周期浏览器标准,稳定跟着框架版本走,可能 breaking change
模板/响应式原生没有,得自己搞或用 Lit声明式模板 + 响应式数据,开箱即用
生态相对小React 生态无可匹敌
SSR/SEOShadow DOM 不友好成熟方案(Next/Nuxt)

一句话总结差异:React/Vue 组件是"框架里的组件",WC 是"HTML 里的组件"。

React 组件的本质是一个返回虚拟 DOM 的函数,它必须挂在 react-dom 运行时上,离开 React 就是一坨死代码。WC 的本质是一个真实的 HTML 元素,和 <div> 同级,任何能操作 DOM 的环境(JS、React、Vue、Angular、jQuery、原生)都能直接 <my-button /> 用。

tsx
// React 项目里直接用 WC,零适配
function App() {
  return <my-button onClick={handleClick}>按钮</my-button>;
}

这种"组件存活期比框架还长"的特性,是 WC 唯一不可替代的价值。


四、WC 的优势:为什么有人选它

  1. 真正的跨框架。一套设计系统,React/Vue/Angular/原生项目通吃。这是 WC 最硬的场景,所有大厂的设计系统(Ionic、SAP UI5、Adobe Spectrum、GitHub 的部分组件)都用 WC 来实现。

  2. 不死。标准一旦被浏览器实现,就基本不会被移除。十年前写的 WC 今天还能跑;十年前用 Backbone 写的代码呢?React Class 组件如今也是"能用但不推荐"。

  3. 样式天然隔离。Shadow DOM 让你不用再纠结 CSS 命名空间、不用 Tailwind 也能写出干净的组件库。

  4. 分发简单。一个 WC 就是一个 JS 文件,<script> 引进去就能用,不需要 npm install 一堆依赖,适合嵌入式分发给第三方。


五、WC 的劣势:别当银弹

  1. API 偏底层,工程体验差。原生 WC 没有声明式模板、没有响应式数据。你写个表单组件,要手动监听属性变化、手动 this.shadowRoot.querySelector 更新 DOM——这是 jQuery 时代的开发体验。实际项目几乎都要配 Lit 这类库。

  2. SEO 是硬伤。Shadow DOM 的内容默认不被搜索引擎索引(虽然近年有改善,但仍然不可靠)。需要 SSR + 重 SEO 的内容站,WC 不是好选择。

  3. 表单集成麻烦。原生 WC 元素默认不参与 <form> 校验,要用 ElementInternals API 一通操作才能让自定义元素像 <input> 一样工作。这块心智成本不低。

  4. 生态远不如 React/Vue。React 有 20 万+ npm 包,WC 的生态小得多。你想要的轮子不一定有,有也不一定维护得好。

  5. TypeScript 支持一般。JSX 里用 WC 标签,TS 默认会报类型错误,得自己声明 JSX.IntrinsicElements。


六、什么时候值得用

实战里我会推荐在这几个场景上 WC:

  1. 跨框架组件库 / 设计系统。这是 WC 最正确的场景。公司里有多个技术栈(老项目 jQuery、中台 React、新业务 Vue),设计系统用 WC 写一次全用上,不用维护三套。

  2. 微前端。把一个复杂模块封装成 WC,嵌入到任意主应用的任意位置。框架无关、样式隔离,天然适合跨团队、跨技术栈集成。

  3. 浏览器扩展。扩展的 content script 注入到第三方页面,WC 的 Shadow DOM 能保证你的 UI 样式不被宿主页面污染,也不污染宿主。

  4. 给第三方分发的嵌入式组件(评论框、播放器、支付按钮)。一个 <script> 标签搞定,使用方不用装一堆依赖。


七、什么时候别用

反过来,这些场景上 WC 就是给自己挖坑:

  1. 单一框架的业务项目。公司就一个 React 中台,业务组件用 React 组件就好,响应式、生态、SSR 全是现成的。为了"未来可能换框架"提前上 WC,属于过度设计。

  2. 重 SEO 的内容站。博客、电商商品页、新闻站——Shadow DOM 内容搜索引擎抓不到,直接影响流量。用 Next.js/Nuxt 的 SSR 才是正解。

  3. 重度依赖表单和校验的后台系统。原生 WC 表单集成心智成本高,React/Vue 的表单库(Formik、react-hook-form、VeeValidate)省心得多。

  4. 小团队、追求快速迭代。WC 的开发体验(即使配 Lit)也比不上 React + Tailwind 的速度。


八、Lit:把 WC 从底层 API 解放出来

前面反复说原生 WC 体验差,行业解法是 Lit(Google 出品)。它是一个轻量库(约 5KB),在原生 WC 之上加了声明式模板和响应式属性,开发体验接近 React。

同样的按钮,用 Lit 写是这样:

typescript
import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';

@customElement('my-button') // 自动调用 customElements.define
export class MyButton extends LitElement {
  // 响应式属性:变化时自动触发重渲染
  @property({ type: Boolean }) disabled = false;
  @property() label = '点我';

  static styles = css`
    button {
      padding: 8px 16px;
      border-radius: 6px;
      background: #2563eb;
      color: white;
      cursor: pointer;
    }
    button.is-disabled {
      background: #9ca3af;
      cursor: not-allowed;
    }
  `;

  render() {
    return html`
      <button class=${this.disabled ? 'is-disabled' : ''} ?disabled=${this.disabled}>
        ${this.label}
      </button>
    `;
  }
}

对比原生版本,Lit 的好处一目了然:

  • 声明式模板:html 模板字符串,不用手写 innerHTML 和 querySelector。
  • 响应式属性:@property 标注的属性变化时,自动局部重渲染(类似 React 的 state)。
  • 样式作用域自动:static styles 自动注入 Shadow DOM,不用手动 attachShadow。
  • 装饰器注册:@customElement('my-button') 一行搞定注册。

Lit 依然输出标准 WC,所以它跨框架、永恒、可分发的特性一个没丢。结论:如果决定上 WC,生产项目几乎一定配 Lit,别硬写原生。


小结

  • Web Components 是浏览器原生的组件标准(Custom Elements + Shadow DOM + HTML Templates),零依赖、跨框架、不死。
  • 跨框架、永恒、样式隔离是它的三大优势;API 底层、SEO 难、生态小是它的硬伤。
  • 该用的场景:跨框架组件库/设计系统、微前端、浏览器扩展、给第三方分发的嵌入式组件。
  • 别用的场景:单一框架的业务项目、重 SEO 内容站、重表单的后台系统、小团队快速迭代。
  • 生产环境配 Lit,声明式 + 响应式,把原生 WC 的开发体验拉到接近 React。

金句:框架会过时,标准不会。Web Components 不是用来取代 React 的,它是用来写"活得比框架还久"的组件的。

Related

相关推荐