Web 组件的优点是可重用性:您可以创建一个 UI 小部件一次,并多次重复使用。虽然您确实需要 JavaScript 来创建 Web 组件,但您不需要 JavaScript 库。HTML 和相关的 API 提供了您所需的一切。
Web 组件标准由三部分组成:HTML 模板、自定义元素 和 Shadow DOM。结合使用,它们可以构建自定义的、自包含(封装)的、可重用的元素,这些元素可以像我们已经介绍过的所有其他 HTML 元素一样,无缝集成到现有应用程序中。
在本节中,我们将创建 <star-rating>
元素,这是一个 Web 组件,允许用户在一到五星的范围内对体验进行评分。在命名自定义元素时,建议使用全部小写字母。此外,还要包含一个短划线,因为这有助于区分常规元素和自定义元素。
我们将讨论如何使用 <template>
和 <slot>
元素、slot
属性以及 JavaScript 来创建具有封装 Shadow DOM 的模板。然后,我们将重用定义的元素,自定义一部分文本,就像您对任何元素或 Web 组件所做的那样。我们还将简要讨论如何从自定义元素的内部和外部使用 CSS。
<template>
元素
<template>
元素用于声明 HTML 片段,以便使用 JavaScript 克隆并插入到 DOM 中。默认情况下,元素的内容不会呈现。而是使用 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 的一部分。
仅为一个星级评分实现模板需要 JavaScript 并不是很有用,但是为一个重复使用的、可自定义的星级评分小部件创建 Web 组件是有用的。
<slot>
元素
我们包含一个 slot 以包含自定义的每次出现图例。HTML 提供了一个 <slot>
元素作为 <template>
内的占位符,如果提供名称,则创建一个“命名 slot”。命名 slot 可用于自定义 Web 组件中的内容。<slot>
元素使我们能够控制自定义元素的子元素应插入到其 shadow 树中的位置。
在我们的模板中,我们将 <legend>
更改为 <slot>
<template id="star-rating-template">
<form>
<fieldset>
<slot name="star-rating-legend">
<legend>Rate your experience:</legend>
</slot>
如果元素具有 slot 属性且其值与命名 slot 的名称匹配,则 name
属性用于将 slot 分配给其他元素。如果自定义元素没有与 slot 匹配的内容,则会呈现 <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>
元素的内容将被 shadow root 替换,该 shadow root 包含我们与之关联的模板的所有内容。模板中的 <slot>
元素将被 <star-rating>
中元素的内容替换,该元素的 slot
属性值与 <slot>
的 name 值匹配(如果有)。否则,将显示模板 slot 的内容。
自定义元素中未与 slot 关联的内容(例如,我们的第三个 <star-rating>
中的 <p>Is this text visible?</p>
)不包含在 shadow root 中,因此不会显示。
我们通过扩展 HTMLElement
定义名为 star-rating
的自定义元素
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>
是每个自定义元素的 shadow root 的一部分。在开发者工具和浏览器中,每个自定义元素中都显示了 <template>
内容的克隆,但自定义元素本身的内容不会呈现到屏幕上。
在 <template>
示例中,我们将模板内容附加到文档正文,从而将内容添加到常规 DOM。在 customElements
定义中,我们使用了相同的 appendChild()
,但克隆的模板内容已附加到封装的 shadow DOM。
注意到星星如何变回未设置样式的单选按钮了吗?由于是 shadow DOM 而不是标准 DOM 的一部分,因此 Codepen 的 CSS 选项卡中的样式不适用。该选项卡的 CSS 样式作用域限定为文档,而不是 shadow DOM,因此样式未应用。我们必须创建封装的样式来设置封装的 Shadow DOM 内容的样式。
Shadow DOM
Shadow DOM 将 CSS 样式作用域限定为每个 shadow 树,使其与文档的其余部分隔离。这意味着外部 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 组件使用内部 <template>
标记进行封装,并且 CSS 样式作用域限定为 shadow DOM 并对组件外部的所有内容隐藏,但呈现的 slot 内容(即 <star-rating>
的 <anyElement slot="star-rating-legend">
部分)未封装。
当前作用域之外的样式设置
可以(但并不简单)从 shadow DOM 内设置文档样式,以及从全局样式设置 shadow DOM 内容的样式。shadow 边界(shadow DOM 在此边界处结束,而常规 DOM 在此边界处开始)可以被遍历,但只能非常有意识地进行遍历。
shadow 树是 shadow DOM 内的 DOM 树。shadow root 是 shadow 树的根节点。
:host
伪类选择 <star-rating>
(shadow host 元素)。shadow host 是 shadow DOM 附加到的 DOM 节点。要仅定位主机的特定版本,请使用 :host()
。这将仅选择与传递的参数(如类或属性选择器)匹配的 shadow host 元素。要选择所有自定义元素,您可以在全局 CSS 中使用 star-rating { /* styles */ }
,或者在模板样式中使用 :host(:not(#nonExistantId))
。在 特异性方面,全局 CSS 获胜。
::slotted()
伪元素从 shadow DOM 内跨越 shadow DOM 边界。如果 slot 元素与选择器匹配,则会选择该元素。在我们的示例中,::slotted(legend)
与我们的三个图例匹配。
要从全局作用域中的 CSS 定位 shadow DOM,需要编辑模板。可以将 part
属性添加到要设置样式的任何元素。然后使用 ::part()
伪元素来匹配 shadow 树中与传递的参数匹配的元素。伪元素的锚点或原始元素是主机或自定义元素名称,在本例中为 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。
检查您的理解情况
测试您对 template、slot 和 shadow 的知识。
默认情况下,来自 shadow DOM 外部的样式将设置内部元素的样式。
哪个答案是对 <template>
元素的正确描述?