用戶端模板的標準化
簡介
模板概念並非網頁開發的新概念,事實上,Django (Python)、ERB/Haml (Ruby) 和 Smarty (PHP) 等伺服器端的範本語言/引擎已經存在很久了。不過,過去幾年我們發現,MVC 架構的數量激增。這些層級之間略有不同,但大多數都共用一個機制,用於算繪呈現層 (又稱為 DA 檢視畫面):範本。
我們面對現實吧。使用範本真是太棒了!歡迎隨時詢問。即使是本身的定義,也都能讓您感到溫暖舒適:
「…不必每次都重新建立…。」我不確定您是否也這麼想,但我很樂意避免額外的工作。那麼,為什麼網頁平台缺乏開發人員所重視的本機支援功能?
答案是 WhatWG HTML 範本規格。並定義一個新的 <template>
元素,用於說明用戶端範本時採用以 DOM 為基礎的標準方法。範本可讓您宣告標記片段,這些片段會解析為 HTML,在網頁載入時不會使用,但可在稍後的執行階段中例項化。引述 Rafael Weinstein 的話:
這可讓您放置大量 HTML 程式碼,不希望瀏覽器基於任何理由而出現亂亂無章的 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>
中,我們幾乎無法取得任何重要屬性。
其內容在未啟用前不會有任何作用。基本上,您的標記會隱藏,而且無法轉譯。
範本中的所有內容不會產生副作用。使用範本前,指令碼不會執行、圖片不會載入、音訊不會播放。
將內容視為不屬於文件中。在主頁面中使用
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
大多數人會將 Shadow DOM 附加至主機,方法是將標記字串設為 .innerHTML
:
<div id="host"></div>
<script>
var shadow = document.querySelector('#host').createShadowRoot();
shadow.innerHTML = '<span>Host node</span>';
</script>
這種做法的問題是,Shadow DOM 越複雜,您就必須進行越多的字串連結。這麼做無法擴大規模,而且很快就會變得一團糟,嬰兒也會開始哭鬧。這也是 XSS 一開始出現的原因!<template>
可解決這個問題。
更合理的做法是直接使用 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> (video) - 由我親自製作的超完整簡報。