template engine embedded in underscore.js
クライアントサイドのテンプレートエンジンはjQueryのものが有名ですが、 今回はUnderscore.jsに備わっているミニマムなテンプレートエンジンの使い方を紹介します。 Underscore.jsは、クライアントMVCの代表格の1つであるBackbone.jsで採用されている便利ライブラリです。 (ちなみに両ライブラリの作者はCoffeeScriptを開発しているbraddunbarだったりします。)
この記事はUnderscore.js ver1.4.3を元に記載しています。
テンプレートの構文
Underscore.jsのテンプレートの区切り文字(delimiter)はデフォルトではERB-styleです。
区切り文字を変更するには、_.templateSettings
というオブジェクトの各プロパティ(evaluate
, interpolate
, escape
)を変更することで可能です。
- 評価
- コードをテンプレートの中で評価したい場合は、<% %>で括ります。
- 埋め込み
- 変数などを評価してHTMLに埋め込みたい場合、<%= %>で括ります。
- エスケープ
- HTML文字としてエスケープする場合は、<%- %>で括ります。
例)
// テンプレート実行前 <% for (var i = 0; i < 3; i++) { %> <p>Hello <%= i+1 %> Underscore!</p> <% } %> // テンプレート実行後 <p>Hello 1 Underscore!</p> <p>Hello 2 Underscore!</p> <p>Hello 3 Underscore!</p>
HTML内での使い方
実際にテンプレートを使用してみましょう。
HTML内にインラインでテンプレートを記述する場合は<script>
タグを使用し、HTMLとして画面に出力されないようにします。
そしてテンプレートの部分を文字列として取得し、それを_.template()
関数に入れ、テンプレートをプリコンパイルします。
プリコンパイルした結果は関数となっており、その関数を実行することで最終的なHTML文字列が得られます。
また、プリコンパイルした関数は引数としてオブジェクトを渡すことができ、テンプレート内ではそのプロパティ名でアクセス可能です。
元HTMLコード
<html> <head> <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script> <script src="http://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.4.3/underscore-min.js"></script> </head> <body> <div id="target"></div> <!-- テンプレート --> <script type="text/html" id="target_template"> <% for (var i = 0; i < 3; i++) { %> <p>Hello <%= foo %>! <%= bar %>!</p> <% } %> </script> <script> // テンプレートを文字列として取得 var $templateString = $('#target_template').html(); // テンプレートをコンパイルし、テンプレート関数を得る var template = _.template($templateString); // テンプレート関数を実行 var html = template({ foo: 'foo', bar: 'bar' }); // $('#target').html(html); </script> </body> </html>
ブラウザで見るとJavaScriptが解釈され、このように表示されます。
Hello foo! bar! Hello foo! bar! Hello foo! bar!
Underscore.jsでのテンプレートの実装
使い方は非常に簡単でした。 さて、実装はどうなっているのでしょうか。 underscore.jsはコードサイズも小さくまとまっているため、実装を追いやすく、JavaScriptのコードリーディングにも最適です。
テンプレート部分のコードを転載します。
_.template = function(text, data, settings) { var render; settings = _.defaults({}, settings, _.templateSettings); // Combine delimiters into one regular expression via alternation. var matcher = new RegExp([ (settings.escape || noMatch).source, (settings.interpolate || noMatch).source, (settings.evaluate || noMatch).source ].join('|') + '|$', 'g'); // Compile the template source, escaping string literals appropriately. var index = 0; var source = "__p+='"; text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { source += text.slice(index, offset) .replace(escaper, function(match) { return '¥¥' + escapes[match]; }); if (escape) { source += "'+¥n((__t=(" + escape + "))==null?'':_.escape(__t))+¥n'"; } if (interpolate) { source += "'+¥n((__t=(" + interpolate + "))==null?'':__t)+¥n'"; } if (evaluate) { source += "';¥n" + evaluate + "¥n__p+='"; } index = offset + match.length; return match; }); source += "';¥n"; // If a variable is not specified, place data values in local scope. if (!settings.variable) source = 'with(obj||{}){¥n' + source + '}¥n'; source = "var __t,__p='',__j=Array.prototype.join," + "print=function(){__p+=__j.call(arguments,'');};¥n" + source + "return __p;¥n"; try { render = new Function(settings.variable || 'obj', '_', source); } catch (e) { e.source = source; throw e; } if (data) return render(data, _); var template = function(data) { return render.call(this, data, _); }; // Provide the compiled function source as a convenience for precompilation. template.source = 'function(' + (settings.variable || 'obj') + '){¥n' + source + '}'; return template; };
まず、matcher
という変数が定義されています。
このmatcher
はテンプレート構文に従った正規表現オブジェクトになっています。
そしてこの関数に渡されたテンプレート文字列text
を、text.replace
でテンプレート構文にマッチする箇所を探し出します。
replace
関数にfunctionを渡すと、マッチする箇所が見つかるたびにその関数が呼び出されます。ここでのreplace
関数の使い方は元のtext
を変更するのではなく、変数source
に文字列を付け足していく処理を行なっています。
replace
関数に渡されたfunction
の中は、
- escape(エスケープ)構文が見つかった場合
'+\n ( (__t=(foo) ) == null ? '' : _.escape(__t) )+\n'
- interpolate(埋め込み)構文が見つかった場合
'+\n ( (__t=(bar) ) == null ? '' : __t)+\n'
- evaluate(評価)構文が見つかった場合
';\n var i = 0; \n __p+='`
で場合分けされ、対応する文字列が変数source
に付け加えられます。
何をやっているのかよくわかりませんね。実際に実行した時の変数source
の中身を見てみましょう。
(Chromeのデバッガを使うと便利です。)
<% for (var i = 0; i < 3; i++) { %> <p>Hello <%= foo %></p> <% } %>
上記のようなテンプレート文字列があったとき、text.replace
を通過したあとの変数source
は以下のような文字列になっています。
source = "__p+='\n';for (var i = 0; i < 3; i++) { __p+='\n<p>Hello '+((__t=( foo ))==null?'':__t)+'!</p>\n';} __p+='\n';\n"
なにやら、__p
という変数に文字列を付け加えていく処理を表す文字列のようです。
最終的にこのsource
は、Function
コンストラクタを介して関数オブジェクトへと変わります。以下、動的に作られた関数renderの内容です。
function render (obj, _) { var __t, __p = '', __j = Array.prototype.join, print = function () { __p += __j.call(arguments, ''); }; with (obj || {}) { __p+='\n '; for (var i = 0; i < 3; i++) { __p+='\n <p>Hello '+ ((__t=( foo ))==null?'':__t)+ '!</p> \n '; } __p+='\n '; } return __p; }
これでやっていることは明確になりました。
__p
はテンプレート関数の実行結果であるHTML文字列を表します。
またwith文で実行時のコンテキストがobj
になるため、引数で渡されたオブジェクトのプロパティにアクセスする際に、オブジェクト名をつけずに(obj.foo)、プロパティ名そのもの(foo)でアクセスできます。
(もしオブジェクト名を付けなければいけないとすると、必ず'obj'という固定の仮引数名をテンプレートの中で使わなくてはいけなく、柔軟性に欠けます。)
この_.template()
関数の最終的な返り値は、動的に作ったrender関数を呼ぶ関数になります。
var template = function(data) { return render.call(this, data, _); }; return template;
まとめ
Underscore.jsのテンプレートエンジンの使い方と実装を見てみました。
Backbone.jsと対に使われるUnderscore.jsですが、単体で見てもこのテンプレート機能以外にも面白い(特に関数型言語のような)関数が豊富に用意されているので、皆さんも是非使って、そして実装を追ってみてください。