HTML の新しい template タグ

クライアントサイドのテンプレート化の標準化

はじめに

テンプレート化のコンセプトは、ウェブ開発において新しいものではありません。実際、Django(Python)、ERB/Haml(Ruby)、Smarty(PHP)などのサーバーサイドのテンプレート言語 / エンジンは、長い間使用されてきました。しかし、ここ数年で、MVC フレームワークが急増しています。それぞれ少しずつ異なりますが、ほとんどは、プレゼンテーション レイヤ(別名: ビュー)をレンダリングするための共通のメカニズム(テンプレート)を共有しています。

実際のところ、テンプレートは非常に便利です。ぜひ、周りの人に聞いてみてください。定義だけでも、温かく心地よい気分になります。

「毎回再作成する必要がない」というのは、余計な作業を避けたい私にとって、とても魅力的なことです。では、デベロッパーが明らかに重視している機能が、ウェブ プラットフォームでネイティブにサポートされていないのはなぜでしょうか。

その答えが WhatWG HTML テンプレート仕様です。この仕様では、クライアントサイドのテンプレート化のための標準的な DOM ベースのアプローチを記述する新しい <template> 要素が定義されています。テンプレートを使用すると、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> の内部の任意の場所に配置できます。また、これらの要素で許可されている任意のタイプのコンテンツを含めることができます。「どこでも」とは、HTML パーサーが許可しない場所でも <template> を安全に使用できることを意味します。ただし、コンテンツ モデルの子は除きます。<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>

解決済み

<template> を実際に使用する際に遭遇した、いくつかの注意点をご紹介します。

  • modpagespeed を使用している場合は、このバグに注意してください。インライン <style scoped> を定義するテンプレートは、PageSpeed の CSS 書き換えルールによって head に移動される場合があります。
  • テンプレートを「事前レンダリング」する方法はありません。つまり、アセットのプリロード、JS の処理、初期 CSS のダウンロードなどを行うことはできません。これはサーバーとクライアントの両方に当てはまります。テンプレートがレンダリングされるのは、公開されるときのみです。
  • ネストされたテンプレートには注意してください。想定どおりに動作しません。次に例を示します。

    <template>
      <ul>
        <template>
          <li>Stuff</li>
        </template>
      </ul>
    </template>
    

    外側のテンプレートを有効にしても、内側のテンプレートは有効になりません。つまり、ネストされたテンプレートでは、その子も手動で有効にする必要があります。

標準化への道のり

私たちがどこから来たのかを忘れないようにしましょう。標準ベースの HTML テンプレートへの道のりは長いものでした。長年にわたり、再利用可能なテンプレートを作成するための巧妙な手法が考案されてきました。以下に、よく見られる 2 つの例を示します。この記事では、比較のためにそれらを含めています。

方法 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: オーバーロード スクリプト

もう 1 つの手法は、<script> をオーバーロードして、そのコンテンツを文字列として操作することです。2008 年に John Resig 氏が Micro Templating ユーティリティで初めてこの方法を示しました。現在では、handlebars.js のような新しいものを含め、他にも多くのテンプレート エンジンがあります。

次に例を示します。

<script id="mytemplate" type="text/x-handlebars-template">
  <img src="logo.png">
  <div class="comment"></div>
</script>

この手法の概要は次のとおりです。

  • 何もレンダリングされない - <script> はデフォルトで display:none であるため、ブラウザはこのブロックをレンダリングしません。
  • Inert - タイプが「text/javascript」以外に設定されているため、ブラウザはスクリプト コンテンツを JS として解析しません。
  • セキュリティの問題 - .innerHTML の使用を推奨します。ユーザーが指定したデータの実行時文字列解析は、XSS の脆弱性につながりやすくなります。

まとめ

jQuery によって DOM の操作が非常に簡単になったことを覚えていますか?その結果、querySelector()/querySelectorAll() がプラットフォームに追加されました。これは明らかにメリットですよね。CSS セレクタを使用した DOM の取得を普及させたライブラリがあり、後に標準で採用されました。いつもそううまくいくとは限りませんが、うまくいったときは最高です。

<template> は同様のケースだと思います。クライアントサイドのテンプレート作成方法を標準化するだけでなく、2008 年のハックが不要になるという重要なメリットもあります。ウェブ オーサリング プロセス全体をより健全で、保守しやすく、フル機能にすることは、常に良いことだと私は考えています。

参考情報