こんにちは。フロントエンドエンジニアをしている清水です。
私は今、スマートフォン向けブラウザゲーム「天下統一クロニクル」を開発・運用しています。


本記事では、天下統一クロニクルで利用しているJsRenderというJavaScriptテンプレートエンジンの基本的な使い方とちょっとしたTIPSを紹介しようと思います。


JsRenderとは?

JsRenderとは、テンプレートエンジンの一種です。

jQuery Templatesの作者でもあるBorisMoore氏が、同ライブラリの後継として作成しているライブラリです。
jQuery Templatesの後継ライブラリではありますが、jQueryにもDOMにも依存していません(jQueryと一緒に使うこともできます)。
まだベータ版という位置づけではありますが、非常に安定しており、APIもわかりやすいです。



JsRenderのよいところ

JsRenderでは、簡単なtrue/falseの条件分岐だけでなく、論理演算子を利用したり、JavaScriptとほぼ同様の条件式が記述できます。


テンプレート内に複雑なロジックを書くべきではない、という議論もあります。
しかし、運用中にJavaScriptを書けないメンバーがテンプレートを触ることもあるので、ある程度許容する必要があると思います。
また、ロジックがテンプレートに書けることにより、ある程度の変更ならテンプレートの修正だけで完結できる、ということもポイントが高いです。



基本的な使い方

天下統一クロニクルはカードゲームなので、カード一覧を例にします。(実際のコードとは異なります)

下記のようなデータを元にカード一覧を作成します。

var data = {
cards: [
{
id: '1',
name: 'レアカード',
level: 10,
maxLevel: 20,
hasSkill: true,
skill: 'それなりのスキルだよ',
rarity: 'rare'
},
{
id: '2',
name: 'のーまるかーど',
level: 10,
maxLevel: 10,
hasSkill: false,
skill: '',
rarity: 'normal'
}
],
maxCardCount: 100,
totalCardCount: 2,
userId: '12345'
};


テンプレートはこんな感じです。
JavaScriptにインラインで書くこともできますが、HTML側にスクリプトタグで書いてしまったほうがメンテナンスはしやすいと思います(もちろんメンバーのスキルセットによります)。

<div id="result"></div>

<script id="templateCardList" type="text/x-jsrender">
<p>{{:totalCardCount}}/{{:maxCardCount}}</p>
<ul>
{{for cards}}
<li id="card_{{:id}}">
<p>Name:<span>{{>name}}({{>rarity}}),</span>
<span>{{:level}}/{{:maxLevel}}{{if level === maxLevel}}(max!){{/if}}</span>
</p>
{{if hasSkill && skill !== ''}}
<p>skill:<span>{{>skill}}</span></p>
{{/if}}
</li>
{{/for}}
</ul>
</script>


実際に使う場所はこんな感じです。

var data = { /* 上で書いた内容 */ };

// テンプレートの登録
jsviews.templates({
cardList: document.getElementById('templateCardList').innerHTML
});

// 描画
document.getElementById('result').innerHTML = jsviews.render.cardList(data);


こんなHTMLが出力されるはずです。

<ul>
<li id="card_1">
<p>Name:<span>レアカード(rare),</span><span>10/20</span></p>
<p>skill:<span>それなりのスキルだよ</span></p>
</li>
<li id="card_2">
<p>Name:<span>のーまるかーど(normal),</span><span>10/10(max!)</span></p>
</li>
</ul>

サンプル



JsRenderのタグの説明

では、上記サンプルで使用したJsRenderのタグについて簡単に説明します。

{{:foo}}, {{>foo}}

これらは値を出力するためのタグです。>だと指定された値をエスケープして表示します。


{{for someArray}}/* ループしたい内容 */{{/for}}

{{for someArray}} ~ {{/for}}で囲われた部分を、開始タグで指定された配列データを充てながらループします。
ループ内のコンテキストは配列内の各要素です。
ちなみにループのインデックスは#indexで取得できます。
また、{{for someArray}} ~ {{else}} ~ {{/for}のように記述すると、配列の長さが0の時に出力する内容を記述できます。


{{if someCondition}} ~ {{else}} ~ {{/if}}

指定した条件で分岐できます。
単純なtrue/falseの値だけでなく、比較や論理演算子の利用もできます。
また、複数の条件分岐をしたい時は{{else}}句に条件を書き、いわゆるelseifとして使えます

{{if someCondition}}
// some code
{{else anotherCondition}}
// another code
{{/if}}


知っていると捗るTIPS

テンプレート内で別のテンプレートを呼び出す

上記の例だと、例えば新たに一つカードを追加したいときなどにすべての要素を描画しなおさないといけません。

それだと効率が悪いので、カード情報を別のテンプレートにし、個別に描画できるようにします。
コードは下記のとおりです。

<!-- HTML -->
<div id="result"></div>

<script id="templateCardList" type="text/x-jsrender">
<p>所持数: <span>{{:totalCardCount}}/{{:maxCardCount}}</span></p>
<ul id="myCardList">
{{for cards tmpl="cardItem"/}}
</ul>
</script>

<!-- テンプレートを分割 -->
<script id="templateCardItem" type="text/x-jsrender">
<li>
<p>Name:<span>{{>name}}({{>rarity}})</span></p>
<p>Level:<span>{{:level}}/{{:maxLevel}}{{if level === maxLevel}}(max!){{/if}}</span></p>
{{if hasSkill && skill}}
<p>skill:<span>{{>skill}}</span></p>
{{/if}}
</li>
</script>

/* JavaScript */
var data = { /* 上で使ったオブジェクト */ };

var anotherCardItem = {
id: '4',
name: 'ハイノーマルカード',
level: 12,
maxLevel: 15,
hasSkill: true,
skill: 'なんともいえないスキルだよ',
rarity: 'highnormal'
};

// テンプレートの登録
jsviews.templates({
cardList: document.getElementById('templateCardList').innerHTML,
cardItem: document.getElementById('templateCardItem').innerHTML
});

jsviews.views.helpers({
dump: function(value) { return JSON.stringify(value);}
});

// 描画
document.getElementById('result').innerHTML = jsviews.render.cardList(data);

// カードをリストにを追加
document.getElementById('myCardList').innerHTML += jsviews.render.cardItem(anotherCardItem);

サンプル


{{for}}以外に、{{props}}{{include}}でも同様に外部のテンプレートを呼び出すことができます。
ただし、{{include}}では描画に使用するデータを指定することはできず、親のテンプレートと同じコンテキストが使用されます。



ループの中で、コンテキストの親データにアクセスする

forタグ内のコンテキストは配列内の各要素ですが、forループ内で配列の外側の値にアクセスしたい、ということもあります。
そんなときは下記の3つの方法が使えます。


1. ~rootを利用する

~rootは、そのテンプレートに渡されたオブジェクト/配列への参照です。
こから辿って行くことですべてのデータにアクセスできます。

<script id="templateCardList" type="text/x-jsrender">
<p>所持数: <span>{{:totalCardCount}}/{{:maxCardCount}}</span></p>
<ul>
{{for cards}}
<li data-user-id="{{>~root.userId}}">
<p>Name:<span>{{>name}}({{>rarity}})</span></p>
<p>Level:<span>{{:level}}/{{:maxLevel}}{{if level === maxLevel}}(max!){{/if}}</span></p>
{{if hasSkill && skill}}
<p>skill:<span>{{>skill}}</span></p>
{{/if}}
<p>ユーザID: {{:~root.userId}}, カードID: {{:id}}</p>
</li>
{{/for}}
</ul>
</script>

サンプル


2. #parent.dataを利用する

#parent.dataを利用すると、そのコンテキストの親データに移動することができます。
parentは任意の数だけつなげて上の階層へ移動できます。

<script id="templateCardList" type="text/x-jsrender">
<p>所持数: <span>{{:totalCardCount}}/{{:maxCardCount}}</span></p>
<ul>
{{for cards}}
<li data-user-id="{{>#parent.parent.data.userId}}">
<p>Name:<span>{{>name}}({{>rarity}})</span></p>
<p>Level:<span>{{:level}}/{{:maxLevel}}{{if level === maxLevel}}(max!){{/if}}</span></p>
{{if hasSkill && skill}}
<p>skill:<span>{{>skill}}</span></p>
{{/if}}
<p>ユーザID: {{:#parent.parent.data.userId}}, カードID: {{:id}}</p>
</li>
{{/for}}
</ul>
</script>

サンプル


3. ループの中から参照できる値を設定する。

ループの中からは基本的に配列内の各要素にしかアクセスできませんが、ループの中から参照する値を個別に設定することができます。
for文の開始タグ内で~foo=someValueのように定義すると、for文内で~fooという形で参照できます。

<script id="templateCardList" type="text/x-jsrender">
<p>所持数: <span>{{:totalCardCount}}/{{:maxCardCount}}</span></p>
<ul>
{{for cards ~userId=userId}}
<li data-user-id="{{>~userId}}">
<p>Name:<span>{{>name}}({{>rarity}})</span></p>
<p>Level:<span>{{:level}}/{{:maxLevel}}{{if level === maxLevel}}(max!){{/if}}</span></p>
{{if hasSkill && skill}}
<p>skill:<span>{{>skill}}</span></p>
{{/if}}
<p>ユーザID: {{:~userId}}, カードID: {{:id}}</p>
</li>
{{/for}}
</ul>
</script>

サンプル



テンプレートに、メインのデータ以外に別のデータを渡す

たとえばデータとテンプレートは使いまわす。でも画面によって少し出し分けしたい。という時などに利用できます。

下記はカード一覧と強化カード選択画面でボタンを出し分けたい、という時の一例です。ページの識別子を渡し、判定に使っています。
この別に渡された値のことを「ヘルパー」と呼びます。ヘルパーは変数名の前に~(チルダ)をつけることで呼び出せます。
ヘルパーは数値型や文字列型だけでなく、関数や配列、オブジェクト等を設定することもできます。

<!-- HTML -->
<script id="templateCardList" type="text/x-jsrender">
<p>所持数: <span>{{:totalCardCount}}/{{:maxCardCount}}</span></p>
<ul>
{{for cards ~userId=userId}}
<li data-user-id="{{>~userId}}">
<p>Name:<span>{{>name}}({{>rarity}})</span></p>
<p>Level:<span>{{:level}}/{{:maxLevel}}{{if level === maxLevel}}(max!){{/if}}</span></p>
{{if hasSkill && skill}}
<p>skill:<span>{{>skill}}</span></p>
{{/if}}
<p>ユーザID: {{:~userId}}, カードID: {{:id}}</p>
<button class="jscCardAction">
{{if ~pageId==='cardList'}}リーダーにする
{{else ~pageId==='upgrade'}}強化素材にする{{/if}}
</button>
</li>
{{/for}}
</ul>
</script>

/* JavaScript */
jsviews.render.cardList(data, {pageId: 'cardList'});

サンプル


出力する値をカスタマイズする

たとえば数値型の値をカンマで三桁区切りにしたいなど、サーバーから受け取った値を加工して表示したいことがあると思います。

そんな時はコンバーターが使えます。コンバーターはただの関数なので様々な処理が行えます。
コンバーターは{{converterName:value}}のようにして使います。

<!-- HTML -->
<script id="tmplConverter" type="text/x-jsrender">
<p>攻撃力:<span>{{thousands:attackPower}}</span></p>
</script>

/* JavaScript */
var data = {
id: '1',
name: 'レアカード',
level: 10,
maxLevel: 20,
hasSkill: true,
skill: 'それなりのスキルだよ',
rarity: 'rare',
attackPower: 25000
};

jsviews.templates({
converter: document.getElementById('tmplConverter').innerHTML
});

// コンバーターの登録
jsviews.views.converters({
thousands: function(num) {
// 小数点消す
num = '' + parseInt(num);

// 3桁毎に区切る
while(num != (num = num.replace(/^(-?\d+)(\d{3})/, "$1,$2")));

return num;
}
});

document.getElementById('result').innerHTML = jsviews.render.converter(data);

サンプル


オブジェクトをループする

{{props}}を使うと、特定のオブジェクトのプロパティをループすることができます。
{{props}}では、keyにプロパティ名、propに対応する値が入っています。
また、{{for}}と同じように、tmplを使って描画に使うテンプレートを指定することもできます。


サンプル


テンプレートをDOM形式で出力する

JsRenderは出力結果を文字列で返してくれますが、一手間加えることでDOM形式に変換することができます。

文字列をDOMに変換、というと

var temp = document.createElement('div');
temp.innerHTML = '<p>foo:<span>bar</span></p>';
document.body.appendChild(temp.firstChild);

みたいな書き方もできますが、divを余計に作る必要があったりしてスマートでないのでrangeを使います。

var range = document.createRange();
range.selectNodeContents(document.body);

// JsRenderで出力された文字列をdocumentFragment化
var tmplFragment = range.createContextualFragment(jsviews.render.someTmpl(data));

document.getElementById('result').appendChild(tmplFragment);

documentFragmentだとそのままappendChildできるのでいいですね。


サンプル



最後に

以上、簡単にJsRenderの使い方について説明させていただきました。

各サンプル毎にjsdo.itのサンプルを用意しておきましたので、実際に試してもらえるとありがたいです。
また、本家のドキュメントも英語ではありますが非常に充実しています。
その場でコードを試すこともできるので、何かあったら英語だと尻込みせず確認してみると幸せになれます。


それでは、最後まで読んでいただきありがとうございました!