Web Components

介绍

Web Components 是一套由 W3C(万维网联盟)制定的 Web 标准,旨在提供一种原生、标准化的方式来创建可重用、封装良好的自定义 HTML 元素。它不是某个框架或库,而是浏览器原生支持的一组 API 的集合。

一、历史背景

1. 起源(2011 年前后)

2011 年:Google 工程师 Dimitri Glazkov 首次在 Chromium 博客中提出 Web Components 的概念。
最初是作为解决前端组件化问题的一种“原生”方案,希望摆脱对 jQuery 插件、Angular、React 等框架的依赖。

同年,W3C 成立了 Web Components 社区组,开始标准化工作。

2. 标准演进

Web Components 最初包含四个主要技术规范:

  • Custom Elements(自定义元素)
  • Shadow DOM(影子 DOM)
  • HTML Templates(HTML 模板)
  • HTML Imports(HTML 导入) ← 已被废弃
    ⚠️ 注意:HTML Imports 因兼容性和性能问题未被广泛采纳,后来被 ES Modules 取代。

3. 浏览器支持时间线

  • 2014–2016 年:各浏览器逐步实现基础功能(Chrome 最先支持)。
  • 2018 年左右:主流浏览器(Chrome、Safari、Firefox、Edge)基本完成对核心规范(Custom Elements v1 + Shadow DOM v1)的支持。
  • 2020 年后:Web Components 进入稳定成熟阶段,被广泛用于微前端、设计系统、跨框架组件等场景。

二、为什么出现?要解决什么问题?

在 Web Components 出现之前,前端开发面临几个核心痛点:

1. 缺乏原生组件化能力

HTML 只有有限的内置标签(如 <div>、<button>),无法表达业务语义。
开发者需要通过 JavaScript + CSS 手动组合结构、样式和行为,难以复用。

2. 样式和脚本全局污染

CSS 选择器是全局的,容易发生命名冲突(比如两个组件都用了 .button)。
JavaScript 变量和逻辑也容易互相干扰。

3. 框架锁定(Framework Lock-in)

React、Vue、Angular 等框架都有自己的组件模型,但彼此不兼容。
如果想在不同项目或团队间共享 UI 组件,往往需要为每个框架单独实现一遍。

4. 封装性差

传统方式无法将 HTML 结构、CSS 样式、JS 行为“打包”成一个黑盒单元。

三、Web Components 如何解决这些问题?

问题 Web Components的解决方案
无法创建语义化标签 Custom Elements:允许定义 <my-button> 这样的新 HTML 标签
样式/脚本污染 Shadow DOM:提供 DOM 和样式的封装,内部与外部隔离
缺乏模板机制 <template> 和 <slot>:声明可复用的 HTML 片段,支持内容分发
跨框架复用困难 原生浏览器支持,可在 React、Vue、Angular、纯 HTML 中无缝使用

四、典型应用场景

  • Web 框架:如 Salesforce 的 Lightning Web Components、GitHub 的 Primer ViewComponents。
  • 微前端架构:不同团队开发的组件可以独立部署、互不干扰。
  • 跨框架 UI 库:一次编写,到处使用(例如:Vaadin、Shoelace 等基于 Web Components 的 UI 库)。
  • 嵌入第三方小部件:如聊天窗口、支付按钮、地图控件等,避免污染宿主页面。

Web Components 三大核心 API

我们以计数器这个组件为例子进行说明:

计数器

1. Custom Elements(自定义元素)

<my-counter></my-counter>

<script>
// 定义一个自定义元素类
class MyCounter extends HTMLElement {
  constructor() {
    super(); // 必须调用 super()

    // 创建 Shadow DOM
    const shadow = this.attachShadow({ mode: 'open' });

    // 初始化状态
    this.count = 0;

    // 构建内部结构
    shadow.innerHTML = `
      <style>
        button { font-size: 16px; padding: 4px 8px; }
        span { margin: 0 8px; }
      </style>
      <button id="dec">-</button>
      <span id="value">${this.count}</span>
      <button id="inc">+</button>
    `;

    // 绑定事件
    shadow.getElementById('inc').addEventListener('click', () => {
      this.count++;
      this.update();
    });

    shadow.getElementById('dec').addEventListener('click', () => {
      this.count--;
      this.update();
    });
  }

  update() {
    this.shadowRoot.getElementById('value').textContent = this.count;
  }
}

// 注册自定义元素
customElements.define('my-counter', MyCounter);
</script>

✅ 注意:

  • 标签名必须包含连字符(-),如 x-button,这是为了与未来 HTML 原生标签区分。
  • 必须继承 HTMLElement(或其子类如 HTMLButtonElement)。
  • 使用 customElements.define() 注册自定义元素,第一个参数为标签名,第二个参数为类名。

生命周期回调

生命周期方法 触发时机 类似于 Vue / React 的什么?
constructor() 元素被创建时(通过 new 或解析 HTML) setup() / constructor
connectedCallback() 元素被插入到 DOM 树中时 mounted() / componentDidMount
disconnectedCallback() 元素从 DOM 树中移除时 unmounted() / componentWillUnmount
attributeChangedCallback(name, oldValue, newValue) 被监听的属性发生变化时(需配合 observedAttributes) watch(props) / useEffect(依赖属性变化)
adoptedCallback() 元素被移动到另一个 document(如 document.adoptNode()) 极少使用,无直接对应
// 监听 dom 属性变化
static get observedAttributes() {
  return ['value'];
}

// 当 value 属性变化时调用
attributeChangedCallback(name, oldValue, newValue) {
  if (name === 'value') {
    this.count = parseInt(newValue, 10);
    this.update();
  }
}
<my-counter value="10"></my-counter>

这里我们了解了外部通过标签属性通知组件内部的方式,接下我们介绍如何从组件内部通知外部的方法。

// 组件内部:派发事件
this.dispatchEvent(new CustomEvent('count-change', {
  detail: { count: this.count },
  bubbles: true, // 事件冒泡
  composed: true // 事件可以穿越 Shadow DOM 边界
}));
// 外部监听组件事件
document.querySelector('my-counter').addEventListener('count-change', (event) => {
    console.log('收到组件事件:', event.detail); 
  });

// 收到组件事件: {count: 11}  

2. Shadow DOM(影子 DOM)

提供封装的 DOM 子树,将组件的内部结构、样式与外部页面完全隔离。

关键特性:

  • 样式隔离:Shadow DOM 内部的 CSS 不会影响外部,外部 CSS 也无法穿透(除非使用 ::part 或 CSS 变量)。
  • DOM 封装:document.querySelector() 无法直接访问 Shadow DOM 内部节点。
  • 插槽机制(Slots):支持内容分发(类似 Vue 的 slot / React 的 children)。
<my-article>
  <span slot="title">我的文章</span>
  <p>这是文章正文。</p>
</my-article>

<script>
class MyArticle extends HTMLElement {
    constructor() {
      super(); // 必须调用 super()

      // 创建 Shadow DOM
      const shadow = this.attachShadow({ mode: 'open' });

      // 初始化状态
      this.title = '';

      // 构建内部结构
      shadow.innerHTML = `
        <style>
          ::slotted(span) { font-size: 24px; font-weight: bold; }
          ::slotted(p) { color: blue; }
        </style>
        <header><slot name="title">默认标题</slot></header>
        <main><slot>默认内容</slot></main>
      `;
    }
  }

// 注册自定义元素
customElements.define('my-article', MyArticle);
</script>
  • open:可通过 element.shadowRoot 访问内部。
  • closed:外部无法访问(但实际很少用,调试困难)。
  • ::slotted(p):用于给分配到 slot 中的 <p> 元素设置样式
::slotted 规则 说明
只能选直接子元素 ::slotted(.content > p) 无效;必须是 ::slotted(p)
不能用复杂组合选择器 ::slotted(div p)、::slotted(p + ul) 都无效
不能继承 Shadow DOM 内部样式 slot 内容仍保留其原有样式,除非被 ::slotted 覆盖

3. HTML Templates(<template> 和 <slot>)

虽然 <template> 本身不是 Web Components 独有,但它是构建组件的重要工具。

  • 内容在页面加载时不会渲染。
  • 内容是惰性的(inert),脚本不会执行,图片不会加载。
  • 可通过 JavaScript 克隆并插入 DOM。
<template id="my-card-template">
  <style> .card { border: 1px solid #ccc; padding: 10px; } </style>
  <div class="card">
    <h3><slot name="name"></slot></h3>
    <p><slot></slot></p>
  </div>
</template>

<script>
class MyCard extends HTMLElement {
  constructor() {
    super();
    const template = document.getElementById('my-card-template');
    const content = template.content.cloneNode(true);
    this.attachShadow({ mode: 'open' }).appendChild(content);
  }
}
customElements.define('my-card', MyCard);
</script>

工程化

虽然原生 Web Components 功能强大,但直接手写存在一些痛点:

  • 模板字符串难以维护(尤其是复杂组件)
  • 缺乏响应式更新(需手动操作 DOM)
  • 浏览器兼容性虽好,但开发体验不如框架

因此,社区出现了许多基于 Web Components 的增强库:

特点
Lit(Google) 轻量、响应式、支持 TypeScript,最流行的 Web Components 库
Stencil(Ionic 团队) 编译时优化,可输出标准 Web Components,支持虚拟 DOM
FAST(Microsoft) 高性能,支持设计系统构建

例如,用 Lit 写上面的计数器只需几行:

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

@customElement('my-counter')
export class MyCounter extends LitElement {
  static styles = css`button { font-size: 16px; }`;

  @state() count = 0;

  render() {
    return html`
      <button @click=${() => this.count--}>-</button>
      <span>${this.count}</span>
      <button @click=${() => this.count++}>+</button>
    `;
  }
}

它还提供了,响应式更新、事件处理、插槽机制等功能,基本能实现与 vue 等框架相同的开发体验,这里因为篇幅的原因就不展开了。

补充

在即将到来的 Vue 3.6 中告别虚拟 DOM,采取类似于 Web Components 的实现逻辑,编译时直接生成真实 DOM 操作指令,跳过 VNode 创建与 Diff 过程。

所以在本人看来,一切的框架最终还是会回到最基本的操作 DOM 元素,只是在这个过程中加入了一些语法糖、工具函数等,使开发体验更加友好。

国内对于 Web Components 生态十分稀少,这种天然具有跨平台、微前端、高效的特性,使得它成为了一种非常有潜力的技术。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容