読者です 読者をやめる 読者になる 読者になる

mixi engineer blog

ミクシィ・グループで、実際に開発に携わっているエンジニア達が執筆している公式ブログです。様々なサービスの開発や運用を行っていく際に得た技術情報から採用情報まで、有益な情報を幅広く取り扱っています。

Pure JavaScript Template Engine

JavaScript mixi

はじめましてhirokiです。こんにちは。新卒で弊社に入って一年が経過しようとしているので、そろそろエンジニアブロガーの仲間入りをしてみようかと思っています。

今回はJavaScriptのお話です。ハードボイルドなバックエンド側技術のご紹介が多い当ブログですが、スイーツ(笑)なフロントエンド技術もおもしろいんだよ!ということをアピってやろうという魂胆です><。

HTML.Template(JavaScript)

弊社では、サーバサイドによるHTMLの出力テンプレートエンジンにCPANモジュールであるHTML::Template::Proを使用しています。今回はそのJavaScript実装をオープンソースとして開発しましたので、紹介をさせていただきます。

HTML::Templateは貧弱で、冗長で、洗練されていないシンタックスでお馴染みのテンプレートエンジンですが、高速で必要以上のロジックを積み込まないようにできるという点で優れたView Templateモジュールです。

長年、このモジュールを使用してきたという経緯もあり、社内ではこのテンプレート言語に対するノウハウも蓄積されています。そんな中、昨今ではJavaScriptを利用したDHTML+XHRなアプリケーション開発要件も増加傾向にあり、案件ごとに必要なJavaScriptのテンプレートモジュールを探してみるという状況でした。

そこで、ノウハウの継承をしつつ高速でサーバサイドとの統一利用可能なJavaScript Template EngineとしてHTML.Template(JavaScript)を作成しました。

能書きはいいからドキュメントを見せろ!という方はこちらからどうぞ。Prototype1.6xxを必須としているのでご注意ください。

あとは能書きをだらだらと書かせていただきます。

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

JavaScriptでは、画面の書き換えに主に2つの方法を使用します。1つはinnerHTMLによる方法、もう一つがDOM操作による方法です。

それぞれコードで見てみましょう。

// innerHTMLによる置き換え
document
   .getElementById('bodyMainArea')
       .innerHTML = "Hello!World";
// DOM操作による追記
document
   .getElementById('bodyMainArea')
        .appendChild(
            document.createTextNode('helloWorld!')
	);

それぞれに一長一短があるのですが、大規模な書き換えの場合にはStringとしてHTMLを構築してinnerHTMLを行うという方法がもっとも単純です。

DOM構築の複雑さを補うscript.aculo.usのbuilder.jsのようなライブラリも存在しますが、若干のロジックが入ってくるとどちらの方法も記述が難しくなってきます。

// innerHTMLによる書き換え(変数)
document.getElementById('bodyMainArea').innerHTML=[
    '<div class="testBox">',
    '    <ol>',
    '        <li>'+hoge+'</li>',
    '    </ol>',
    '</div>'
].join('');



// 分岐
document.getElementById('bodyMainArea').innerHTML=[
    '<div class="testBox">',
    '    <ol>',
    (someCondition == true)?'<li>hello!</li>':'',
    '    </ol>',
    '</div>'
].join('');


//繰り返し
document.getElementById('bodyMainArea').innerHTML=[
    '<div class="testBox">',
    '    <ol>',
    (function(){
        var ret = [];
      for(
              var i    = 0,
                elem   = arguments[i],
                length = arguments.length;
                
             i < length;
        
             i++,elem = array[i]
        ){
             if(elem.num % 2 == 0 ) ret.push('<li>'+elem.text+'</li>');
         }

        return ret.join('');
    })(
        {num:1,text:'hoge'},
        {num:2,text:'fuga'},
        {num:3,text:'piyo'},
        {num:4,text:'moga'}
     ),
    '    </ol>',
    '</div>'
].join('');

このような文字列処理や関数処理を毎度行うのは面倒ですし、コーダーによるメンテナンスも難しくなります。場合によっては誰も手をつけることのできない残念なコードになりがちです。

もしもHTML.Template(JavaScript)があれば

先ほどの煩雑になったロジックの入った文字列操作をテンプレートを用いて記述すると次のように書くことができます。

var tmpl = new HTML.Template([
    '<div class="testBox">',
    '    <ol>',
    '    <TMPL_LOOP NAME=loop>',
    '    <TMPL_UNLESS NAME=__odd__>',
    '        <li><TMPL_VAR NAME=text></li>',
    '    </TMPL_UNLESS>',
    '    </TMPL_LOOP>',
    '    </ol>',
    '</div>'
].join(''));

tmpl.param({
    loop:[{text:'hoge'},{text:'fuga'},{text:'piyo'},{text:'moga'}]
});

$('bodyMainArea').update(tmpl);

また、テンプレートをコード中からHTMLコメントやXHRからの取得によって隠蔽することもできます。

// test.tmplとして保存
<div class="testBox">
    <ol>
    <TMPL_LOOP NAME=loop>
    <TMPL_UNLESS NAME=__odd__>
        <li><TMPL_VAR NAME=text></li>
    </TMPL_UNLESS>
    </TMPL_LOOP>
    </ol>
</div>

// テンプレートをXHRで取得
var tmpl = new HTML.Template('url:/test.tmpl');
  :
  :
tmpl.param({
    loop:[{text:'hoge'},{text:'fuga'},{text:'piyo'},{text:'moga'}]
});

$('bodyMainArea').update(tmpl);

あるいは、HTML中にコメントとして記述してネットワークリソースの節約や非同期性に伴う煩雑な処理を軽減して利用することもできます。

// html中に記述
<div class="HTML_TEMPLATE" id='test'><!--

<div class="testBox">
    <ol>
    <TMPL_LOOP NAME=loop>
    <TMPL_UNLESS NAME=__odd__>
        <li><TMPL_VAR NAME=text></li>
    </TMPL_UNLESS>
    </TMPL_LOOP>
    </ol>
</div>

--></div>
// テンプレートをHTMLコメントから取得
var tmpl = new HTML.Template('dom:test');

tmpl.param({
    loop:[{text:'hoge'},{text:'fuga'},{text:'piyo'},{text:'moga'}]
});

$('bodyMainArea').update(tmpl);

これは何をしてるの?

LLによるテンプレートエンジンといえば関数の生成と相場が決まっているのですが、HTML.Template(JavaScript)も多分にもれず関数の生成です。先ほどのテンプレートも内部では以下のような関数に変換されています。

function anonymous() {
    var $_R = [];
    var $_C = [this._param];
    var $_GF = HTML.Template.GLOBAL_FUNC;
    var $_T = this._param;
    var $_S = HTML.Template.Cache.STRING_FRAGMENT;
    var $_SELF = this;
    $_R.push($_S[43]);
    var $_L_40 = $A(($_T.loop ? $_T.loop: undefined)) || [];
    var $_LL_40 = $_L_40.length;
    $_C.push($_T);
    for (var i_40 = 0; i_40 < $_LL_40; i_40++) {
        $_T = (typeof $_L_40[i_40] == "object") ? $_L_40[i_40] : {};
        $_T.__first__ = (i_40 == 0) ? true: false;
        $_T.__counter__ = i_40 + 1;
        $_T.__odd__ = ((i_40 + 1) % 2) ? true: false;
        $_T.__last__ = (i_40 == $_LL_40 - 1) ? true: false;
        $_T.__inner__ = ($_T.__first__ || $_T.__last__) ? false: true;
        $_R.push($_S[44]);
        if (! ($_T.__odd__ ? $_T.__odd__: undefined)) {
            $_R.push($_S[45]);
            $_R.push(($_T.__counter__ ? $_T.__counter__: undefined));
            $_R.push($_S[46]);
        }
        $_R.push($_S[47]);
    }
    $_T = $_C.pop();
    $_R.push($_S[48]);
    return $_R.join("");
}

比較的可読性の高いコードジェネレートになっているかと思いますが、それでもやはり手で書く気のうせるイイ感じのコードに仕上がっています。

これらのコードはCopy and Pasteで再利用可能な形として出力可能ですので、関数生成のコストが気になる方はプロダクションコードとしてはダンプ結果を利用することもできます。ただ、オススメはしません。"evalは遅い、evilだ"と教え込まれた方にとっては受け入れがたいかもしれませんが、ほとんどのWebアプリケーションにとってブラウザのアイドル時間は膨大に存在しており、適切なタイミングでコンパイルされれば、UXに影響を与えることはないからです。

C/S同一のシンプルなテンプレートエンジンを使うということ

なぜ、フロントエンドのテンプレートとサーバサイドのテンプレートを同一にする必要があるんだ?という疑問もあるかと思います。たとえば、EJS(embeded javascript)のようなJavaScriptプログラマにとって分かりやすく、何でもできるテンプレートのほうが便利じゃないか、と。

確かに一面的にはそのとおりで、小規模開発や単発案件であればこういったモジュールのほうが便利だという局面もあるでしょう。たとえばPHP/erubyなどはそういった局面で非常に優れた言語です。

これらの回答としては、「HTMLコーディングのメンテナンス性」と「ViewとLogicの分離」という2点に尽きます。

次のようなケースを考えて見ましょう。 同一のテンプレート構成をPHPとEJSで記述しているケースです。

<!-- PHP -->
<div class="testBox">
    <ol>
    <?php
    $a = array("hoge","fuga","more","piyo");
    for($i=0; $i<count($a); $i++){
        if(($i%2) == 0){?>
	    <li><?= $a[$i] ?></li>
        <?}
    }
    ?>
    </ol>
</div>
<!-- EJS -->
<div class="testBox">
    <ol>
    <% var a = ["hoge","fuga","more","piyo"]; 
     for(var i=0; i<a.length; i++){
     if((i%2) == 0){ %>
         <li>
         <%= a[i] %>
         </li>
    <% }
    }%>
    </ol>
</div>

これらは、HTML中のUIパーツとしてユーザからは同じものとして見えています。どちらも同一のロジックで記述されています。このような状況下でデザイン変更が入ったとします。HTMLコーダは、PHP版のテンプレートとJS版のテンプレートを書き換えなければなりません。また、どちらもロジック部分の記述が生の言語環境を利用しているため、双方の言語についての基本的な知識が無ければ、うかつに変更することができません。

HTML.Template(JavaScript)とHTML::Templateを利用している環境であれば、単一のファイルの修正のみで対応できます。

同じく、JavaScriptの関数生成をして、PerlとJavaScript/ClientとServerで同一のテンプレートを利用可能にしているモジュールにJemplateがあります。TT(Tokyo TyrantじゃなくてTemplate Toolkit)のシンタクスで記述できます。Perl実装で、コマンドラインによるjsファイル生成を基本機能として提供していますが、そのままではメンテナンスには向いていないので、フレームワーク取り込みなど何らかの形で透過的に取り扱えるようにすれば非常に快適なテンプレートライフをエンジョイできます。

HTML.Template(JavaScript)は、Jemplateから一部設計思想をインスパイヤしつつ、Pure JavaScriptによる実装で、EJSのような取り扱いの容易さも実現しています。

まとめ

HTML::Templateのシンタックスを備えたテンプレートエンジンをJavaScriptで実装しました。個人ないし少人数でプロダクトを作っているとなかなか気づかないことですが、大規模なシステムを開発していくということはさまざまな分業で成り立っていて、学習コストが低く、適切なTemplateモジュールを採用することで、ますます複雑化するWebアプリケーション開発をスマートに記述できるようになったりします。

次回はもう一歩踏み込んで、JavaScriptテンプレートエンジンの開発と設計についてのちょっとしたノウハウをお話できたらと思います。

まだまだ、つたない実装ですがツッコミいれながら読んでいただければ幸いです。そんなわけで、この次も、ワッフルワッフル!