ブラウザでもNode.jsでも動くテンプレートライブラリ

これは JavaScript Advent Calendar 2011 (Node.js/WebSocketsコース) の19日目の記事です。


JavaScriptで作られたテンプレートライブラリってたくさんありますよね。今日もこんなブログみかけました。

ここに挙がっているものだけでも聞いたことがないものが結構ありました。おそらく、世の中にはここに挙がっているもの以外にもまだまだたくさんあると思います。とにかく多いです。テンプレートのライブラリに限らない話かもしれないですけどね。

テンプレートライブラリに必要な機能って何?


テンプレートライブラリに必要な機能や特徴って何でしょう?条件分岐や繰り返しなど基本的なものは除外するとして、自分がテンプレートのライブラリに求めるものを3つ挙げてみます。

  1. HTMLのエンコードをデフォルトで行う
  2. ロジックの記述を認めない
  3. 数値や日付をフォーマットがしやすい
1. HTMLのエンコードはデフォルトで行う

ちょっとしたエンコード忘れでXSSが起きるのはさけたいですよね。デフォルトで安全がうれしい。

2. ロジックの記述を認めない

ここでのロジックとは、ifの条件とかforの条件を表すJavaScriptの式などのコード片を想定しています。テンプレートにロジックがでてくると、ごちゃごちゃして見通しが悪くなります。employees.length > 0 みたいな式はテンプレートに出さないのが自分の好みにあっています。

3. 数値や日付のフォーマットがしやすい

数値にカンマを入れたり、日付をスラッシュ区切りにしたりよくやりますよね。ただ、自分としては、毎回毎回 同じような記述(yyyy/MM/ddみたいの)を値ごとにテンプレートに書いたり、値ごとにフォーマット関数を用意したりするのは面倒です。アプリによりますが、日付のフォーマットっていったらアプリの中で2、3種類に定まると思うんですよね。だったら、できるだけ宣言的に記述したい。


1番目と2番目については、mustache.js がとても優れています。しかも、mustache.jsはシンプル、そこも魅力的です。ただ、3番目のフォーマットに関する機能がないんですよね。フォーマット済みの値を返す関数をデータとなるオブジェクトにつけてあげればいいのですが、それはちょっと面倒でわかりにくい気がします。

そ、こ、で、tempuraの登場


フォーマットの機能に主眼を置いて自分で作ってみることにしました。その名もtempura(てんぷら)。あれ、昨日のエントリとかぶっているような? 気にしないでー。


tempuraは、ブラウザでもNode.jsでも動くテンプレートライブラリです。mustache.jsをベースに自分があったらいいなと思う機能を追加し、自分にとって必要性がいまいちな機能はざっくり落としています。


リポジトリ


インストール

npm install tempura


tempuraの特徴は、何と言ってもパイプ。シェルとかででてくるアレです。フォーマットに主眼を置いているといいながら、実はフォーマットの機能そのものは持っていません。フォーマットをしやすくするための仕組みを提供します。それがパイプです。
フォーマットについては、利用者にまかせています。アプリごとにどうフォーマットしたいか違うはずですし、アプリが使うライブラリによってフォーマット方法も異なるはずだからです。


では、例をみていきます。ここでは、日付のフォーマットを取り上げますが、momentというライブラリを使用します。


まずは、requireでmomentとtempuraを使えるようにします。

var moment = require('moment');
var tempura = require('tempura');


次は、パイプの関数を定義します。名前づけのルールはありませんが、識別しやすいようにここでは%を名前の先頭につけておきます。あ、1つルールがありました。| は予約語です。名前には使わないようにしてください。それから、ここでは、全てのテンプレートから参照できるようにtempuraオブジェクトに関数を登録していますが、テンプレートごとやデータごとに固有の関数を定義することもできます。

tempura.mergeSettings({
  pipes: {
    '%date': function(value) {
      return value.format('YYYY/MM/DD');
    },
    '%diffUntilXmas': function(value) {
      var xmas = moment([value.year(), 11, 25]);
      return xmas.diff(value, 'days');
    }
  }
});


それから、テンプレートに流し込むオブジェクトを用意します。moment()で現在時刻がとれます。

var data = {
  today: moment(),
  weather: '晴れ'
};


そして、次はテンプレートを用意します。{{ と }} を使ってデータを参照するのは mustache.js と同じなのですが、{{today|%date}} のようにプロパティの名前とパイプの関数の名前を | で連結しているところがtempura独自の機能になります。意味するところは、todayが参照する値を%date関数の引数として処理しその結果を返すよ、というものになります。パイプという名前が示すように関数は | で何個でも連結できます。

var tmpl = tempura.prepare([
  '今日は{{today|%date}}、{{weather}}。',
  'クリスマスまであと{{today|%diffUntilXmas}}日!'
].join('\n'));


最後は、テンプレートにデータを流し込んで結果を取得しコンソールに出力します。

var result = tmpl.render(data);
console.log(result);


出力はこうなります。

今日は2011/12/19、晴れ。
クリスマスまであと5日!


ということで、クリスマスまであと5日。待ち遠しいですね。


さてさて、Advent Calendar、次の方は? なんとか続くといいですね。

補足


上のコード片ですが、まとめるとこうなります。

var moment = require('moment');
var tempura = require('tempura');

tempura.mergeSettings({
  pipes: {
    '%date': function(value) {
      return value.format('YYYY/MM/DD');
    },
    '%diffUntilXmas': function(value) {
      var xmas = moment([value.year(), 11, 25]);
      return xmas.diff(value, 'days');
    }
  }
});

var data = {
  today: moment(),
  weather: '晴れ'
};

var tmpl = tempura.prepare([
  '今日は{{today|%date}}、{{weather}}。',
  'クリスマスまであと{{today|%diffUntilXmas}}日!'
].join('\n'));

var result = tmpl.render(data);
console.log(result);