浅谈Web Components

一. 定义

什么是Web Components?

Web Components 是一组用于创建可重用、封装的自定义 HTML 元素的标准技术,由 W3C 制定并维护,是现代 Web 开发中构建组件化 UI 的原生方式。它允许开发者创建类似 <my-button> 这样的自定义标签,这些标签拥有自己的样式和行为,而且可以在任何支持 Web Components 的项目中复用,无需依赖任何框架(如 React、Vue 等)

Web Components 包含三个主要技术:

mindmap Web Components Custom Elements(自定义元素) Shadow DOM(影子 DOM) HTML Templates(HTML 模板)

二.详细解释组成

1. Custom Elements(自定义元素)

允许开发者定义自己的 HTML 元素,比如 <my-card>,并控制它的创建、属性、事件等生命周期。

关键点:

  • 通过继承 HTMLElement 类来定义新元素。
  • 使用 customElements.define('my-element', MyElementClass) 注册自定义元素。
  1. 第一个参数为所创建元素的名称 (注:为了和原生的元素区分开,元素的名称不能是单个单词,且其中必须要有短横线,eg: user-card);
  2. 第二个参数为定义元素行为的类;
  3. 第三个参数为可选参数,是一个包含extends属性的配置对象,它指定所创建的元素继承自哪个内置元素,可以继承任何内置元素
  • 支持生命周期回调:
  1. constructor() → 创建元素实例时调用
  2. connectedCallback() → 元素被插入到 DOM 中时调用 (如果元素的某些属性后来被修改,比如通过 JS 设置 element.setAttribute('disabled', 'true')
  3. attributeChangedCallback() → 属性变化时调用(如果元素从 DOM 中被移除,比如 element.remove()
  4. disconnectedCallback() → 元素被移除时调用(如果元素被移动到另一个文档,比如跨 iframe)
  5. adoptedCallback() → 很少用到

简单例子:

javascript 复制代码
class FancyDrawer extends AppDrawer {
  constructor() {
    super(); // always call super() first in the constructor. This also calls the extended class' constructor.
    ...
  }

  toggleDrawer() {
    // Possibly different toggle implementation?
    // Use ES2015 if you need to call the parent method.
    // super.toggleDrawer()
  }

  anotherMethod() {
    ...
  }
}

customElements.define('fancy-app-drawer', FancyDrawer);

在 HTML 中使用:

html 复制代码
<fancy-app-drawer></fancy-app-drawer>

2. Shadow DOM(影子 DOM)

它提供了一种将组件的内部结构、样式和行为进行封装 的机制,使得组件的内部 DOM 树与外部完全隔离,不会与页面的其他部分发生样式或 DOM 结构上的冲突

关键点:

  • 使用 this.attachShadow({ mode: 'open' }) 创建 Shadow DOM。
  • Shadow DOM 内部的样式不会泄漏到外部,外部的样式也不会影响到内部(除非特意设计)。

参数 mode 有两种选项:

模式 说明
'open' Shadow DOM 可以通过 element.shadowRoot 从外部访问(推荐用于自定义元素)
'closed' Shadow DOM 无法从外部访问(element.shadowRoot 返回 null,更封闭)

例子:

javascript 复制代码
class ShadowElement extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = `
      <style>
        p { color: red; }
      </style>
      <p>这段文字是红色的,且样式不会影响外部!</p>
    `;
  }
}

customElements.define('shadow-element', ShadowElement);

3. HTML Templates(HTML 模板)

通过 <template><slot> 标签,可以定义可复用的 HTML 片段,不会在页面加载时渲染,但可以在运行时插入到 Shadow DOM 中。

关键点:

  • <template> 标签用于声明不会立即渲染的 HTML。
  • <slot> 允许外部内容"注入"到组件中,类似于插槽(Vue 中的 slot / React 中的 children)。

例子:

html 复制代码
<template id="user-card">
  <style>
    .card { border: 1px solid #ccc; padding: 16px; }
  </style>
  <div class="card">
    <h2><slot name="name">默认名称</slot></h2>
    <p><slot name="email">默认邮箱</slot></p>
  </div>
</template>

<script>
  class UserCard extends HTMLElement {
    constructor() {
      super();
      const template = document.getElementById('user-card');
      const content = template.content.cloneNode(true);
      this.attachShadow({ mode: 'open' }).appendChild(content);
    }
  }

  customElements.define('user-card', UserCard);
</script>

<!-- 使用自定义组件 -->
<user-card>
  <span slot="name">张三</span>
  <span slot="email">zhangsan@example.com</span>
</user-card>

三.一些进阶技巧

1.样式(CSS)的封装与控制

默认基本特性:

  • Shadow DOM 内部的 CSS 样式默认不会泄漏到外部
  • 外部的 CSS 样式默认也不会影响到 Shadow DOM 内部
  • 这种隔离是 自动的、原生的,是 Shadow DOM 最重要的优势之一。

什么是 :host

  • 在 Shadow DOM 的 <style> 中,:host 表示 当前自定义元素本身(即宿主元素)
  • 它用于设置 该 Web Component 在页面中的外观,比如宽高、边距、布局等。

✅ 示例:

javascript 复制代码
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
  <style>
    :host {
      display: block;
      padding: 20px;
      border: 2px solid #999;
      border-radius: 10px;
      background: #f9f9f9;
    }

    :host([disabled]) {
      opacity: 0.6;
      pointer-events: none;
    }

    p {
      color: green;
    }
  </style>
  <p>我是组件内容</p>
`;

使用方式:

html 复制代码
<my-element></my-element>           <!-- 正常显示 -->
<my-element disabled></my-element>  <!-- 应用了 :host([disabled]) 样式 -->

🔍 说明:

  • :host 控制的是 <my-element> 这个宿主元素自身的样式;
  • :host([disabled]) 是属性选择器,当你的组件使用了 disabled 属性时,可以改变其外观;

2:使用 CSS 变量实现"样式定制"

这是非常常用的一种模式,可以让使用者从 外部传入样式变量,从而定制组件内部的样式,实现一定程度的样式开放。

✅ 示例:通过 CSS 变量定制颜色

javascript 复制代码
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
  <style>
    :host {
      display: block;
      padding: 20px;
      border: 1px solid var(--border-color, #ccc); /* 默认灰色,可被外部覆盖 */
      border-radius: 8px;
      background: var(--bg-color, #fff); /* 默认白色 */
    }

    p {
      color: var(--text-color, #333);
    }
  </style>
  <p>我支持外部通过 CSS 变量定制样式</p>
`;

外部使用:

html 复制代码
<my-styled-element style="
  --border-color: red;
  --bg-color: #f0f8ff;
  --text-color: darkblue;
"></my-styled-element>

🔧 好处:

  • 组件内部样式仍然被封装保护;
  • 但允许外部通过 CSS 自定义属性(变量) 来调整部分样式;
  • 非常灵活,是构建可定制组件库的推荐方式 ✅。

3:使用 ::slotted() 为插槽内容设置样式

如果你在 Shadow DOM 中使用了 <slot>(内容分发),那么默认情况下:

外部传入到插槽的内容,它的样式不受 Shadow DOM 内部样式的影响,反之亦然。

但你可以使用 ::slotted() 伪元素选择器,来为插槽中的内容设置样式!

✅ 示例:

javascript 复制代码
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
  <style>
    ::slotted(p) {
      color: purple;
      font-weight: bold;
    }

    ::slotted(h3) {
      color: orange;
    }
  </style>
  <div>
    <slot></slot>  <!-- 外部传入的内容会在这里显示 -->
  </div>
`;

使用方式:

html 复制代码
<my-slotted-component>
  <p>这个段落会被 ::slotted(p) 样式影响,变成紫色粗体</p>
  <h3>这个标题会被 ::slotted(h3) 影响,变成橙色</h3>
</my-slotted-component>

🔍 说明:

  • ::slotted(p) 只作用于 插入到插槽中的 <p> 元素
  • 但注意:你不能深度控制插槽内容的内部结构 ,比如 ::slotted(div p) 是不支持的;
  • 它只是给插槽的直接子元素设置样式 🎯。

4:使用 ::part 实现"受控的样式穿透"

这是 Shadow DOM 提供的一种官方机制,允许外部页面有选择地、可控地为 Shadow DOM 内部的某些部分设置样式,而不用完全打破封装。

基本用法:

  1. 在 Shadow DOM 内部,给某个元素添加 part="xxx" 属性;
  2. 在外部,使用 ::part(xxx) 选择器来设置该元素的样式;

✅ 示例:

组件内部(Shadow DOM):
javascript 复制代码
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
  <style>
    .inner-box {
      padding: 10px;
      border: 1px solid #aaa;
      border-radius: 4px;
    }
  </style>
  <div class="inner-box" part="content-box">
    我是 Shadow DOM 内部的一个盒子,但允许外部通过 part 设置样式
  </div>
`;
外部 HTML:
html 复制代码
<my-part-component style="
  ::part(content-box) {
    background: yellow;
    font-weight: bold;
    border-color: red;
  }
"></my-part-component>

🔓 说明:

  • 只有你在 Shadow DOM 中显式地给元素添加了 part="xxx",外部才能通过 ::part(xxx) 去设置样式;
  • 这是一种 安全、可控的样式开放方式,比完全开放 Shadow DOM 更优雅 👍。

2.插槽

插槽(<slot>)是 Shadow DOM 中的一个特殊 HTML 元素,它定义了一个"占位符",允许外部使用者向该组件的内部插入任意内容

在组件的 Shadow DOM 模板中,使用 <slot></slot> 来定义一个内容插槽。

示例:最基础的插槽

html 复制代码
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>插槽 Slot 基础示例</title>
</head>
<body>

  <!-- 自定义组件使用 -->
  <my-card>
    <h2>这是标题</h2>
    <p>这是插槽传入的内容,会显示在组件的 <slot> 位置</p>
  </my-card>

  <script>
    class MyCard extends HTMLElement {
      constructor() {
        super();
        const shadow = this.attachShadow({ mode: 'open' });
        shadow.innerHTML = `
          <style>
            .card {
              border: 1px solid #ccc;
              padding: 20px;
              border-radius: 8px;
              font-family: Arial, sans-serif;
            }
          </style>
          <div class="card">
            <slot></slot>  <!-- 这里是内容会被插入的位置 -->
          </div>
        `;
      }
    }

    customElements.define('my-card', MyCard);
  </script>

</body>
</html>

插槽相关用法:

类型 语法 说明 是否推荐
默认插槽 <slot></slot> 接收所有未指定插槽的内容 ✅ 推荐
具名插槽 <slot name="xxx"></slot> 接收 slot="xxx" 的内容 ✅ 推荐(复杂组件常用)
传入内容 <div slot="name">内容</div> 或直接子节点 将内容插入插槽中
默认内容 写在 <slot>默认文案</slot> 插槽无内容时显示 ✅ 推荐
样式控制 ::slotted(选择器) 为插槽内容设置样式 ✅ 推荐
作用 实现内容分发,让组件更灵活、可配置 --- ✅ 核心功能

✅ 结果:

  • 你写在 <my-card> 标签内部的 HTML(如 <h2><p> ,会被自动"传送"到组件 Shadow DOM 中的 <slot> 位置并渲染出来;
  • 组件的内部样式通过 Shadow DOM 完全封装,不会影响外部;
  • 外部可以传入任意结构的内容,非常灵活 ✅。

3.通信

🧩 父 → 子 通信:通过属性(Attributes)或属性监听

这是最常见、最简单的通信方式,尤其适用于 父组件控制子组件行为或展示

✅ 实现方式:
  • 父组件通过 设置子组件的属性(attribute) 来传递数据或控制状态;
  • 子组件通过 attributeChangedCallback 监听这些属性的变化并做出响应;
✅ 示例:父组件控制子组件的 disabled 状态
xml 复制代码
html
<!-- 子组件:my-button -->
<script>
  class MyButton extends HTMLElement {
    static get observedAttributes() {
      return ['disabled'];
    }

    attributeChangedCallback(name, oldValue, newValue) {
      if (name === 'disabled') {
        this.style.opacity = newValue !== null ? '0.5' : '1';
        this.style.pointerEvents = newValue !== null ? 'none' : 'auto';
      }
    }
  }
  customElements.define('my-button', MyButton);
</script>

<!-- 父组件 HTML 中使用 -->
<my-button disabled>点击我(被禁用)</my-button>
<my-button>点击我(可用)</my-button>

🔧 说明:

  • 父组件通过设置 disabled 属性,控制子组件是否禁用;
  • 子组件通过 observedAttributes + attributeChangedCallback 监听变化;

适用场景: 父组件需要控制子组件 UI 状态、配置等;


🧩 子 → 父 通信:通过事件(Custom Events)

子组件可以通过派发 自定义事件(CustomEvent) 来通知父组件发生了什么,比如点击、数据变更等。

✅ 实现方式:
  • 子组件使用 this.dispatchEvent(new CustomEvent(...)) 派发事件;
  • 父组件通过 addEventListener 监听这些事件;
✅ 示例:子组件按钮点击后通知父组件
xml 复制代码
html
<script>
  class MyNotifyButton extends HTMLElement {
    connectedCallback() {
      this.innerHTML = `<button>点击通知父组件</button>`;
      this.querySelector('button').addEventListener('click', () => {
        this.dispatchEvent(new CustomEvent('button-clicked', {
          detail: { message: '子组件被点击了!' },
          bubbles: true  // 允许事件冒泡,方便父组件监听
        }));
      });
    }
  }
  customElements.define('my-notify-button', MyNotifyButton);
</script>

<!-- 父组件监听子组件事件 -->
<my-notify-button></my-notify-button>

<script>
  document.querySelector('my-notify-button').addEventListener('button-clicked', (e) => {
    alert(e.detail.message); // 接收到子组件消息
  });
</script>

🔧 说明:

  • 子组件通过 CustomEvent 派发事件,并可通过 detail 传递数据;
  • 父组件通过监听该事件来响应子组件行为;
  • bubbles: true 是推荐做法,这样即使组件嵌套也能监听;

适用场景: 子组件需要通知父组件用户交互、状态变更等;


🧩 兄弟组件 / 远房组件通信:通过共同的父组件 或 全局事件 / 状态管理

如果两个组件没有直接的父子关系(比如兄弟组件、跨层级组件),常用的方式有:

方式 1:通过共同的父组件中转
  • 子组件 A 向父组件派发事件;
  • 父组件接收到后,调用子组件 B 的方法或修改其属性;
方式 2:使用全局事件(Event Bus 模式)
  • window 上派发和监听自定义事件,实现任意组件间通信;
  • 简单但松散耦合,适合中小型应用;
方式 3:使用状态管理库(如 Redux、MobX)或自定义全局状态
  • 适用于大型应用,后面我们会提到更结构化的状态管理方案;

4.状态管理

方案 1:状态提升(Lifting State Up)------ 通过共同的父组件管理状态

方案 2:全局状态对象(Global State / Singleton)

  • 创建一个 全局的 JavaScript 对象 / 模块,保存共享的状态;
  • 所有组件通过导入或访问该对象来读取或监听状态变化

✅ 示例:使用一个全局状态对象

javascript 复制代码
// global-state.js
export const state = {
  count: 0,
  setCount(newCount) {
    this.count = newCount;
    // 可以在这里派发事件通知所有组件状态更新
    window.dispatchEvent(new CustomEvent('state-changed', { detail: { count: newCount } }));
  }
};
html 复制代码
<script type="module">
  import { state } from './global-state.js';

  class CounterDisplay extends HTMLElement {
    connectedCallback() {
      this.render();
      window.addEventListener('state-changed', this.render.bind(this));
    }

    render() {
      this.innerHTML = `<p>当前计数: ${state.count}</p>`;
    }
  }
  customElements.define('counter-display', CounterDisplay);
</script>

<script type="module">
  import { state } from './global-state.js';

  class CounterButton extends HTMLElement {
    connectedCallback() {
      this.innerHTML = `<button>增加</button>`;
      this.querySelector('button').addEventListener('click', () => {
        state.setCount(state.count + 1);
      });
    }
  }
  customElements.define('counter-button', CounterButton);
</script>

<counter-display></counter-display>
<counter-button></counter-button>

四.总结

Web Component 是一套由 W3C 标准化的原生 Web 技术,用于创建可复用、封装良好的自定义 HTML 元素,核心优势在于跨框架兼容性和样式隔离。以下是其关键信息:

  • 核心技术

    • 自定义元素 :通过继承 HTMLElement 定义新标签,如 <my-button>,并注册到 customElements
    • Shadow DOM:封装组件内部结构与样式,避免全局污染,支持样式隔离。
    • HTML 模板 :通过 <template><slot> 定义可复用的 HTML 片段,动态插入内容。
  • 核心优势

    • 高复用性:组件可在不同项目、框架中直接使用,减少代码冗余。
    • 强封装性:样式和逻辑独立,避免组件间相互影响。
    • 跨框架支持:可在 React、Vue、Angular 等框架中无缝集成。
    • 原生兼容性:现代浏览器(Chrome、Firefox 等)原生支持,旧浏览器需通过 Polyfill 兼容。
  • 应用场景

    • 组件库开发:构建企业级 UI 组件库(如按钮、表单、导航栏)。
    • 微前端架构:拆分大型应用为独立微应用,通过组件化集成。
    • 复杂 UI 构建:实现拖放界面、交互式图表等可维护性高的组件。
  • 兼容性处理

    通过引入 @webcomponents/webcomponentsjs 等 Polyfill 库,支持 IE 及旧版浏览器。

相关推荐
前端大卫23 分钟前
Vue3 + Element-Plus 自定义虚拟表格滚动实现方案【附源码】
前端
却尘39 分钟前
Next.js 请求最佳实践 - vercel 2026一月发布指南
前端·react.js·next.js
ccnocare40 分钟前
浅浅看一下设计模式
前端
Lee川43 分钟前
🎬 从标签到屏幕:揭秘现代网页构建与适配之道
前端·面试
Ticnix1 小时前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人1 小时前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl1 小时前
OpenClaw 深度技术解析
前端
崔庆才丨静觅1 小时前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人1 小时前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼2 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端