タグ別アーカイブ: develop

Hokkaido.pm#7に参加してみた

 

Casualを含めて今回3度目の参加でした。今回はWEBアプリ再入門がテーマということで、WAF(ウェブアプリケーションフレームワーク)のお話が中心でした。WAFは選択肢豊富で迷いそうだけど、一方で、どれも大差ないようにも感じました。いずれにしても今、Perlが熱い。

Amon2(akiymさん)

高校生にあんな立派な発表されたら敵いません。KENT WEBの掲示板を移植していたのが面白かった。ああいうのをサクっと作れちゃうのは素晴らしい。スライドもう一度みたいなあ。あがってた

http://akiym.com/slides/20120512-hokkaidopm7-amon2/index.html

Dancer(aloelightさん)

Dancerは、ルーターに渡すコールバックでレスポンス用の文字列をreturnしていた点に、Mojoliciousとの設計の違いを感じました。あと、プラグイン数の比較がためになりました。

http://www.slideshare.net/aloelight/using-dancer

Mojolicious(jamadam)

MojoliciousでPHPライクなことをするお話をしました。

http://www.slideshare.net/jamadam/mojolicious-12907706

Ops Tools with Perl(riywoさん)

カバンがかっこよかったです。運用のお話は僕にはちょっと難しかったですが、cron blueprintのあたりは楽しげでした。あとDSLの作り方も興味深かった。

http://www.slideshare.net/riywo/ops-tools-with-perl-20120512-hokkaidopm

LTではお二方が音系のお話をされていたり、この日、一番の爆笑をとったapache依存のWAFのお話など、充実の内容でした。

懇親会では多くの方とお話しできて幸せでした。perlの次バージョンはuse strictがデフォになると初めて知りました。古くて危険なスクリプトがやっと淘汰されていくのかな。

昼につけ麺、夜にラーメンは反省点でした。腹壊しました。

mojo-checkbotを作ってみてる!

 

Mojolicious::Liteでウェブサイトのリンクチェックをするmojo-checkbotを作ってみてます。動作は恐ろしくノーチェックです。

$ mojo-checkbot.pl daemon --start http://example.com --match http://example.com/
[Mon Oct 17 23:18:35 2011] [info] Server listening (http://*:3000)
Server available at http://127.0.0.1:3000.

startオプションにURLを指定してdaemonを起動するとクロールが始まり、http://127.0.0.1:3000にブラウザでアクセスすると、チェックしたURLとそのステータスコードが表示されます。

matchオプションでURLを限定することができるので、とりあえずチェック対象を自サイト内にとどめることはできますが、今のところ低機能です。

SYNOPSIS

mojo-checkbot.pl daemon [--start start URL] [--match match string] [--sleep seconds]

Mojoliciousのバンドルファイルを整理するプラグイン

 

Mojoliciousのエラー画面で使われている画像やJSやCSS。リバースプロキシを拡張子などのホワイトリストで記述すると、これらのバンドルファイルがプロキシを通過せずに404エラーになってしまう。具体的にはこういうケース。

RewriteEngine on
RewriteRule ^(.*(.(html|htm|xml))|/)$ http://localhost:3000$1 [L,P,QSA]

バンドルファイルはなぜかpublic直下にちりばめられているため、記述しづらい。それにファイル名が衝突してることに気付かずハマりそう。そこで、バンドルファイルがあたかも単一ディレクトリにまとまってるかのように装うプラグインMojolicious-Plugin-UnmessifyBundleです。

sub startup {

    my $self = shift;

    # Mojolicious
    $self->plugin(unmessify_bundle => {prefix => 'mojolicious-bundle'});
}

# Mojolicious::Lite
plugin unmessify_bundle => {prefix => 'mojolicious-bundle'};

これを使うと例えばバンドルされたjqueryは/mojolicious-bundle/js/jquery.jsでアクセスできます。エラーページのHTMLソースも適当に書き換えられます。

すると、バンドルファイルをワンライナーでホワイトリスト入りさせることができるのです。

    RewriteEngine on
    RewriteRule ^(.*(.(html|htm|xml))|/)$ http://localhost:3000$1 [L,P,QSA]
    RewriteRule ^/mojolicious-bundle(/.+)$ http://localhost:3000$1 [L,P,QSA]

前回以上にニーズのなさそうなネタでした。

 

CSSでいじれるディレクトリツリーの生成スクリプト

ブログ記事にディレクトリーツリーを掲載しようと思ったんだけど、treeコマンドの出力は味気ないので、CSSでいじりやすくHTML出力できるperlスクリプトをチョー適当に作った。本来の仕事が進まない。

[2011.6.22]ソート追加

 

use strict;
use warnings;
use utf8;
use Getopt::Long;

    my %args = ('L' => 10);
    GetOptions(%args, 'L=s');
    my ($dir) = @ARGV;
    &loopDir($dir || '.', 0);
    exit;

    sub loopDir {
        my ($dir, $depth) = @_;
        chdir($dir) || die "Cannot chdir to $dir\n";
        opendir(my $dh, ".");
        my @entries = sort {$a cmp $b} grep {substr($_,0,1) ne '.'} readdir($dh);
        closedir($dh);
        if (scalar @entries) {
            print "<ul>\n";
            for my $f (@entries) {
                if (-d $f) {
                    print qq{<li class="dir">\n};
                    print qq{$f\n};
                    if ($depth < $args{L} - 1) {
                        loopDir($f, $depth + 1);
                    }
                    print qq{</li>n};
                } elsif (-f $f) {
                    print qq{<li class="file">$f</li>\n};
                }
            }
            print "</ul>\n";
        }
        chdir("..");
    }
  • crypt.pl
  • ddsn_get.sh
  • memo.txt
  • ffmpeg
    • 02.3gp
    • 02a.wav
    • 01a.3gp
    • 01a.wav
    • 02a.3gp
    • 2ch.mp3
    • test.3g2
    • brave
      • 01_2.avi.filepart
    • 01.3gp

 

CSSもかなりテキトー。

.directoryTree {
    padding:10px;
    border:1px solid #aaa;
    background-color:#fffbf0;
}
    .directoryTree ul ul {
        padding:0;
        margin-left:0;
        margin-bottom:0.5em;
        padding-bottom: 0 !important;
        border-left:3px solid #ddd;
    }
    .directoryTree li {
        margin-left:0 !important;
        padding-left:30px !important;
        vertical-align:bottom;
        background-repeat:no-repeat;
        background-position:10px 0;
        list-style:none;
    }
        .directoryTree li.dir {
            background-image:url(./images/xiao_icon/40.png);
            margin-bottom:0.5em;
        }
            .directoryTree li.dir > ul {
                margin-left:-1em;
            }
        .directoryTree li.file {
            background-image:url(./images/xiao_icon/21.png);
        }

Mojoliciousにウェブ制作をデプロイする

「ウェブアプリ開発」と「ウェブ制作」って別のジャンルなのかなあ。主に受託のコーポレートウェブ制作を生業にしていると流行りのウェブアプリケーションフレームワーク的なものにあまり馴染めない。まるで別世界の出来事のようだ。これって何となくURLディスパッチに対する発想の違いな気がしたので、Mojoliciousのディスパッチャを豪快に差し替えて、ウェブ制作を「デプロイ」できるようにした。apache風のディスパッチャをMojoliciousに登録する「MojoX::Tusu」。

  • リクエストパスはサーバーのディレクトリ階層を意味する。
  • テンプレートもstaticファイルもpublic_htmlに突っ込む。
  • server-parsedの対象は拡張子で決める。
  • apacheのDirectoryIndexに相当する機能。
  • apacheのErrorDocumentに相当する機能。
  • otherのread権限のないファイルへのアクセスは403エラー。
  • apacheを真似てディレクトリfooへのアクセスはfoo/にリダイレクトしてみた。

Mojoliciousルートにpublic_htmlを作り、手元にあったいくつかの静的サイトデータを突っ込んだところ、ビルトインサーバー, CGI, hypnotoad上で今までどおり動作しました。そして、PHPで言えば「AddType application/x-httpd-php .html」としたのと似た状態になり、htmlファイル内の好きなところに動的コンテンツを挿入できるという訳です。ただし、念のため先に書いておくと、Mojoliciousの標準のテンプレート命名規則(index.html.epとか)がMojoX::Tusuの差し替えたディスパッチャの方針と合致しないため、今のところepやeplは使えません。epRendererあたりをそっくり差し替えれば行けそうですが、とりあえず僕のお気に入りのテンプレートエンジン「Text::PSTemplate」でお楽しみください。

MojoX::Tusuを用いたMojoliciousアプリは下記のようになります。とりあえずrouteは必要ありません。

use MojoX::Tusu;
use strict;
use warnings;
use base 'Mojolicious';

sub startup {
    my $self = shift;
    my $tusu = MojoX::Tusu->new($self);
}

URLディスパッチに関しては内部的に下記のようなことをやってます。リクエストパスがテンプレートの在り処を意味するという訳です。ちなみにname('')としているのはテンプレート名が空(/へのアクセス)のとき、自動生成されたroute名がテンプレート名になってしまうからなのです。

$app->routes
    ->route('/:template', template => qr{.*})
    ->name('')
    ->to(cb => sub {$_[0]->render(handler => 'tusu')});

URLディスパッチルールが昔ながらのルールに乗っ取っているので、mojolicious rootが必要な場合意外はテンプレート内にurl_forと書く必要はありません。デザイナーさんが使うオーサリングソフトとの親和性も高いです。リンクパスの表記を巡っては、ここ1年くらいhtmlのbaseタグを活用して乗り切ってきたのですが、いろいろ面倒を抱え込むことにもなってやめたという経緯もあります。なお、このデフォルトのrouteは初回のon_processフックで登録してるので、startup内でルートが登録されればそちらが優先されます。

ドキュメントルート。内部的にはこうです。

$app->static->root('public_html');
$app->renderer->root('public_html');

Mojoliciousのデフォルトディスパッチャはstaticファイルを先に存在確認するようになってます。テンプレートとstaticを同じディレクトリに詰め込むと、全てstaticとして扱われてしまうので、フロントディスパッチャを組み直しています。デフォルトでは.htmlと.htmと.xmlだけテンプレートとして処理し、それ以外はstatic。ただし、DirectoryIndexの適用やパーミッションのチェックなどをして、エラーを返したりもします。なお、スクリプトファイルがあるとソースが見えちゃうので、リバースプロキシなりhtaccessなりで排除しないといけません。

お問い合わせフォームを作りたくなったら、もちろんrouteを追加することもできます。

sub startup {
    my $self = shift;
    my $tusu = MojoX::Tusu->new($self);
    my $r = $self->routes;
    $r->route('/inquiry/')->via('post')->to(cb => sub {
        ### メール送るとか
    })
}

ただし、MojoX::Tusu方式では、routeはあくまでディレクトリであって、inqueryコントローラーを意味しないってのは譲れないルールなのです。実際、/inquiry/へのアクセスの際には先ず/inquiry/index.htmlの存在確認がなされます。

 

public_html直下にindex.cgiを置いてCGIモードでも動かせます。その場合は

RewriteCond %{REQUEST_FILENAME} ((.(html|htm|xml))|/)$
RewriteRule ^(.*)$ index.cgi/$1 [QSA,L]

としておけば動くと思います。また、この場合、public_html内に既設のcgiやphpがあるとそのまま動くので、放っておきたい過去の遺物がある場合に便利です。

また、稼働中のサイトの一部のディレクトリにMojoX::Tusuを導入したい場合は、仮想的なドキュメントルートを作ってmojoliciousルートを跨ぎます。

RewriteEngine On
RewriteBase /path/to/root/
RewriteCond %{REQUEST_URI} ((.(html|htm|xml))|/)$
RewriteCond %{REQUEST_FILENAME} !index.cgi
RewriteRule ^(.*)$ public_html/index.cgi/$1 [QSA,L]
RewriteCond %{REQUEST_FILENAME} !index.cgi
RewriteCond %{REQUEST_FILENAME} !public_html
RewriteRule ^(.*)$ public_html/$1 [QSA,L]

とすれば、外からは/path/to/root/a/b.htmlと見える物が/path/to/root/public_html/index.cgi/a/b.htmlを指すようになる、はず。mod_rewriteはちょっと自身なし。

 

MojoX::TusuはhtmlファイルをText::PSTemplateを使ってレンダリングするので、こんな感じでHTMLを拡張できます。

<html>
    <div>
        <% include('copyright.html') %>
    </div>

MojoX::Tusu(というかMojolicious)はテンプレートエラーをプリティに表示してくれるので、デザイナーさんでも作業しやすい。

 

 

 

 

あと、MojoX::Tusuは独自にプラグインとコンポーネントという仕組みも提供していて、いろいろ拡張できます。

つづく

Text::PSTemplateをrequireできるようにした

 

Text::PSTemplateをrequireで使えるようにしました。

Twitter上でPerlのAttribute::Handlerが過去の遺物だということを知ってしまった。大好きだったのに。。いや今でも好き。動けば問題ないのでAttributeは使っていく。

Text::PSTemplateではプラグインクラスでTplExportアトリビュートを指定したサブルーチンを検出してテンプレートオブジェクトに設定するため、CHECKフェーズでキューにシンボル名を登録し、実行フェーズで実際のテンプレートオブジェクトに登録する手順にしてました。CHECKフェーズを利用しているため、必然的にプラグインはuseしなければなりませんでした。ただ、流行りのフレームワークなど色々見てまわってみると、どうやらテンプレートエンジンはrequireで使えないと話しにならないっぽい。なのでできるようにした。

何をしたかというと、Attribute::Handlerの呼び出しフェーズをBEGINに変えました。BEGINフェーズではシンボルテーブルが未完成のため、ハンドラの引数でサブルーチンのシンボル名を受け取ろうと思っても中途半端な結果にしかならないっぽい。なので、BEGINフェーズで確実に取得できるコードリファレンスをキューに登録しておき、プラグイン機構の決まりで最初に確実に実行されるnewコンストラクタ内でプラグインパッケージのシンボルテーブルとリファレンスを照合し、シンボル名を得るという手順にしてみた。

汎用的に書き直すと、こんなことをした。attr1アトリビュートを定義する例。

package SomeClassBase;
use strict;
use warnings;
use Attribute::Handlers;

    my %_attr1_cue;
    my %_attr_fixed;

    ### ---
    ### Constractor
    ### ---
    sub new {

        my ($class, @args) = (@_);
        my $self = bless {}, $class;

        if (! $_attr_fixed{$class}) {
            $class->_fix_attr1_cue;
            $_attr_fixed{$class} = 1;
        }
        $class->_do_attr1_specific_process();

        return $self;
    }

    sub ATTR1 : ATTR(BEGIN) {

        my($pkg, undef, $ref, undef, $data, undef) = @_;
        push(@{$_attr1_cue{$pkg}}, [$ref, $data ? {@$data} : {}]);
    }

    sub _fix_attr1_cue {

        my $class = shift;
        if (my $cue = $_attr1_cue{$class}) {
            my $tbl = _get_sub_syms($class);
            for my $entry (@$cue) {
                $entry->[2] = $tbl->{$entry->[0]};
            }
        }
    }

    sub _get_sub_syms {

        my ($pkg) = @_;
        no strict 'refs';
        my $out = {};
        my $sym_tbl = %{"$pkg::"};
        for my $key (keys %$sym_tbl) {
            if (exists &{$sym_tbl->{$key}}) {
                $out->{&{$sym_tbl->{$key}}} = $key;
            }
        }
        return $out;
    }

    sub _do_attr1_specific_process {
        my $class = shift;
        for my $cue (@{$_attr1_cue{$class}}) {
            my ($ref, $data, $sym) = @{$cue};
            # do something
        }
    }

これで例外なく動くのかどうかまだ自信ないけど、今のところは問題なさげ。このやり方はnewコンストラクタが必ず、かつ自動的に呼ばれるようなケースでしか使えそうもない。それ以外の場合はアトリビュート自体使うべきでないのかもしれない。

Perl製の小悪魔テンプレートエンジン「Text::PSTemplate」

 

小悪魔はうそです。Perl製のテンプレートエンジンを再発明しました。「Text::PSTemplate」です。

ひたすら変数や関数を書き込むのがメインのテンプレートエンジンです。ore_blog_ore_format_list1(category => 'fuck')みたいなアプリ固有の関数を作って、制御構造はそっちでやるのが手っ取り早いと思ってます。とはいえ、コアプラグインってものがあって、if文、each文、switch文、if_in_array文などの制御構文、ファイルのinclude、Django風テンプレート継承などの機能も使えます。あと、PHPのテンプレートエンジンDwooの機能を少しだけ移植してみたサンプルプラグインもあります。

PurePerlで依存モジュールもたぶんClass::C3くらいです依存モジュール特になし。少なくともPerl5.8.8以降では動きます。5年前から使ってますがアルファバージョンです。APIは変更するかもしれません。POD書きかけ。

以下、テンプレートの書式。

Masonっぽいタグが基本なので、エディタのMasonモードで開くといい感じにハイライトしてくれます。なお、デリミタは変更可能です。

<% ... %>

変数。

<% $var %>

関数。

<% html_escape($var) %>

関数の引数はPerlのまんまです。プラグインの設計次第で配列やファットカンマもいけます。

<% your_func($var, 'something') %>
<% your_func(name1 => $var, name2 => 'something') %>

関数は入れ子にできます。内側には&が必要。

<% your_func(&your_func($var)) %>

if文。関数はブロック内でも使え、外側のスコープの変数は継承され、内側から参照できます。

<% if_equals($var, 1)<<THEN,ELSE %>
    <% $var %> is 1.
<% THEN %>
    <% your_func($var2) %>
<% ELSE %>

構文と言っても、中身は単なる関数です。関数内でタグに後続するブロックを取得するAPIがあるので、それを使用すれば何となく制御構文っぽい感じに見えます。ちなみにブロックの名前は処理内容には無関係で、出現順だけが意味を持ちます。今のところ。

<% your_control($var)<<FOO,BAR %>
    block argument1
<% FOO %>
    block argument2
<% BAR %>

ファイル挿入。includeは入れ子にでき、例によって変数は継承されます。

<% include('path/to/file.txt') %>

テンプレート内に書かれたパス名はデフォルトではそのままPerlのopenに渡されますが、ファイル名の整形のためのコールバックをロジック側で指定することができるので、例えば、常に現在のテンプレートからの相対パスで指定できるようにしたり、基底ディレクトリを指定したり、.htmlは省略可にしたり、予めファイルの有無をチェックしたり、などできます。

テンプレート継承構文。Djangoのドキュメントを真似た例です。

こちらがbase.html

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
    <link rel="stylesheet" href="style.css" />
    <title><% placeholder('title')<<DEFAULT %>My amazing site<% DEFAULT %></title>
</head>

<body>
    <div id="sidebar">
        <% placeholder('sidebar')<<DEFAULT %>
        <ul>
            <li><a href="/">Home</a></li>
            <li><a href="/blog/">Blog</a></li>
        </ul>
        <% DEFAULT %>
    </div>

    <div id="content">
        <% placeholder('content')<<DEFAULT %><% DEFAULT %>
    </div>
</body>
</html>

extends構文で継承します。

<% extends('base.html')<<EXTENDS %>
    <% block('title')<<BLOCK %>My amazing blog<% BLOCK %>
    <% block('content')<<BLOCK %>
    <% each($blog_entries, 'entry')<<ENTRIES %>
        <h2><% $entry->{title} %></h2>
        <p><% $entry->{body} %></p>
    <% ENTRIES %>
    <% BLOCK %>
<% EXTENDS %>

下記はif_equals文を含むプラグインの実装例です。引数とブロック指定の2ウェイのインターフェースです。

package SomeModule;
use strict;
use warnings;
use base qw(Text::PSTemplate::PluginBase);
use Text::PSTemplate;

    sub if_equals : TplExport {

        my ($self, $target, $value, $then, $else) = @_;

        my $tpl = Text::PSTemplate->new;

        if ($target eq $value) {
            if ($then) {
                return $then;
            } elsif (my $inline = Text::PSTemplate::inline_data(0)) {
                return $tpl->parse($inline);
            }
        } else {
            if ($else) {
                return $else;
            } elsif (my $inline = Text::PSTemplate::inline_data(1)) {
                return $tpl->parse($inline);
            }
        }
        return;
    }

PluginBaseを継承するとTplExportアトリビュートを指定できるようになります。TplExportなサブルーチンはテンプレート関数と一対一に対応します。if文などの制御構文も実際はサブルーチンなので同じです。また、PluginBaseは同梱のClass::FileCacheable::Liteというクラスを継承していて、関数毎にファイルキャッシュをすることもできます。db_record_listなんて関数を作ったらFileCacheableアトリビュートを付与するとよいです。

このプラグインは下記のようにアクティベートできます。

use Text::PSTemplate::Plugable;
use SomePlugin; # 必要なくなった

my $tpl = Text::PSTemplate::Plugable->new;
$tpl->plug('SomePlugin',''); # 第二引数で名前空間を指定できます
my $parsed = $tpl->parse_file('path/to/file');

基底クラスのText::PSTemplateを継承したPlugableがプラグイン機能を拡張した使いやすいクラスなので、通常これを使います。

SQL::OOPなるPerlモジュールです

SQL::OOPです。Perlです。

ORマッパーとか難しくてよくわかりません。SQL::Abstractの書式が難しすぎて覚えれません。OOPスタイルのSQLジェネレータも色々あるようですが、あまり気に入ったものがなかった。

SQL::OOPはオブジェクト指向なインターフェースでSQL::Abstractライクな結果を得るものです。簡単なことは難しく、難しいことは簡単にできます。ここ1年くらいでいくつかのプロジェクトで使ってるけど、今のところ問題なさげ。

詳しくはpodを参照ください。

例1

my $select = SQL::OOP::Select->new();
$select->set(
    $select->ARG_FIELDS => '*',
    $select->ARG_FROM   => 'table',
    $select->ARG_WHERE  => sub {
        my $where = SQL::OOP::Where->new;
        return $where->and(
            $where->cmp('=', 'a', 1),
            $where->cmp('=', 'b', 1),
        )
    },
);

こうなります。改行とインデントはフェイクです。

SELECT
    *
FROM
    table
WHERE
    "a" = ? AND "b" = ?

例2

こんな使い方しないってもの含めて、結構何でもできます。

my $select = SQL::OOP::Select->new();
$select->set(
    $select->ARG_FIELDS => SQL::OOP->new(q{"ky1", "ky2", *}),
    $select->ARG_FROM   => q("tbl1", "tbl2", "tbl3"),
    $select->ARG_WHERE  => sub {
        my $where = SQL::OOP::Where->new();
        return $where->and(
            $where->cmp('>=', 'hoge1', 'hoge1'),
            $where->cmp('=', 'hoge2', 'hoge2'),
            $where->or(
                $where->cmp('=', 'hoge3', 'hoge3'),
                $where->cmp('=', 'hoge4', 'hoge4'),
                $where->between('price', 10, 20),
                $where->is_null('vprice'),
                SQL::OOP->new('a = b'),
                'a = b',
                SQL::OOP->new('c = ? ?', ['code1', 'code2']),
                $where->between('price', 10, 20),
            ),
            $where->or(
                $where->cmp('=', 'hoge3', undef),
                $where->cmp('=', 'hoge4', undef),
            ),
        )
    },
    $select->ARG_ORDERBY => SQL::OOP::Order->abstract([['hoge1', 1], ['hoge2']]),
    $select->ARG_LIMIT  => 11315,
    $select->ARG_OFFSET => 1,
);

こうなります。

SELECT
    "ky1", "ky2", *
FROM
    "tbl1", "tbl2", "tbl3"
WHERE
    "hoge1" >= ?
    AND
    "hoge2" = ?
    AND
    (
        "hoge3" = ?
        OR
        "hoge4" = ?
        OR
        "price" BETWEEN ? AND ?
        OR
        "vprice" IS NULL
        OR
        a = b
        OR
        a = b
        OR
        c = ? ?
        OR
        "price"
        BETWEEN ? AND ?
    )
ORDER BY
    "hoge1" DESC, "hoge2"
LIMIT
    11315
OFFSET
    1

ところで、setメソッドなどの引数が変態的な書式になってますが、これはタイポをコンパイルエラーにさせるのと、入力補完するためです。

jquery.jsontpl.jsというテンプレートエンジンだよ

jsonを使用したテンプレートエンジン「jquery.jsontpl.js」です。jQueryバージョン1.5以上で動作します。

特徴は、テンプレートのパースに正規表現置換やevalなどせず、jQueryの高レベルAPIのみを使用している点です。テンプレート内ではほぼ普通にjavascriptが使えますので、つまりテンプレート内で条件分岐、反復制御ができます。テンプレートは入れ子にもできます。よくある特殊制御構文とか特にありませんので、学習コストが低いかも知れません。

アルファバージョンです。数日でテキトーに作ったので、コンセプトすら正しいのか不明です。

[2011.01.29更新]

デモサイトを作りました[デモ]。あと、IE6でも動作するよう修正しました。正規表現つかっちまった。


テンプレートは下記のように使用します。

$.jsontpl.parse('./tpl.html','./data.json').appendTo(selector);

テンプレート内に下記のようなタグを記述すると変数名nameと解釈され、値に置換されます。

<var name="name"></var>

jsonデータは連想配列の配列として渡します。連想配列のキーがテンプレート変数名です。

[{"name":value}, {"name":value2}, {"name":value3}]

jQuery.jsontpl.getContextメソッドにてコンテキストオブジェクトcontextを取得することができます。コンテキストというのは要するに現在のテンプレートの範囲ということです。contextオブジェクトから、現在のコンテキストでの変数を取得したり、DOM操作をしたりできます。

// コンテキストオブジェクトを取得
var context = $.jsontpl.getContext();

// 変数取得
var name = context.getvar("name");

テンプレート内のdomにはid属性を指定してスクリプトでパース時に制御することができます。テンプレート内のid属性は、衝突を避けるためパース後に削除されます。テンプレート、つまり複製されるものなので、適用後にidが必要になるはずはないという訳です。別にclassでも構いませんが。

context.find("#flag")

変数スコープには注意が必要です。グローバル空間で衝突しないように下記のように記述するとよいです。

<script type="text/javascript">
//<![CDATA[
(function() {
    var context = $.jsontpl.getContext();
})();
//]]>
</script>

jsontplプラグインはjQuery.jsontpl名前空間を占有します。jQuery.jsontplはjQuery.subclassそのものなので、jQuery(selector)とjQuery.jsontpl(selector)はよく似ています。ただし、下記のメソッドを追加、またはオーバーライドしています。詳しくはソースを参照してください。

jQuery.jsontpl.ajax
jQuery.jsontpl.getTpl
jQuery.jsontpl.getJSON
jQuery.jsontpl.get
jQuery.jsontpl.getContext
jQuery.jsontpl.parse
jQuery.jsontpl.fn.getvar
jQuery.jsontpl.fn.parse
jQuery.jsontpl.fn.appendTo
jQuery.jsontpl.fn.prependTo
jQuery.jsontpl.fn.insertBefore
jQuery.jsontpl.fn.insertAfter

使用例

data.json

[
    {
        "name": "Yamada",
        "mail": "yamada@example.com",
        "blood": "A"
    },
    {
        "name": "Yamamoto",
        "mail": "yamamoto@example.com",
        "blood": "A"
    },
    {
        "name": "Suzuki",
        "mail": "suzuki@example.com",
        "blood": "B",
        "detail":{
            "data1":"hoge",
            "data2":"hoge",
            "data3":"hoge"
        }
    }
]

index.html

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja">
<head>
    <title>jQuery Template</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta http-equiv="Content-Script-Type" content="text/javascript" />
    <style type="text/css">
    table,th,td {
        border:1px solid #ccc;
    }
    th {
        background-color: #e0e0f0;
    }
    </style>
    <script src="./common/js/jquery.1.5.js" type="text/javascript"></script>
    <script src="./common/js/jquery.jsontpl.js" type="text/javascript"></script>
    <script type="text/javascript">
    //<![CDATA[
        $(document).ready(function(){
            // テンプレートとJSONをサーバーから取得
            $.jsontpl.parse('./tpl.html','./data.json').appendTo('.movie');
        });
    //]]>
    </script>
</head>

<body>
    <table class="movie">
        <tr>
            <th>名前</th>
            <th>メール</td>
            <th>血液型</td>
            <th>A型</th>
            <th>詳細情報</th>
        </tr>
    </table>
</body>
</html>

tpl.html

<tr>
    <td><var name="name"></var></td>
    <td><var name="mail"></var></td>
    <td><var name="blood"></var></td>
    <td id="flag"></td>
    <td id="subtable" class="subtable">
    </td>
</tr>
<script type="text/javascript">
//<![CDATA[
(function() {
    // コンテキストオブジェクトを取得
    var context = $.jsontpl.getContext();

    // テンプレート内で条件分岐例
    if (context.getvar("blood") == 'A') {
        context.find("#flag").html("true");
    }
    // サブテンプレートを挿入
    $.jsontpl.parse('./tpl2.html', context.getvar("detail")).appendTo('#subtable');
})();
//]]>
</script>

tpl2.html

<div class="hoge">
    data1:<var name="data1"></var>
    <br />data2:<var name="data2"></var>
    <br />data3:<var name="data3"></var>
</div>

jquery.jsontpl.js(最新版はgithubにて)

/*!
 * jquery.jsontpl - client side template engine powered by jQuery
 *
 * @requires jQuery version 1.5 or higher
 * @version 0.0.3
 */
(function ($) {

    $.jsontpl = $.subclass();

    /**
     * プラグイン開発用のデバッグコンソール
     */
    var console = {
        log : (window.console) ? window.console.log : function () {}
    };

    /**
     * デフォルトパラメータ
     */
    $.jsontpl.params = {
        classPrefix : 'jsontpl',
        __trailing__: null
    };

    /**
     * デフォルトパラメータをオーバーライド
     *
     * @param obj {object}
     * @returns {jQuery.jsontpl}
     */
    $.jsontpl.setParams = function (params) {
        $.extend($.jsontpl.params, params);
        return this;
    };

    /**
     * Ajaxキャッシュ{url : data}
     */
    var cache = {};

    /**
     * キャッシュ機構を追加したajaxメソッド
     *
     * @param params {object}
     * @returns {XMLHttpRequest | empty object}
     */
    $.jsontpl.ajax = function (params) {
        if (cache[params.url]) {
            params.success(cache[params.url]);
            return {};
        }
        params.success = (function() {
            var url = params.url;
            var orgSuccess = params.success;
            return function(a) {
                cache[url] = a;
                orgSuccess(a);
            }
        })();
        return $.ajax(params);
    };

    /**
     * テンプレート取得用のAjaxラッパーメソッド
     *
     * @param url {string}
     * @returns {jQuery.jsontpl}
     */
    $.jsontpl.getTpl = function (url) {
        var ret;
        $.jsontpl.ajax({
            type: "GET",
            url: url,
            cache: true,
            async: false,
            error: function (error) {
                console.log(error.statusText + " at " + url);
            },
            success: function (tpl) {
                ret = $.jsontpl(tpl);
            }
        });
        return ret;
    };

    /**
     * JSON取得用のAjaxラッパーメソッド
     *
     * @param url {string}
     * @returns {jQuery.jsontpl}
     */
    $.jsontpl.getJSON = function (url) {
        var ret;
        $.jsontpl.ajax({
            type: "GET",
            url: url,
            cache: true,
            async: false,
            dataType: 'json',
            error: function (error) {
                console.log(error.statusText + " at " + url);
            },
            success: function (json) {
                ret = json;
            }
        });
        return ret;
    };

    /**
     * テンプレートとJSONの取得用のAjaxラッパーメソッド
     *
     * @param tpl {string} テンプレートファイル名
     * @param json {string} JSONファイル名
     * @returns {jQuery.jsontpl} テンプレートとJSON
     */
    $.jsontpl.get = function (tpl, json) {
        return {
            tpl  : $.jsontpl.getTpl(tpl),
            json : $.jsontpl.getJSON(json)
        };
    };

    /**
     * テンプレート内のコンテキストを保持
     */
    var context;

    /**
     * テンプレートから現在のコンテキストを取得するAPI
     * @returns jQueryエレメント
     */
    var getContext = $.jsontpl.getContext = function () {
        return context || $('body');
    }

    /**
     * テンプレート内から現在のコンテキストでの変数を取得するAPI
     * @param name {string} 変数名
     * @returns 値
     */
    $.jsontpl.fn.getvar = function (name) {
        return this.data('jsontpl')['var'][name];
    }

    /**
     * テンプレートパース
     *
     * @param tpl {jQuery element | string} jQueryエレメントまたはファイル名
     * @param json {object | string} JSONオブジェクトまたはファイル名
     * @returns {jQuery element}
     */
    var _parse =  function (tpl, json) {
        if (typeof tpl === 'string') {
            tpl = $.jsontpl.getTpl(tpl);
        }
        if (typeof json === 'string') {
            json = $.jsontpl.getJSON(json);
        } else if ((json && json.constructor !== Array)) {
            json = [json];
        }

        var ret = $.jsontpl('<div />');

        for (var idx in json) {
            var data = json[idx];
            var tpl_tmp = $.jsontpl("<div />");
            tpl_tmp.data("jsontpl", {'var':data});
            var current_context = context;
            context = tpl_tmp;
            tpl_tmp.html(tpl.clone());
            for (var tplvar in data) {
                tpl_tmp.find("[name=" + tplvar + "]").replaceWith(data[tplvar]);
            }
            ret.append(tpl_tmp.children());
            context = current_context;
        }
        ret.find("[id]").removeAttr("id");
        return ret.children();
    };

    /**
     * テンプレートパースAPI
     *
     * @param tpl {jQuery element | string} jQuery element or filename
     * @param json {object | string} object or filename
     * @returns {jQuery element}
     */
    $.jsontpl.parse = _parse;

    /**
     * テンプレートパースAPI
     *
     * @param json {object | string} object or filename
     * @returns {jQuery.jsontpl element}
     */
    $.jsontpl.fn.parse = function (json) {
        return _parse(this, json);
    };

    $.jsontpl.fn.appendTo = function (selector) {
        getContext().find(selector).append(this);
        return this;
    };
    $.jsontpl.fn.prependTo = function (selector) {
        getContext().find(selector).prepend(this);
        return this;
    };
    $.jsontpl.fn.insertBefore = function (selector) {
        getContext().find(selector).before(this);
        return this;
    };
    $.jsontpl.fn.insertAfter = function (selector) {
        getContext().find(selector).after(this);
        return this;
    };

})(jQuery);

 

jQueryテンプレートエンジンを改良してみた

こないだ作ったjquery.jsontpl.jsを改良してみた。

プレースホルダーとして<var class="Foo"></var>などと記述すると変数Fooの値で置換される。または、class=".Foo.jsontpl-var"な任意の要素のinnerHTMLに変数Fooの値が設定される。みたいな仕様。

index.html

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja">
<head>
    <title>jQuery Template</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta http-equiv="Content-Script-Type" content="text/javascript" />
    <script src="./common/js/jquery.1.5.js" type="text/javascript"></script>
    <script src="./common/js/jquery.jsontpl.js" type="text/javascript"></script>
    <script type="text/javascript">
    //<![CDATA[
        $(document).ready(function(){
            // set global params
            jQuery.jsontpl.setParams({classPrefix:'jsontpl'});

            // jQueryエレメントをテンプレートとして使用
            $.jsontpl.parse($('#templates tr'),'./data.json').appendTo('.movie');

            // jQueryエレメントをテンプレートとして使用(別パターン)
            $.jsontpl('#templates tr').parse('./data.json').appendTo('.movie');

            // テンプレートとJSONをサーバーから取得
            $.jsontpl.parse('./tpl.html','./data.json').appendTo('.movie');

            // テンプレートのみサーバーから取得
            var json = {
                "Name": "test1",
                "ReleaseYear": "test2",
                "Director": "test3"
            };
            $.jsontpl.parse('./tpl.html', json).appendTo('.movie');

            // テンプレートとJSONをサーバーから取得
            $.jsontpl.getTpl('./tpl.html').parse('./data.json').appendTo('.movie');
        });
    //]]>
    </script>
</head>

<body>
    <div id="templates" style="display:none;">
        <table>
            <tr><!-- template -->
                <th class="Name jsontpl-var"></th>
                <td class="ReleaseYear jsontpl-var"></td>
                <td class="Director jsontpl-var"></td>
                <td class="conbination">
                    <!-- placeholer not required -->
                    <var class="Name"></var>
                    <var class="ReleaseYear"></var>
                </td>
            </tr>
        </table>
    </div>
    <table class="movie" summary="summary">
    </table>
</body>
</html>

data.json

[
{ "Name": "The Red Violin", "ReleaseYear": "1998", "Director": "Francois Girard" },
{ "Name": "Eyes Wide Shut", "ReleaseYear": "1999", "Director": "Stanley Kubrick" },
{ "Name": "The Inheritance", "ReleaseYear": "1976", "Director": "Mauro Bolognini" }
]

結果

下記のようになります。

<table summary="summary" class="movie">
    <tbody><tr><!-- template -->
                <th class="Name">The Red Violin</th>
                <td class="ReleaseYear">1998</td>
                <td class="Director">Francois Girard</td>
                <td class="conbination">
                    <!-- placeholer not required -->
                    The Red Violin
                    1998
                </td>
            </tr><tr><!-- template -->
                <th class="Name">Eyes Wide Shut</th>
                <td class="ReleaseYear">1999</td>
                <td class="Director">Stanley Kubrick</td>
                <td class="conbination">
                    <!-- placeholer not required -->
                    Eyes Wide Shut
                    1999
                </td>
            </tr><tr><!-- template -->
                <th class="Name">The Inheritance</th>
                <td class="ReleaseYear">1976</td>
                <td class="Director">Mauro Bolognini</td>
                <td class="conbination">
                    <!-- placeholer not required -->
                    The Inheritance
                    1976
                </td>
            </tr><tr><!-- template -->
                <th class="Name">The Red Violin</th>
                <td class="ReleaseYear">1998</td>
                <td class="Director">Francois Girard</td>
                <td class="conbination">
                    <!-- placeholer not required -->
                    The Red Violin
                    1998
                </td>
            </tr><tr><!-- template -->
                <th class="Name">Eyes Wide Shut</th>
                <td class="ReleaseYear">1999</td>
                <td class="Director">Stanley Kubrick</td>
                <td class="conbination">
                    <!-- placeholer not required -->
                    Eyes Wide Shut
                    1999
                </td>
            </tr><tr><!-- template -->
                <th class="Name">The Inheritance</th>
                <td class="ReleaseYear">1976</td>
                <td class="Director">Mauro Bolognini</td>
                <td class="conbination">
                    <!-- placeholer not required -->
                    The Inheritance
                    1976
                </td>
            </tr><tr>
    <th class="Name">The Red Violin</th>
    <td class="ReleaseYear">1998</td>
    <td class="Director">Francois Girard</td>
    <td class="conbination">
        <!-- placeholer not required -->
        The Red Violin
        1998
    </td>
</tr><tr>
    <th class="Name">Eyes Wide Shut</th>
    <td class="ReleaseYear">1999</td>
    <td class="Director">Stanley Kubrick</td>
    <td class="conbination">
        <!-- placeholer not required -->
        Eyes Wide Shut
        1999
    </td>
</tr><tr>
    <th class="Name">The Inheritance</th>
    <td class="ReleaseYear">1976</td>
    <td class="Director">Mauro Bolognini</td>
    <td class="conbination">
        <!-- placeholer not required -->
        The Inheritance
        1976
    </td>
</tr><tr>
    <th class="Name">test1</th>
    <td class="ReleaseYear">test2</td>
    <td class="Director">test3</td>
    <td class="conbination">
        <!-- placeholer not required -->
        test1
        test2
    </td>
</tr><tr>
    <th class="Name">The Red Violin</th>
    <td class="ReleaseYear">1998</td>
    <td class="Director">Francois Girard</td>
    <td class="conbination">
        <!-- placeholer not required -->
        The Red Violin
        1998
    </td>
</tr><tr>
    <th class="Name">Eyes Wide Shut</th>
    <td class="ReleaseYear">1999</td>
    <td class="Director">Stanley Kubrick</td>
    <td class="conbination">
        <!-- placeholer not required -->
        Eyes Wide Shut
        1999
    </td>
</tr><tr>
    <th class="Name">The Inheritance</th>
    <td class="ReleaseYear">1976</td>
    <td class="Director">Mauro Bolognini</td>
    <td class="conbination">
        <!-- placeholer not required -->
        The Inheritance
        1976
    </td>
</tr></tbody></table>

jquery.jsontpl.js

/*!
 * jquery.jsontpl - client side template engine powered by jQuery
 *
 * @requires jQuery version 1.5 or higher
 * @version 0.0.2
 */
(function ($) {

    $.jsontpl = $.subclass();

    /**
     * debug console for plugin develop
     */
    var console = {
        log : (window.console) ? window.console.log : function () {}
    };

    /**
     * default params
     */
    $.jsontpl.params = {
        classPrefix : 'jsontpl',
        __trailing__: null
    };

    /**
     * Override params
     *
     * @param obj {object}
     * @returns {jQuery.jsontpl}
     */
    $.jsontpl.setParams = function (params) {
        $.extend($.jsontpl.params, params);
        return this;
    };

    /**
     * ajax cache {url : data}
     */
    var cache = {};

    /**
     * modified $.ajax for cache mechanism
     *
     * @param params {object}
     * @returns {XMLHttpRequest | empty object}
     */
    $.jsontpl.ajax = function (params) {
        if (cache[params.url]) {
            params.success(cache[params.url]);
            return {};
        }
        params.success = (function() {
            var url = params.url;
            var orgSuccess = params.success;
            return function(a) {
                cache[url] = a;
                orgSuccess(a);
            }
        })();
        return $.ajax(params);
    };

    /**
     * $.ajax wrapper for template get
     *
     * @param url {string}
     * @returns {jQuery.jsontpl}
     */
    $.jsontpl.getTpl = function (url) {
        var ret;
        $.jsontpl.ajax({
            type: "GET",
            url: url,
            cache: true,
            async: false,
            error: function (error) {
                console.log(error.statusText + " at " + url);
            },
            success: function (tpl) {
                ret = $.jsontpl(tpl);
            }
        });
        return ret;
    };

    /**
     * $.ajax wrapper for JSON get
     *
     * @param url {string}
     * @returns {jQuery.jsontpl}
     */
    $.jsontpl.getJSON = function (url) {
        var ret;
        $.jsontpl.ajax({
            type: "GET",
            url: url,
            cache: true,
            async: false,
            dataType: 'json',
            error: function (error) {
                console.log(error.statusText + " at " + url);
            },
            success: function (json) {
                ret = json;
            }
        });
        return ret;
    };

    /**
     * $.ajax wrapper for getting tpl & json
     *
     * @param tpl {string} template filename
     * @param json {string} json filename
     * @returns {jQuery.jsontpl} template & json
     */
    $.jsontpl.get = function (tpl, json) {
        return {
            tpl  : $.jsontpl.getTpl(tpl),
            json : $.jsontpl.getJSON(json)
        };
    };

    /**
     * Parse template
     *
     * @param tpl {jQuery element | string} jQuery element or filename
     * @param json {object | string} object or filename
     * @returns {jQuery element}
     */
    var _parse =  function (tpl, json) {
        if (typeof tpl === 'string') {
            tpl = $.jsontpl.getTpl(tpl);
        }
        if (typeof json === 'string') {
            json = $.jsontpl.getJSON(json);
        } else if ((json.constructor !== Array)) {
            json = [json];
        }
        var ret = $('<div></div>');
        for (var obj in json) {
            var a = tpl.clone();
            for (var tplvar in json[obj]) {

                // replace <var>
                a.find("var." + tplvar).after(json[obj][tplvar]).remove();

                // replace innerHTML of class *.placeholder
                var placeholder = $.jsontpl.params.classPrefix + '-var';
                a.find("." + placeholder + "." + tplvar)
                    .html(json[obj][tplvar])
                    .removeClass(placeholder);
            }
            ret.append(a);
        }
        return $.jsontpl(ret.html());
    };

    /**
     * Parse template
     *
     * @param tpl {jQuery element | string} jQuery element or filename
     * @param json {object | string} object or filename
     * @returns {jQuery element}
     */
    $.jsontpl.parse = _parse;

    /**
     * Parse template
     *
     * @param json {object | string} object or filename
     * @returns {jQuery.jsontpl element}
     */
    $.jsontpl.fn.parse = function (json) {
        return _parse(this, json);
    };
})(jQuery);