浅谈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 及旧版浏览器。

相关推荐
白白白鲤鱼2 分钟前
Vue2项目—基于路由守卫实现钉钉小程序动态更新标题
服务器·前端·spring boot·后端·职场和发展·小程序·钉钉
xianxin_16 分钟前
HTML5 地理定位
前端
Running_C24 分钟前
Vue组件化开发:从基础到实践的全面解析
前端·vue.js·面试
Clain24 分钟前
如何搭建一台属于自己的服务器并部署网站,超详细小白教程
linux·运维·前端
胡清波26 分钟前
小程序中使用字体图标的最佳实践
前端
xianxin_28 分钟前
HTML5 客户端存储
前端
南玖i28 分钟前
使用vue缓存机制 缓存整个项目的时候 静态的一些操作也变的很卡,解决办法~超快超简单~
前端·javascript·vue.js
计算机毕设定制辅导-无忧学长1 小时前
InfluxDB 集群部署与高可用方案(二)
java·linux·前端
袁煦丞1 小时前
MongoDB数据存储界的瑞士军刀:cpolar内网穿透实验室第513号成功挑战
前端·程序员·远程工作
天才熊猫君2 小时前
npm 和 pnpm 的一些理解
前端