網頁元件的好處在於可重複使用:您可以建立 UI 小工具一次,然後重複使用多次。雖然您需要 JavaScript 才能建立網頁元件,但不需要 JavaScript 程式庫。HTML 和相關聯的 API 提供您所需的一切。
網頁元件標準由三個部分組成:HTML 範本、自訂元素和 Shadow DOM。結合這些技術,您就能建構自訂、獨立 (封裝) 且可重複使用的元素,並完美整合至現有應用程式,就像我們已介紹過的其他 HTML 元素一樣。
在本節中,我們會建立 <star-rating> 元素,這是一個網頁元件,可讓使用者以一到五顆星的評分方式,為體驗評分。命名自訂元素時,建議一律使用小寫字母。請務必加入破折號,以便區分一般元素和自訂元素。
我們將討論如何使用 <template> 和 <slot> 元素、slot 屬性和 JavaScript,建立含有封裝影子 DOM 的範本。我們重複使用定義的元素,自訂文字區段,就像任何元素或網頁元件一樣。我們也會簡短討論如何從自訂元素內外使用 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 實作範本,並不是很有用,但如果建立可重複使用、可自訂的星級評等小工具,就很有用。
<slot> 元素
我們加入了一個版位,可加入自訂的每次發生圖例。HTML 提供 <slot> 元素做為 <template> 內的預留位置,如果提供名稱,就會建立「具名版位」。具名版位可用來自訂網頁元件中的內容。<slot> 元素可讓我們控制自訂元素的子項應插入 Shadow Tree 的位置。
在範本中,我們將 <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> 元素沒有使用者代理程式套用的樣式或語意。
在這個 CodePen 中,<template> 和內容不會算繪。<template> 是已知元素,包含不應轉譯的內容。<star-rating> 元素尚未定義。在我們定義元素之前,瀏覽器會像顯示所有無法辨識的元素一樣顯示該元素。目前,無法辨識的 <star-rating> 會視為匿名內嵌元素,因此第三個 <star-rating> 中包含圖例和 <p> 的內容,會像在 <span> 中一樣顯示。
接著定義元素,將這個無法辨識的元素轉換為自訂元素。
自訂元素
您必須使用 JavaScript 定義自訂元素。定義後,<star-rating> 元素的內容會替換為 shadow root,其中包含與其相關聯範本的所有內容。範本中的 <slot> 元素會替換為 <star-rating> 內元素的內容,其 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。
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 tree 的範圍內,與文件的其他部分隔離。這表示外部 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>
雖然網頁元件會封裝在 <template> 標記中,且 CSS 樣式會限定在 Shadow DOM 範圍內,並隱藏在元件外部的所有內容中,但轉譯的版位內容 (<star-rating> 的 <anyElement slot="star-rating-legend"> 部分) 不會封裝。
目前範圍外的樣式
雖然可以從 shadow DOM 內設定文件樣式,以及從全域樣式設定 shadow DOM 內容的樣式,但這並不容易。shadow DOM 的終點和一般 DOM 的起點是 shadow 邊界,可以周遊,但必須非常刻意。
shadow 樹狀結構是 shadow DOM 內部的 DOM 樹狀結構。Shadow root 是 Shadow Tree 的根節點。
:host 虛擬類別會選取 <star-rating>,也就是 Shadow 主體元素。「shadow 主體」是 shadow DOM 附加的 DOM 節點。如要僅指定特定版本的主機,請使用 :host()。這只會選取與傳遞參數相符的 Shadow Host 元素,例如類別或屬性選取器。如要選取所有自訂元素,您可以在全域 CSS 中使用 star-rating { /* styles */ },或在範本樣式中使用 :host(:not(#nonExistantId))。就特異性而言,全域 CSS 優先。
::slotted() 虛擬元素會從 shadow DOM 內部跨越 shadow DOM 邊界。如果與選取器相符,就會選取有槽元素。在本例中,::slotted(legend) 與三個圖例相符。
如要從全域範圍的 CSS 指定 Shadow DOM,則需要編輯範本。part 屬性可新增至任何要設定樣式的元素。接著,使用 ::part() 虛擬元素,比對 Shadow Tree 中與傳遞參數相符的元素。虛擬元素的錨定或原始元素是主機或自訂元素名稱,在本例中為 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 */ }
Part 名稱的作用與類別類似:一個元素可以有多個以空格分隔的 Part 名稱,多個元素也可以有相同的 Part 名稱。
Google 提供建立自訂元素的實用檢查清單。您可能也想瞭解宣告式 Shadow DOM。
隨堂測驗
測試您對範本、版位和陰影的瞭解程度。
根據預設,shadow DOM 外部的樣式會套用至內部的元素。
下列哪一項正確描述 <template> 元素?