Web Components 如何通过 Slot 实现组件树的嵌套?

Web Components 可以简单地理解为一种自定义 HTML 元素和组件的技术,它由三个技术组成:Custom Element、Shadow DOM 和 HTML Templates。一个 Web Component 首先要通过 Custom Element 定义,并且可以使用 Shadow DOM 来封装它的样式和行为。而通过 HTML Templates,我们可以预定义组件的 HTML 结构,这样就可以将组件进行复用。

Web Components 作为一种组件化的技术,最重要的优势之一就是组件树的嵌套。组件树是由多个组件组成的层级结构,由此实现了对 Web 页面的语义化组织。这种嵌套方式是通过 Slot 实现的,本文将介绍 Web Components 如何通过 Slot 实现组件树的嵌套。

Slot 简介

Slot 是 Shadow DOM 的一个重要特性,它可以让开发者将一段 HTML 代码“插入”到 Shadow DOM 中。在 Custom Element 中,通过定义 元素可以在 Shadow DOM 中划定出一个“挂载点”(Slot),我们可以在 Custom Element 中使用这个 Slot,来将外部传入的 HTML 挂载到这个位置。

假设我们在一个 Custom Element 中定义了以下 Shadow DOM:

<template>
  <style>
    h2 {
      color: red;
    }
  </style>
  <h2><slot></slot></h2>
</template>

在这个 Shadow DOM 中,我们定义了一个 h2 元素,并将 元素作为它的子元素。此时这个 Custom Element 的模板就定义了一个挂载点,并且在 Shadow DOM 中使用了这个挂载点,这个 Custom Element 就可以使用使用这个 Slot 去渲染外部传入的信息。

<my-element>
  <div>Hello World!</div>
</my-element>

这里,我们在 Constructor 中添加初始化 Shadow DOM 模板的逻辑:

class MyElement extends HTMLElement {
  constructor() {
    super();
    const shadowRoot = this.attachShadow({ mode: 'open' });
    const template = document.createElement('template');
    template.innerHTML = `
      <style>
        h2 {
          color: red;
        }
      </style>
      <h2><slot></slot></h2>
    `;
    shadowRoot.appendChild(template.content.cloneNode(true));
  }
}
customElements.define('my-element', MyElement);

通过上述代码,我们可以发现 Cusomt Element 的内部已经可以通过 Slot 让外部传入的 HTML 输出到自己的 Shadow DOM 中了。

内置 Slot 的使用

在 Web Components 的组件树结构中,内置 Slot 还有两个重要的用处:

  1. 组件的默认“挂载点”
  2. 父子组件之间的传值

1. 组件的默认“挂载点”

在 Custom Element 中,我们可以通过内置 Slot 为组件定义默认的“挂载点”,这样当使用这个组件并且没有向它传递任何内容时,就可以输出组件默认的内容。

<my-component></my-component>
class MyComponent extends HTMLElement {
  constructor() {
    super();
    const shadowRoot = this.attachShadow({ mode: 'open' });
    const template = document.createElement('template');
    template.innerHTML = `
      <p>Default Content</p>
    `;
    shadowRoot.appendChild(template.content.cloneNode(true));
  }
}
customElements.define('my-component', MyComponent);

在这个例子中,当我们没有向 MyComponent 传递任何内容时,它就会输出默认的

Default Content

2. 父子组件之间的传值

Slot 除了用来插入外部 HTML,还可以用于组件之间的传值。这就是我们在 Web Components 中经常遇到的父组件向子组件传值的方式之一。我们可以通过将一些组件功能封装到 Custom Element 中,然后将子组件作为 Slot 的内容注入进去,达到实现父组件向子组件传值的效果。

我们可以通过定义子组件的装饰器 @slot 来实现这个过程。装饰器可以看作带有特殊行为的函数,我们可以在 @slot 中定义子组件的内容,并作为 Slot 在 Shadow DOM 中进行渲染:

<my-component>
  <my-sub-component>Sub Component Content</my-sub-component>
</my-component>
class MyComponent extends HTMLElement {
  constructor() {
    super();
    const shadowRoot = this.attachShadow({ mode: 'open' });
    const template = document.createElement('template');
    template.innerHTML = `
      <h2><slot name="title">Default Title</slot></h2>
      <p><slot name="content"></slot></p>
    `;
    shadowRoot.appendChild(template.content.cloneNode(true));
  }
}
customElements.define('my-component', MyComponent);

class MySubComponent extends HTMLElement {
  constructor() {
    super();
    const shadowRoot = this.attachShadow({ mode: 'open' });
    const template = document.createElement('template');
    template.innerHTML = `
      <style>
        p {
          color: red;
        }
      </style>
      <p><slot></slot></p>
    `;
    shadowRoot.appendChild(template.content.cloneNode(true));
  }
}
customElements.define('my-sub-component', MySubComponent);

在这个例子中,我们首先定义了一个父组件 MyComponent,并在其中定义了两个 Slot,分别用于渲染标题和内容。在子组件 MySubComponent 中,我们再次定义了一个 Slot,并向 MyComponent 传递需要放入子组件的内容。这时,在 MyComponent 中,我们通过将 MySubComponent 插入到第二个 Slot 中,实现了父子组件间的传值功能。

自定义 Slot 的使用

除了默认的 Slot,我们还可以通过定义自定义 Slot 实现更灵活的传值。

假设我们需要实现一个可以渲染任务列表的应用,任务列表由多个任务项组成,每个任务项中包含任务的标题和详细内容,我们需要通过 Web Components 将这个任务列表组合到一起。

首先,我们定义任务项组件 TaskItem,并在其中使用自定义 Slot 来渲染任务:

<task-item>
  <div slot="title">Title</div>
  <div slot="content">Content</div>
</task-item>
class TaskItem extends HTMLElement {
  constructor() {
    super();
    const shadowRoot = this.attachShadow({ mode: 'open' });
    const template = document.createElement('template');
    template.innerHTML = `
      <div class="task-item">
        <h3><slot name="title"></slot></h3>
        <p><slot name="content"></slot></p>
      </div>
    `;
    shadowRoot.appendChild(template.content.cloneNode(true));
  }
}
customElements.define('task-item', TaskItem);

在定义 TaskItem 组件时,我们定义了两个自定义的 Slot:Slot name 分为 title 和 content。这样,我们在定义 TaskItem 时就能够通过以下的方式将任意内容放入到这两个 Slot 中。

在这个例子中,我们使用两个自定义 Slot 分别渲染了一个任务项的任务标题和详细内容,接下来我们可以通过使用灵活的自定义 Slot 在多个任务项中传值,以实现整个任务列表的绘制:

<my-task-list>
  <task-item>
    <div slot="title">Task 1 Title</div>
    <div slot="content">Task 1 Content</div>
  </task-item>
  <task-item>
    <div slot="title">Task 2 Title</div>
    <div slot="content">Task 2 Content</div>
  </task-item>
  ...
</my-task-list>
class MyTaskList extends HTMLElement {
  constructor() {
    super();
    const shadowRoot = this.attachShadow({ mode: 'open' });
    const template = document.createElement('template');
    template.innerHTML = `
      <div class="task-list">
        <slot></slot>
      </div>
    `;
    shadowRoot.appendChild(template.content.cloneNode(true));
  }
}
customElements.define('my-task-list', MyTaskList);

在这个例子中,我们定义了一个包含多个 TaskItem 的 MyTaskList,并通过自定义 Slot 构造实现了多个 TaskItem 间的传值功能。在 MyTaskList 的模板中,我们仅仅使用了一个默认 Slot 来渲染这些 TaskItem,并使用 Slot 元素来将 TaskItem 插入到了 Shadow DOM 中。这样,我们就能够实现一个简单的任务列表组件。

总结

本文介绍了 Web Components 如何通过 Slot 实现组件树的嵌套,包括:

  1. Slot 的定义和基本用法
  2. 内置 Slot 的使用方法(默认内容和传值)
  3. 自定义 Slot 的使用方法(灵活传值)

Web Components 的组件化方案可以实现更为灵活的前端组件开发,并且基于标准化的 HTML、CSS 和 JavaScript 让我们能够在项目中更好地复用和组装组件。通过本文的学习,我们相信可以更好地理解 Web Components 的部分内容,进而能够更为熟练地开发基于 Web Components 的组件技术。

来源:JavaScript中文网 ,转载请注明来源 本文地址:https://www.javascriptcn.com/post/65b4d7f3add4f0e0ffdb3b1e