模板、槽和阴影

网络组件的优势在于可重用性:您只需创建一次界面 widget,便可多次重复使用。在您 需要使用 JavaScript 来创建网络组件,则不需要 JavaScript 库。HTML 和关联的 API 可满足您所需的一切。

网络组件标准由三部分组成:HTML 模板自定义元素阴影 DOM。 通过结合使用,它们可以构建自定义、独立(封装)、可重复使用的元素,这些元素可以无缝集成 就像我们已介绍过的所有其他 HTML 元素一样。

在本部分中,我们将创建 <star-rating> 元素,这是一个 Web 组件,可让用户对网站上的体验进行评分 评分范围为 1 星到 5 星为自定义元素命名时,建议使用全小写字母。此外,还要添加短划线 因为这有助于区分常规元素和自定义元素。

我们将讨论如何使用 <template><slot> 元素、slot 属性以及 JavaScript 来创建包含 封装的 Shadow DOM。然后,我们将重复使用已定义的元素,自定义一部分文本, 就像创建任何元素或网络组件一样。我们还将简要讨论在自定义元素内部和外部使用 CSS。

<template> 元素

<template> 元素用于声明使用 JavaScript 克隆和插入 DOM 的 HTML 片段。默认情况下,系统不会呈现元素的内容。而是使用 JavaScript 对其进行实例化。

<template id="star-rating-template">
  <form>
    <fieldset>
      <legend>Rate your experience:</legend>
      <rating>
        <input type="radio" name="rating" value="1" aria-label="1 star" required />
        <input type="radio" name="rating" value="2" aria-label="2 stars" />
        <input type="radio" name="rating" value="3" aria-label="3 stars" />
        <input type="radio" name="rating" value="4" aria-label="4 stars" />
        <input type="radio" name="rating" value="5" aria-label="5 stars" />
      </rating>
    </fieldset>
    <button type="reset">Reset</button>
    <button type="submit">Submit</button>
  </form>
</template>

由于 <template> 元素的内容不会写入屏幕,因此不会呈现 <form> 及其内容。 是的,此 Codepen 为空,但如果您检查 HTML 标签页,则会看到 <template> 标记。

在此示例中,<form> 不是 DOM 中 <template> 的子项。相反,<template> 元素的内容是子元素, (由 HTMLTemplateElement.content 返回的 DocumentFragment) 属性。要使其可见,必须使用 JavaScript 抓取内容并将这些内容附加到 DOM。

这段简短的 JavaScript 并未创建自定义元素。相反,此示例将 <template> 的内容附加到了 <body> 中。 内容已成为可见、可设置样式的 DOM 的一部分。

DOM 中显示的上一个 Codepen 的屏幕截图。

通过 JavaScript 实现仅针对 1 星评分的模板不是很有用,但为 可自定义星级微件非常有用。

<slot> 元素

我们添加一个槽,以包含自定义的每个发生实例图例。HTML 提供了一个 <slot> 元素作为占位符在 <template> 中,如果提供了名称,则会创建一个“已命名的槽位”。命名的槽位可以 来自定义网络组件中的内容。<slot> 元素为我们控制自定义子元素的位置 元素应插入其影子树中。

在我们的模板中,我们将 <legend> 更改为 <slot>

<template id="star-rating-template">
  <form>
    <fieldset>
      <slot name="star-rating-legend">
        <legend>Rate your experience:</legend>
      </slot>

如果其他元素具有 slot 属性值与name 指定广告位的名称如果自定义元素没有与槽位匹配的元素,系统会呈现 <slot> 的内容。 因此,我们添加了一个包含通用内容的 <legend>,如果任何人在其 HTML 中直接添加了不含任何内容的 <star-rating></star-rating>,就可以呈现这些内容。

<star-rating>
  <legend slot="star-rating-legend">Blendan Smooth</legend>
</star-rating>
<star-rating>
  <legend slot="star-rating-legend">Hoover Sukhdeep</legend>
</star-rating>
<star-rating>
  <legend slot="star-rating-legend">Toasty McToastface</legend>
  <p>Is this text visible?</p>
</star-rating>

slot 属性是一个全局属性,用于 替换 <template><slot> 的内容。在我们的自定义元素中,具有 slot 属性的元素 是 <legend>。其实不必如此。在我们的模板中,<slot name="star-rating-legend"> 将替换为 <anyElement slot="star-rating-legend">, 其中 <anyElement> 可以是任意元素,甚至可以是另一个自定义元素。

未定义的元素

<template> 中,我们使用了 <rating> 元素。这不是自定义元素。相反,它是未知元素。浏览器 在无法识别某个元素时不会失败。浏览器将无法识别的 HTML 元素视为匿名内嵌元素 可使用 CSS 设置样式的元素。与 <span> 类似,<rating><star-rating> 元素未应用用户代理 样式或语义。

请注意,系统不会呈现 <template> 和内容。<template> 是一个已知元素,其中包含 不会呈现。<star-rating> 元素尚未定义。在我们定义元素之前,浏览器会显示该元素 所有无法识别的元素。目前,无法识别的 <star-rating> 被视为匿名内嵌元素,因此其内容 (包括图例)以及第三个 <star-rating> 中的 <p>,其显示方式与在 <span> 中的显示效果一致。

定义我们的元素,将这个无法识别的元素转换为自定义元素。

自定义元素

定义自定义元素需要使用 JavaScript。定义后,<star-rating> 元素的内容将替换为 影子根,其中包含我们与该模板相关联的模板的所有内容。模板中的 <slot> 元素已被替换 包含 <star-rating> 中其 slot 属性值与 <slot> 的名称值匹配的元素的内容,如果 有一个如果没有,则会显示模板的槽内容。

与槽位无关的自定义元素中的内容(即第三个 <star-rating> 中的 <p>Is this text visible?</p>)不会包含在 影子根,因此不会显示。

我们定义自定义元素,并将其命名为 star-rating。 通过扩展 HTMLElement

customElements.define('star-rating',
  class extends HTMLElement {
    constructor() {
      super(); // Always call super first in constructor
      const starRating = document.getElementById('star-rating-template').content;
      const shadowRoot = this.attachShadow({
        mode: 'open'
      });
      shadowRoot.appendChild(starRating.cloneNode(true));
    }
  });

现在,该元素已定义,每当浏览器遇到 <star-rating> 元素时,它都会按照定义的方式进行渲染 包含 #star-rating-template(即我们的模板)的元素。浏览器会将一个 shadow DOM 树附加到该节点,将 将模板内容的克隆到该 shadow DOM。 请注意,您可以对哪些元素进行 attachShadow() 限制

const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.appendChild(starRating.cloneNode(true));

查看开发者工具会发现,<template> 中的 <form> 是每个自定义元素的影子根的一部分。 在开发者工具中的每个自定义元素中,<template> 内容的克隆很明显,并且可以在浏览器中显示,但内容 自定义元素本身的某些部分未渲染到屏幕上。

显示每个自定义元素中克隆的模板内容的开发者工具屏幕截图。

<template> 示例中,我们将模板内容附加到文档正文,将内容添加到常规 DOM。 在 customElements 定义中,我们使用了相同的 appendChild(),但克隆的模板内容附加到 封装的 shadow DOM。

注意到星形图标是如何变回未设置样式的单选按钮的了吗?由于作为 shadow DOM 的一部分而非标准 DOM,因此 Codepen 的 CSS 标签页中的样式设置不适用。该标签页的 CSS 样式的作用域限定为文档,而不是 shadow DOM,因此样式不会应用。我们必须创建 设置封装 Shadow DOM 内容的样式。

阴影 DOM

Shadow DOM 将 CSS 样式的范围限定为每个影子树,将其与文档的其余部分隔离开来。这意味着外部 CSS 也不会应用于组件,且组件样式也不会影响文档的其余部分,除非我们有意 将他们引导至

由于我们已将内容附加到了 shadow DOM,因此我们可以添加一个 <style> 元素 为自定义元素提供封装的 CSS。

由于将作用域限定为自定义元素,因此我们无需担心样式渗漏到文档的其余部分。我们可以 大大降低选择器的特异性。例如,由于自定义元素中使用的唯一输入是单选 按钮,可以使用 input 而不是 input[type="radio"] 作为选择器。

 <template id="star-rating-template">
  <style>
    rating {
      display: inline-flex;
    }
    input {
      appearance: none;
      margin: 0;
      box-shadow: none;
    }
    input::after {
      content: '\2605'; /* solid star */
      font-size: 32px;
    }
    rating:hover input:invalid::after,
    rating:focus-within input:invalid::after {
      color: #888;
    }
    input:invalid::after,
      rating:hover input:hover ~ input:invalid::after,
      input:focus ~ input:invalid::after  {
      color: #ddd;
    }
    input:valid {
      color: orange;
    }
    input:checked ~ input:not(:checked)::after {
      color: #ccc;
      content: '\2606'; /* hollow star */
    }
  </style>
  <form>
    <fieldset>
      <slot name="star-rating-legend">
        <legend>Rate your experience:</legend>
      </slot>
      <rating>
        <input type="radio" name="rating" value="1" aria-label="1 star" required/>
        <input type="radio" name="rating" value="2" aria-label="2 stars"/>
        <input type="radio" name="rating" value="3" aria-label="3 stars"/>
        <input type="radio" name="rating" value="4" aria-label="4 stars"/>
        <input type="radio" name="rating" value="5" aria-label="5 stars"/>
      </rating>
    </fieldset>
    <button type="reset">Reset</button>
    <button type="submit">Submit</button>
  </form>
</template>

虽然 Web 组件使用 in-<template> 标记进行封装,但 CSS 样式的作用域限定为 shadow DOM 并处于隐藏状态 组件之外的所有内容、所渲染的槽内容、<anyElement slot="star-rating-legend"><star-rating> 部分未封装。

样式设置在当前范围之外

您可以在 shadow DOM 中对文档进行样式设置,在 shadow DOM 中通过 全局样式可以遍历影子边界,即 shadow DOM 结束和常规 DOM 开始的位置,但只能遍历 。

影子树是 shadow DOM 内部的 DOM 树。影子根是影子树的根节点。

:host 伪类选择 <star-rating>,即影子宿主元素。 影子主机是 shadow DOM 附加的 DOM 节点。如需仅定位到主机的特定版本,请使用 :host()。 这样将仅选择与传递的参数匹配的影子宿主元素,例如类或属性选择器。选择 所有自定义元素,您都可以在全局 CSS 中使用 star-rating { /* styles */ },或在模板样式中使用 :host(:not(#nonExistantId))。术语 则全局 CSS 将胜出。

::slotted() 伪元素跨越了 shadow DOM 边界 。如果与选择器匹配,它会选择带槽的元素。在我们的示例中,::slotted(legend) 与我们的三个图例相匹配。

要在全局范围内通过 CSS 定位到 shadow DOM,需要修改模板。part 属性可以添加到您要设置样式的任何元素中。然后使用 ::part() 伪元素 来匹配影子树中与传递的参数匹配的元素。伪元素的锚点或原始元素为 宿主或自定义元素名称,在本示例中为 star-rating。该参数是 part 属性的值。

如果模板标记的开头是这样的:

<template id="star-rating-template">
  <form part="formPart">
    <fieldset part="fieldsetPart">

我们可以使用以下命令定位 <form><fieldset>

star-rating::part(formPart) { /* styles */ }
star-rating::part(fieldsetPart) { /* styles */ }

部件名称的作用与类类似:一个元素可以具有多个以空格分隔的部件名称,多个元素可以 都具有相同的部件名称。

Google 针对创建自定义元素提供了实用的核对清单。您可能还想了解 声明式 shadow DOM

检查您的理解情况

测试您对模板、槽位和阴影的掌握情况。

默认情况下,来自 shadow DOM 外部的样式将为其中的元素设置样式。

正确。
请重试。
错误。
正确!

以下哪一项是对 <template> 元素的正确说明?

用于显示您网页中任何内容的通用元素。
请重试。
占位符元素。
请重试。
用于声明默认情况下不会呈现的 HTML 片段的元素。
正确!