Jemplate で JavaScript でもロジックとビューを分離する

JSON を Template-Toolkit で展開する Jemplate という記事を書いたんですが、Jemplate を使うと何がいいかってのをもう少し詳しく書いてみます。

Jemplate は TT で JavaScript 上の JSON を展開できるんですが、それだけ聞いてもしかすると「これで普段サーバーサイドでやってるテンプレートの展開をクライアントサイドに持って行けて負荷がクライアントに移ってウマー」っていうのが使いどころのようにも思えちゃいますけど、そうじゃない。検索エンジンに引っかからなくなったりとか、アプリケーションの使い勝手が悪くなったりとか色々弊害があります。

そうじゃなくて、Jemplate は JavaScript のためのテンプレートとして使います。

試しに Catalyst で簡単なアプリケーションを作ってみました。ちょっと動かしておく環境がないのでソースだけの解説になっちゃいますが。

DNS lookup をインクリメンタルにやってしまう、というものを題材にしました。

ajax_resolver.png

こんな感じの。まずは Jemplate を使わずに。HTML のソースは以下。

<html>
<head>
<script type="text/javascript" src="/static/js/prototype.js"></script>
<script type="text/javascript" src="/static/js/resolver.js"></script>
</head>
<body>

<h1>Ajax DNS Resolver</h1>

<form id="lookup_handler" onsubmit="return false">
<input type="text" name="query" />
<input type="submit" value="lookup" />
</form>

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

</body>
</html>

JavaScript の resolver.js というのが、このアプリケーションのフロントエンド処理の主な箇所です。サーバーサイドから名前解決の結果を JSON で受け取って、id="result" な要素に結果を展開するということをやってます。prototype.js を使ってます。

resolver.js の中身は以下です。

function lookup() {
     new Ajax.Request('/resolv', {
         parameters : Form.serialize($('lookup_handler')),
         onComplete : function (request) {
             var output = '';
             var records = eval(request.responseText);
             if (records.length) {
                 output = "<h2>" + records.length + " records are found.</h2>";
                 output = output + '<ul>';
                 for (var i = 0; i < records.length; i++)
                     output += '<li>' + records[i].address + '</li>';
                 output = output + '</ul>';
             }
             $('result').innerHTML = output;
         }
     });
     return false;
}

Event.observe(window, 'load', installHandler);

function installHandler() {
    Event.observe($('lookup_handler').query, 'keyup', lookup);
}

Ajax.Request で /resolv という URL を叩くと、JSON が返ってくるのでそれを eval してデータ構造を JavaScript 上で再構築した後、ごちゃごちゃといじっています。

参考までに、/resolv を叩いたときに実行されるコントローラ Resolv.pm のソースは以下です。

package MyApp::Controller::Resolv;

use strict;
use warnings;
use base 'Catalyst::Controller';
use Net::DNS;
use JSON::Syck;

sub default : Private {
    my ( $self, $c ) = @_;
    my $query = $c->req->param('query');
    my $resolver = Net::DNS::Resolver->new;
    my $result = $resolver->search($query);

    my @data;
    if ($result) {
        for my $ans ($result->answer) {
            my $record = {};
            for my $attr (qw(name type address class rdatastr ttl)) {
                $record->{$attr} =  $ans->$attr;
            }
            push @data, $record;
        }
    }

    $c->res->content_type('text/javascript');
    $c->res->output(JSON::Syck::Dump(?@data));
}

1;

Net::DNS で DNS 検索して、その結果からデータ構造を作って、それを JSON::Syck で JSON にしてはき出すということをしてます。

でまあ、これで所望のアプリケーションになるはなるんですけど、先の resolver.js の中身を見たとき

var records = eval(request.responseText);
if (records.length) {
    output = "<h2>" + records.length + " records are found.</h2>";
    output = output + '<ul>';
    for (var i = 0; i < records.length; i++)
        output += '<li>' + records[i].address + '</li>';
    output = output + '</ul>';
}

という部分が「オエッ」という感じになってしまいます。XML じゃなくて JSON を使ってて、DOM みたいなよりややこしいコードがないのでまだマシとは言え、組み立てる HTML が複雑になればなるほどここがいやーんな感じになっちゃいます。

こういう時にこそ JavaScript テンプレートの出番なわけで、prototype.js の新しいバージョンにはそういう機能も付いてくるし、既存のものでもいくつかそういうのはある。しかし、Jemplate を使うと普段なじみの深い TT のシンタックスでそれが扱えちゃいますよと、そういうことになります。

Jemplate を使うように resolver.js の中を書き替えると、

function lookup() {
     new Ajax.Request('/resolv', {
         parameters : Form.serialize($('lookup_handler')),
         onComplete : function (request) {
             var records = eval(request.responseText);
             if (records.length) {
                 $('result').innerHTML = Jemplate.process(
                     'result.tt',
                     { records : records }
                 );
             }
         }
     });
     return false;
}

こんな感じになって、HTML を追い出すことができます。適当なところに result.tt というファイルを作って、

<h2>[% records.size %] records are found.</h2>

<ul>
[% FOREACH rec IN records -%]
<li>[% rec.address %]</li>
[% END -%]
</ul>

という風に書き、これを jemplate でコンパイルする。

$ jemplate --compile jemplates/*.tt > static/js/jemplate01.js

それで、これを HTML からインクルードしますと。

<html>
<head>
<script type="text/javascript" src="/static/js/prototype.js"></script>
<script type="text/javascript" src="/static/js/Jemplate.js"></script>
<script type="text/javascript" src="/static/js/jemplate01.js"></script>
<script type="text/javascript" src="/static/js/resolver.js"></script>
</head>
...

これで動きを変えずに、JavaScript の中身を綺麗にすることができて保守性があがります。要するに、Jemplate は JSON と TT を使った JavaScript テンプレートそのものっていうわけで。

今後 Jemplate の機能が拡張されていって、例えば TT プラグインみたいなものも処理できるようになったりすると、JavaScript テンプレート以上の機能がいろいろ期待できて、すなわち「うはー夢がひろがりんぐ」となりそう、ということです。

ちなみに、Jemplate で置き換えた版は出力がちゃんと出ませんでしたw 一応 FOREACH ディレクティブで JSON を回すところまではいけてるんですが、rec.address の値が undefined だったり、TT の Virtual Methods がまだ実行できないので records.size で値が得られなかったりとか。この辺はこれからの開発動向に期待。

ところでこのサンプルアプリケーションですが、今月末(2/23)に発売される WEB+DB PRESS Vol.31 に書いた記事で使ったものをちょこっと改造してます。記事の内容はといいますと、さすがに Jemplate のことには触れてませんが、prototype.js の話を中心に Class-style OO とか Ajax とか、あとちょっと JSON とかそういう話を書いたものになってます。