標準化用戶端範本
簡介
範本的概念在網頁程式開發中並不新穎。事實上,Django (Python)、ERB/Haml (Ruby) 和 Smarty (PHP) 等伺服器端範本語言/引擎已存在很長一段時間。不過,過去幾年來,MVC 架構如雨後春筍般湧現。雖然這些架構略有不同,但大多數都使用相同的機制來轉譯呈現層 (也就是檢視畫面):範本。
我們必須承認,範本非常實用。不信的話,你可以到處問問。就連定義也讓人感到溫暖舒適:
「…不必每次都重新建立…」不知道您怎麼想,但我很喜歡避免額外的工作。那麼,為什麼網路平台不原生支援開發人員顯然關心的功能呢?
WhatWG HTML 範本規格就是答案。它定義了新的 <template> 元素,用於描述用戶端範本的標準 DOM 方法。範本可讓您宣告標記片段,這些片段會剖析為 HTML,在載入網頁時不會使用,但稍後可在執行階段例項化。引用 Rafael Weinstein 的話:
你可以將大量 HTML 放在這裡,瀏覽器就不會以任何理由干擾這些內容。
Rafael Weinstein (規格作者)
特徵偵測
如要偵測 <template>,請建立 DOM 元素,並檢查 .content 屬性是否存在:
function supportsTemplate() {
return 'content' in document.createElement('template');
}
if (supportsTemplate()) {
// Good to go!
} else {
// Use old templating techniques or libraries.
}
宣告範本內容
HTML <template> 元素代表標記中的範本。其中包含「範本內容」,基本上是可複製 DOM 的惰性區塊。您可以將範本視為鷹架,在應用程式的生命週期內使用 (和重複使用)。
如要建立範本內容,請宣告一些標記,並將其包裝在 <template> 元素中:
<template id="mytemplate">
<img src="" alt="great image">
<div class="comment"></div>
</template>
四大要素
將內容包裝在 <template> 中,可提供一些重要屬性。
在啟用前,其內容基本上不會執行。基本上,標記是隱藏的 DOM,不會算繪。
範本中的任何內容都不會產生副作用。指令碼不會執行、圖片不會載入、音訊不會播放…直到範本使用為止。
系統會將內容視為不在文件中。在主頁面中使用
document.getElementById()或querySelector(),不會傳回範本的子節點。範本可以放在
<head>、<body>或<frameset>內的任何位置,且可以包含這些元素允許的任何類型內容。請注意,「任何位置」是指<template>可安全用於 HTML 剖析器不允許的位置,但內容模型子項除外。也可以放在<table>或<select>的子項中:
<table>
<tr>
<template id="cells-to-repeat">
<td>some content</td>
</template>
</tr>
</table>
啟用範本
範本必須啟用才能使用。否則內容將無法顯示。
最簡單的方法是使用 document.importNode() 建立 .content 的深層副本。.content 屬性是唯讀的 DocumentFragment,包含範本的內容。
var t = document.querySelector('#mytemplate');
// Populate the src at runtime.
t.content.querySelector('img').src = 'logo.png';
var clone = document.importNode(t.content, true);
document.body.appendChild(clone);
範本套用後,內容就會「上線」。在這個範例中,系統會複製內容、提出圖片要求,並算繪最終標記。
示範
範例:無效指令碼
這個範例說明範本內容的惰性。<script> 只會在按下按鈕時執行,並蓋印範本。
<button onclick="useIt()">Use me</button>
<div id="container"></div>
<script>
function useIt() {
var content = document.querySelector('template').content;
// Update something in the template DOM.
var span = content.querySelector('span');
span.textContent = parseInt(span.textContent) + 1;
document.querySelector('#container').appendChild(
document.importNode(content, true)
);
}
</script>
<template>
<div>Template used: <span>0</span></div>
<script>alert('Thanks!')</script>
</template>
範例:使用範本建立 Shadow DOM
大多數人會將標記字串設為 .innerHTML,藉此將 Shadow DOM 附加至主體:
<div id="host"></div>
<script>
var shadow = document.querySelector('#host').createShadowRoot();
shadow.innerHTML = '<span>Host node</span>';
</script>
這種做法的問題在於,Shadow DOM 越複雜,需要串連的字串就越多。這種做法無法擴充,很快就會變得一團亂,讓人頭痛不已。這也是 XSS 最初的誕生方式!<template>即刻救援。
比較合理的做法是將範本內容附加至 Shadow Root,直接使用 DOM:
<template>
<style>
:host {
background: #f8f8f8;
padding: 10px;
transition: all 400ms ease-in-out;
box-sizing: border-box;
border-radius: 5px;
width: 450px;
max-width: 100%;
}
:host(:hover) {
background: #ccc;
}
div {
position: relative;
}
header {
padding: 5px;
border-bottom: 1px solid #aaa;
}
h3 {
margin: 0 !important;
}
textarea {
font-family: inherit;
width: 100%;
height: 100px;
box-sizing: border-box;
border: 1px solid #aaa;
}
footer {
position: absolute;
bottom: 10px;
right: 5px;
}
</style>
<div>
<header>
<h3>Add a Comment
</header>
<content select="p"></content>
<textarea></textarea>
<footer>
<button>Post</button>
</footer>
</div>
</template>
<div id="host">
<p>Instructions go here</p>
</div>
<script>
var shadow = document.querySelector('#host').createShadowRoot();
shadow.appendChild(document.querySelector('template').content);
</script>
Gotchas
以下是我在實際使用 <template> 時遇到的一些問題:
- 如果您使用 modpagespeed,請留意這個錯誤。定義內嵌
<style scoped>的範本,可透過 PageSpeed 的 CSS 重寫規則移至標頭。 - 範本無法「預先算繪」,也就是說,您無法預先載入素材資源、處理 JS、下載初始 CSS 等。伺服器和用戶端都是如此。範本只會在發布時算繪。
請謹慎使用巢狀範本。運作方式可能與預期不同。例如:
<template> <ul> <template> <li>Stuff</li> </template> </ul> </template>啟用外部範本不會啟用內部範本。也就是說,巢狀範本也需要手動啟用子範本。
標準化之路
別忘了我們的初衷。標準 HTML 範本的發展歷程漫長。多年來,我們想出了一些相當巧妙的方法,可以建立可重複使用的範本。以下是我遇過的兩個常見問題。 本文會一併介紹,方便你比較。
方法 1:螢幕外 DOM
長久以來,人們一直使用的一種方法是建立「螢幕外」DOM,並使用 hidden 屬性或 display:none 將其隱藏。
<div id="mytemplate" hidden>
<img src="logo.png">
<div class="comment"></div>
</div>
雖然這項技術可行,但有一些缺點。這項技術的概要如下:
- 使用 DOM:瀏覽器知道 DOM。這項功能很實用。我們可以輕鬆複製。
- 不顯示任何內容:新增
hidden可防止顯示區塊。 - 非惰性:即使內容隱藏,系統仍會對圖片發出網路要求。
- 樣式和主題設定繁瑣:嵌入頁面必須在所有 CSS 規則前加上
#mytemplate,才能將樣式範圍縮小至範本。這種做法不夠穩固,無法保證日後不會發生命名衝突。舉例來說,如果嵌入頁面已有該 ID 的元素,我們就無法順利完成。
方法 2:超載指令碼
另一種技術是超載 <script>,並將其內容當做字串進行操控。John Resig 可能是第一個在 2008 年使用微型範本公用程式展示這項功能的人。現在還有許多其他工具,包括 handlebars.js 等新工具。
例如:
<script id="mytemplate" type="text/x-handlebars-template">
<img src="logo.png">
<div class="comment"></div>
</script>
這項技術的概要如下:
- 不顯示任何內容:瀏覽器不會顯示這個區塊,因為
<script>預設為display:none。 - Inert:瀏覽器不會將指令碼內容剖析為 JS,因為其類型設為「text/javascript」以外的值。
- 安全性問題:鼓勵使用
.innerHTML。在執行階段剖析使用者提供的資料字串,很容易導致 XSS 漏洞。
結論
還記得 jQuery 讓 DOM 的使用變得非常簡單嗎?結果是 querySelector()/querySelectorAll() 成功加入平台。這顯然是個好處,對吧?某個程式庫推廣了使用 CSS 選取器擷取 DOM 的做法,後來標準也採用了這種做法。雖然不一定每次都能這樣,但我很喜歡這種感覺。
我認為 <template> 也是類似情況。它將我們進行用戶端範本化的方式標準化,但更重要的是,它消除了我們對 2008 年駭客的需求。我認為,讓整個網頁製作程序更合理、更易於維護、功能更全面,絕對是件好事。
其他資源
- WhatWG 規格
- 網頁元件簡介
- <web>components</web> (影片) - 由本人親自製作的精彩完整簡報。