ただいま新デザインバージョンのプレビュー中です。旧バージョンはこちら。不具合等はお問い合わせより報告お願いいたします。

テンプレートエンジンのススメ 前編 テンプレートエンジンとは

昨今、JavaScriptの役割がいろいろと増え、サーバーからもらったデータをもとに、HTMLの断片を作るという処理が増えてきました。そのような場合に役立つテンプレートエンジンについて解説します。

発行

著者 高津戸 壮 フロントエンド・エンジニア
テンプレートエンジンのススメ シリーズの記事一覧

はじめに

昨今、JavaScriptの役割がいろいろと増え、サーバーからもらったデータをもとに、HTMLの断片を作るという処理が増えてきました。このようなケースではテンプレートエンジンを使うと、よりスマートにコードが書けます。このシリーズでは、2回にわたって、テンプレートエンジンとHandlebarsというライブラリについて解説します。

テンプレートエンジンとは

まず、テンプレートエンジンとは何かというところから解説しましょう。テンプレートエンジンとはデータとテンプレートを合体させ、文字列を作る仕組みのことです。

JavaScriptでいいますと、データというのは、オブジェクトや配列のことで、テンプレートエンジン自体は、JavaScriptのライブラリと考えて問題ありません。

百聞は一見にしかずといいますし、まずはUnderscore.jsの_.templateを使ったサンプルを見てみましょう。Underscore.jsは、CodeGridでも以前紹介しましたが、その機能の一つとして_.templateという簡素なテンプレートエンジンを備えています。

次のサンプルでは、Underscore.jsとjQueryを読み込んでいます。JSONファイルであるdata.jsonを読み込み、_.templateを使ってテンプレートと合わせ、ページ内にappendしています。

index.html

<script id="source" type="text/x-template">
  <ul>
    <% _.each(products, function(product) { %>
      <li><%- product.name %>: <%- product.price %></li>
    <% }); %>
  </ul>
</script>

<script id="source" type="text/x-template">の意味

text/x-templatetypeに指定したscript要素を使うと、ブラウザは対応していないMIMEタイプであると判断するため、これを無視します。このブラウザの挙動を利用し、このようなscript要素をテキストのコンテナとして使っています。これは、テンプレートエンジンを使う時によく利用されるTipsです。

script.js

$(function() {
  var tmplSrc = $('#source').html();
  $.getJSON('data.json').done( function(data) {
    var compiledHtml = _.template(tmplSrc, data);
    $('body').append(compiledHtml);
  });
});

data.json

{
  "products": [
    { "name": "TV", "price": "10000" },
    { "name": "toaster", "price": "3000" },
    { "name": "shaver", "price": "4000" }
  ]
}

結果

こんな具合に、_.templateが用意した独自の記述が混ざったテキスト—以降テンプレートと呼びます —とデータを合体させることで、柔軟にhtmlの断片を作れるというわけです。

さらに、<%- ... %>で展開した内容では、<&lt;に、>&gt;にしてくれるのです。わざわざ特殊文字をエスケープする必要も省けます。

_.templateについては、次の記事に解説がありますので、ご参考ください。とにかく、こういうふうに文字列作成を柔軟にやってくれるのがテンプレートエンジンなのです。

おすすめライブラリつまみ食い - Underscore.js:メソッド

テンプレートエンジンなしで書いてみると

さて、ではこれをUnderscore.jsの_.templateなしで書くとどうなるでしょうか。例えばjQueryでDOMを逐一作ったやり方が、次の例です。

script.js

$(function() {
  var tmplSrc = $('#source').html();
  $.getJSON('data.json').done( function(data) {
    var $ul = $('<ul></ul>');
    var $li, liText;
    _.each(data.products, function(product) {
      $li = $('<li></li>');
      liText = product.name + ': ' + product.price + '円';
      $li.text(liText);
      $ul.append($li);
    });
    $('body').append($ul);
  });
});

結果

地味な文字列結合や要素の作成をたくさん書かなければなりません。

テンプレートエンジンの利点

先ほどのサンンプルくらいならまだいいですが、データがもっと複雑になったらどうでしょうか。サンプルのような処理が超絶な入れ子になってしまうのが目に見えています。

それに実際にHTMLを作るとなると、レイアウトのためのdivだったりクラスだったりがたくさん加わることがほとんどですから、このサンプルのような単純なリストで済むことはまれです。

そんな複雑なコードを一目見て、完成状態のHTMLを想像できるのは超人だけです。テンプレートエンジンは、この複雑さを解消してくれる一つの手段なのです。

テンプレートエンジンを使うと、複雑さを解消する方法として「JavaScriptから、HTML作成部分を切り離す」ということが可能になります。これにより、HTMLを変更したい時につらい思いをしなくて済みます。

データを納品した後「すみません、ここの表示をこういう風に変更したいんですけど……」などと相談された場合、テンプレート部分がきちんと切り離されていれば、そんな修正は楽勝です*。他人が見ても、どこをどう直せばよいのか、探し当てるのに時間はかからないでしょう。

*注:修正の度合い

修正が楽勝、といっても、データ構造そのものを変えなければいけないような大きな改変は別です。

さらにいえば、何かしらのデータを扱う必要が出たのであれば、そのデータをJSONファイルにまとめ、テンプレートエンジンを使ってHTMLソースを作るという風に整理するとよいでしょう。コードをすっきりさせることができます。ぜひ、試してみてほしい方法です。

しかし私は_.templateを使わない

ということで、テンプレートエンジンは素晴らしいよね、という結論ですが……。実は筆者(高津戸)は最近、_.templateを使っていません。代わりに冒頭で名前だけ紹介したHandlebarsを使っています。

そこで今回は_.templateの何が使いづらいのか、次回はHandlebarsのどこが素敵なのかを解説していきたいと思います。

一言、申し添えておきますが、筆者は_.templateが悪いといっているわけではありません。_.templateは、シンプルなテンプレート機能を提供してくれます。しかし、以降に解説するように、いくつかの使いづらさがあるのです。一方、Handlebarsはもっと高機能なのです。

_.templateは属性が見づらい

まずは、要素の属性を書くときの見づらさです。これはリンクの情報をまとめたJSONデータを元に、a要素を作る例です。

data.json

{
  "text": "リンクテキスト",
  "url": "http://example.com",
  "target": "_blank"
}

index.html(テンプレート)

<a href="<%- url %>" <%="" if(target){="" %="">
    target="&lt;%- target %&gt;"
  &lt;% } %&gt;
&gt;
  &lt;%- text %&gt;
<!--%---><!--%--><!--%---></a>

結果

このテンプレートで行っていることは、a要素を作ってhref属性と要素内にテキストを入れるということですが、もしtargetの値が指定されていた場合、その値をtarget属性として追加しています。

_.templateのテンプレート記法では、テンプレートの中でJavaScriptを使いたい時、<% ... %>内に記述します。しかし当然ながら、HTMLのタグも、同じような見栄えの<elem ...>で書きます。これらが1つのテンプレート内に混在すると、いったいここでは何が行われているんだ?と思うこともしばしばです。コードの見通しが悪くなります。

入れ子が深くなる

どうしようもないのが、入れ子が深くなるという点です。何階層にもなっているJSONデータを扱う場合は、テンプレートの中で何回もeachをしなければならなくなるでしょう。

例えば、先ほどのサンプルのような商品名と価格がセットになった商品情報のデータに、さらにcategoriesという項目を付け、それを出力させたいと思ったら、このようになってしまいます。

data.json

{
  "products": [
    {
      "name": "TV",
      "price": "10000",
      "categories": [ "ファミリー", "プレゼント" ]
    },
    {
      "name": "toaster",
      "price": "3000",
      "categories": [ "プレゼント" ]
    }
  ]
}

index.html(テンプレート)

<table>
  <thead>
    <tr>
      <th>商品名</th>
      <th>値段</th>
      <th>カテゴリ</th>
    </tr>
  </thead>
  <tbody><tr>
        <th>&lt;%- product.name %&gt;<!--%---></th>
        <td>&lt;%- product.price %&gt;円<!--%---></td>
        <td>
          &lt;% if(product.categories) { %&gt;
            <ul>
              &lt;% _.each(product.categories, function(category) { %&gt;
                <li>&lt;%- category %&gt;<!--%---></li>
              &lt;% }); %&gt;
            <!--%--><!--%--></ul>
          &lt;% } %&gt;
        <!--%--><!--%--></td>
      </tr><!--%--><!--%--></tbody>
</table>
    &lt;% _.each(products, function(product) { %&gt;

    &lt;% }); %&gt;

結果

これをテンプレートエンジンなしでやるとなると、めまいがしそうですが、テンプレートエンジンを使っても、なかなかにインデントしまくっている感じになってしまいます。

さらに<% } %><% }); %>をたくさん書かなければならないため、これらがどこと対応しているのか、分かりづらいという点も気になってきます。

的確なエラー表示がない

そして、このような長いテンプレートを書いていると、テンプレートの記述を間違えることはよくあることです。そんな時_.templateが出すのは、次のようなエラーです。

_.templateを用いたテンプレートの記述にエラーがあった場合のConsoleでの表示。これだけではどこに間違いがあるのか、ほとんど分からない。

どこで記述が間違っているのか、細かくは教えてくれません。このようなエラーが出たときは、ちょこちょこコードを削りながら、エラーの箇所を特定するしかありません。

まとめ

そんなこんなで、_.templateの使いづらい部分を挙げてみました。_.templateはシンプルですが、使っていると「もうちょっとなんとかできればなぁ...…」と思う点が出てきます。複雑なことをしたかったら、もう少し高機能なテンプレートエンジンがほしいと思うこととなるでしょう。

次回は、今回解説したような問題を解決してくれるHandlebarsを紹介します。