HTML's 新增範本標記

用戶端模板的標準化

簡介

模板概念並非網頁開發的新概念,事實上,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> 中,我們幾乎無法取得任何重要屬性。

  1. 其內容在未啟用前不會有任何作用。基本上,您的標記會隱藏,而且無法轉譯。

  2. 範本中的所有內容不會產生副作用。使用範本前,指令碼不會執行、圖片不會載入、音訊不會播放

  3. 將內容視為不屬於文件中。在主頁面中使用 document.getElementById()querySelector() 不會傳回範本的子節點。

  4. 範本放置在 <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 年入侵事件的需求。讓整個網頁製作程序更合理、更易於維護,並提供更多功能,這對我來說一直都是好事。

其他資源