HTML 的新範本標記

標準化用戶端範本

簡介

範本的概念在網頁程式開發中並不新穎。事實上,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> 中,可提供一些重要屬性。

  1. 在啟用前,其內容基本上不會執行。基本上,標記是隱藏的 DOM,不會算繪。

  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

大多數人會將標記字串設為 .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 年駭客的需求。我認為,讓整個網頁製作程序更合理、更易於維護、功能更全面,絕對是件好事。

其他資源