簡介
網頁元件是一組最新標準,可用於:
- 讓您建構小工具
- …可靠地重複使用
- …如果元件的下一個版本變更內部實作詳細資料,也不會導致頁面中斷。
這是否表示您必須決定何時使用 HTML/JavaScript,以及何時使用 Web 元件?不可以!HTML 和 JavaScript 可用於製作互動式視覺內容。小工具是互動式的視覺元素,開發小工具時,建議您善用 HTML 和 JavaScript 技能。網頁元件標準可協助您執行這項操作。
不過,有一個基本問題會導致以 HTML 和 JavaScript 建構的小工具難以使用:小工具內的 DOM 樹狀結構並未與網頁的其他部分隔離。沒有封裝表示文件樣式表可能會意外套用至小工具內的某些部分;JavaScript 可能會意外修改小工具內的某些部分;您的 ID 可能會與小工具內的 ID 重疊,以此類推。
Web 元件由三個部分組成:
Shadow DOM 可解決 DOM 樹狀結構封裝問題。網頁元件的四個部分是共同設計,但您也可以挑選要採用網頁元件的哪些部分。本教學課程會說明如何使用 Shadow DOM。
Shadow World 你好
透過 Shadow DOM,元素可以取得與其相關的新類型節點。這種新類型的節點稱為陰影根。具有相關聯陰影根的元素稱為「陰影主機」。系統不會轉譯陰影主機的內容;改為轉譯陰影根的內容。
舉例來說,如果你使用的標記如下:
<button>Hello, world!</button>
<script>
var host = document.querySelector('button');
var root = host.createShadowRoot();
root.textContent = 'こんにちは、影の世界!';
</script>
而非
<button id="ex1a">Hello, world!</button>
<script>
function remove(selector) {
Array.prototype.forEach.call(
document.querySelectorAll(selector),
function (node) { node.parentNode.removeChild(node); });
}
if (!HTMLElement.prototype.createShadowRoot) {
remove('#ex1a');
document.write('<img src="SS1.png" alt="Screenshot of a button with \'Hello, world!\' on it.">');
}
</script>
你的頁面如下所示:
<button id="ex1b">Hello, world!</button>
<script>
(function () {
if (!HTMLElement.prototype.createShadowRoot) {
remove('#ex1b');
document.write('<img src="SS2.png" alt="Screenshot of a button with \'Hello, shadow world!\' in Japanese on it.">');
return;
}
var host = document.querySelector('#ex1b');
var root = host.createShadowRoot();
root.textContent = 'こんにちは、影の世界!';
})();
</script>
不僅如此,如果網頁上的 JavaScript 詢問按鈕的 textContent
為何,它不會傳回「こんにちは、影の世界!»,而是「Hello, world!»,因為 shadow root 底下的 DOM 子樹已封裝。
將內容與簡報分開
接下來,我們將探討如何使用 Shadow DOM 將內容與呈現內容分開。假設其中包含以下名稱標記:
<style>
.ex2a.outer {
border: 2px solid brown;
border-radius: 1em;
background: red;
font-size: 20pt;
width: 12em;
height: 7em;
text-align: center;
}
.ex2a .boilerplate {
color: white;
font-family: sans-serif;
padding: 0.5em;
}
.ex2a .name {
color: black;
background: white;
font-family: "Marker Felt", cursive;
font-size: 45pt;
padding-top: 0.2em;
}
</style>
<div class="ex2a outer">
<div class="boilerplate">
Hi! My name is
</div>
<div class="name">
Bob
</div>
</div>
以下是標記。這是您今天要寫的內容。不使用 Shadow DOM:
<style>
.outer {
border: 2px solid brown;
border-radius: 1em;
background: red;
font-size: 20pt;
width: 12em;
height: 7em;
text-align: center;
}
.boilerplate {
color: white;
font-family: sans-serif;
padding: 0.5em;
}
.name {
color: black;
background: white;
font-family: "Marker Felt", cursive;
font-size: 45pt;
padding-top: 0.2em;
}
</style>
<div class="outer">
<div class="boilerplate">
Hi! My name is
</div>
<div class="name">
Bob
</div>
</div>
由於 DOM 樹狀結構缺乏封裝,因此 name 標記的整個結構會公開給文件。如果網頁上的其他元素不小心使用相同的樣式或指令碼類別名稱,就會造成問題。
我們可以避免發生不愉快的情況。
步驟 1:隱藏簡報詳細資料
從語意的角度來看,我們可能只關心:
- 這是名牌。
- 名稱為「Bob」。
首先,我們會編寫更接近所需實際語意的標記:
<div id="nameTag">Bob</div>
接著,我們將用於呈現的所有樣式和 div 放入 <template>
元素:
<div id="nameTag">Bob</div>
<template id="nameTagTemplate">
<span class="unchanged"><style>
.outer {
border: 2px solid brown;
… same as above …
</style>
<div class="outer">
<div class="boilerplate">
Hi! My name is
</div>
<div class="name">
Bob
</div>
</div></span>
</template>
此時,系統只會算繪「Bob」。由於我們已將呈現的 DOM 元素移到 <template>
元素中,因此不會算繪,但這些元素「可以」透過 JavaScript 存取。我們現在就來填入陰影根目錄:
<script>
var shadow = document.querySelector('#nameTag').createShadowRoot();
var template = document.querySelector('#nameTagTemplate');
var clone = document.importNode(template.content, true);
shadow.appendChild(clone);
設定陰影根目錄後,系統會再次轉譯名稱標記。如果您在名稱標籤上按一下滑鼠右鍵並檢查元素,就會看到精美的語意標記:
<div id="nameTag">Bob</div>
這表示我們使用 Shadow DOM 時,已隱藏文件中名稱標記的呈現詳細資料。呈現詳細資料會封裝在 Shadow DOM 中。
步驟 2:將內容與簡報分開
我們的名稱標記現在會隱藏頁面中的簡報詳細資料,但實際上不會與內容區分開來,因為雖然內容 (名稱「Bob」) 位於頁面中,但顯示的名稱為我們複製到陰影根目錄中的名稱。如果要變更名稱標記的名稱,必須從兩個位置進行,並且可能無法同步。
HTML 元素是組合式元素,例如您可以將按鈕放在資料表中。組合是我們需要的資訊:名稱標記必須是紅色背景、「Hi!」文字以及名稱標記上的內容。
您 (元件作者) 使用名為 <content>
的新元素,定義小工具如何與小工具搭配運作。這會在小工具的呈現方式中建立插入點,而插入點會從陰影主機精選內容,並在該點呈現。
如果我們將 Shadow DOM 中的標記變更為以下程式碼:
<span class="unchanged"><template id="nameTagTemplate">
<style>
…
</style></span>
<div class="outer">
<div class="boilerplate">
Hi! My name is
</div>
<div class="name">
<content></content>
</div>
</div>
<span class="unchanged"></template></span>
算繪名稱標記時,陰影主機的內容會投射到 <content>
元素顯示的位置。
由於名稱只會出現在文件中,因此文件結構變得更簡單。如果頁面需要更新使用者名稱,只要寫下以下內容即可:
document.querySelector('#nameTag').textContent = 'Shellie';
就是這樣。瀏覽器會自動更新名稱標記的顯示效果,因為我們會使用 <content>
將名稱標記的內容投射到適當位置。
<div id="ex2b">
現在,我們已將內容和呈現方式分開。內容位於文件中,而呈現方式則位於 Shadow DOM 中。在算繪時,瀏覽器會自動保持同步。
步驟 3:獲利
將內容和呈現方式分隔開來後,我們可以簡化操控內容的程式碼。在名稱標記範例中,該程式碼只需要處理包含一個 <div>
的簡單結構,而非多個。
現在,如果我們變更呈現方式,就不需要變更任何程式碼!
舉例來說,假設我們想將名稱標籤本地化。它仍然是名稱標記,因此文件中的語意內容並未變更:
<div id="nameTag">Bob</div>
陰影根設定程式碼則維持不變。只不過是在影子根變更中加入的內容:
<template id="nameTagTemplate">
<style>
.outer {
border: 2px solid pink;
border-radius: 1em;
background: url(sakura.jpg);
font-size: 20pt;
width: 12em;
height: 7em;
text-align: center;
font-family: sans-serif;
font-weight: bold;
}
.name {
font-size: 45pt;
font-weight: normal;
margin-top: 0.8em;
padding-top: 0.2em;
}
</style>
<div class="outer">
<div class="name">
<content></content>
</div>
と申します。
</div>
</template>
這項功能相較於目前的網頁情況,可說是大幅改善,因為您的名稱更新程式碼可依賴元件的結構,而這類結構簡單且一致。名稱更新程式碼不需要知道用於轉譯的結構。若考量到實際顯示的內容,英文名稱會在「Hi! My name is”),但先以日文表示 (在「と申します」之前)。從更新顯示名稱的角度來看,這兩種說法在語意上沒有意義,因此名稱更新程式碼不需要知道這項細節。
額外學分:進階投影
在上述範例中,<content>
元素會從陰影主機中挑選所有內容。您可以使用 select
屬性控管內容元素的內容。您也可以使用多個內容元素。
舉例來說,如果您有包含以下內容的文件:
<div id="nameTag">
<div class="first">Bob</div>
<div>B. Love</div>
<div class="email">bob@</div>
</div>
以及使用 CSS 選取器選取特定內容的陰影根:
<div style="background: purple; padding: 1em;">
<div style="color: red;">
<content **select=".first"**></content>
</div>
<div style="color: yellow;">
<content **select="div"**></content>
</div>
<div style="color: blue;">
<content **select=".email">**</content>
</div>
</div>
<div class="email">
元素會與 <content select="div">
和 <content
select=".email">
元素相符。Bob 的電子郵件地址出現幾次,以及以什麼顏色顯示?
答案是 Bob 的電子郵件地址出現一次,且為黃色。
原因是,如同對 Shadow DOM 進行駭客攻擊的使用者所知,建構螢幕上實際轉譯內容的樹狀結構,就像是舉辦大型派對一樣。內容元素是邀請函,可讓文件中的內容進入幕後 Shadow DOM 轉譯方。這些邀請會依序傳送;誰會收到邀請,取決於邀請函的收件者 (即 select
屬性)。內容一經邀請,就會一律接受邀請 (誰會拒絕呢?),然後就會開始播放。如果後續邀請再次傳送到該地址,那麼沒有人在家,邀請函也不會傳送到您的派對。
在上述範例中,<div class="email">
與 div
選取器和 .email
選取器皆相符,但由於含有 div
選取器的內容元素在文件中出現得較早,<div class="email">
會進入黃色群組,而沒有人會進入藍色群組。(這原因可能是「原因」很藍色,不過鮮為人知的是公司,故您永遠無法得知此情況)。
如果某個項目邀請的派對為「無」,則不會產生任何算繪。這就是第一個範例的 「Hello, world」文字。這項做法可用於產生截然不同的轉譯結果:在文件中編寫語意模型,讓網頁中的指令碼可以存取,但為了轉譯目的而隱藏該模型,並使用 JavaScript 將其連結至 Shadow DOM 中截然不同的轉譯模型。
舉例來說,HTML 有一個不錯的日期選擇器。如果您寫入 <input
type="date">
,系統會顯示整齊的彈出式日曆。不過,如果您想讓使用者為沙漠島度假挑選日期範圍 (您知道的,有紅葡萄藤製成的吊床),您可以按照下列步驟設定文件:
<div class="dateRangePicker">
<label for="start">Start:</label>
<input type="date" name="startDate" id="start">
<br>
<label for="end">End:</label>
<input type="date" name="endDate" id="end">
</div>
但建立 Shadow DOM,使用表格建立精美的日曆,以便醒目顯示日期範圍等。當使用者點選日曆中的日期時,元件會更新 startDate 和 endDate 輸入內容中的狀態;當使用者提交表單時,這些輸入元素的值就會提交。
如果標籤不會顯示,為何要在文件中加入標籤?原因是如果使用者透過不支援 Shadow DOM 的瀏覽器查看表單,該表單仍可使用,只是效果不彰。使用者會看到類似以下的畫面:
<div class="dateRangePicker">
<label for="start">Start:</label>
<input type="date" name="startDate" id="start">
<br>
<label for="end">End:</label>
<input type="date" name="endDate" id="end">
</div>
您通過了 Shadow DOM 101
以上就是 Shadow DOM 的基本概念,您已通過 Shadow DOM 101 課程!您可以使用 Shadow DOM 執行更多操作,例如在一個 shadow host 上使用多個 shadow、為封裝使用巢狀 shadow,或是使用模型導向檢視畫面 (MDV) 和 Shadow DOM 架構網頁。網頁元件不僅僅是 Shadow DOM。
我們會在後續文章中說明這些內容。