さて前回はテンプレートエンジンの前史のようなレベルの話だったのですが、もう少しましなレベルにしましょう。
テンプレートエンジンを使用するということは、二つの側面があります。ひとつはプログラマが楽をすること(=生産性をあげること)、もうひとつは、サーバーの負荷を下げることです。
RubyのテンプレートエンジンのeRBや、PHPのテンプレートエンジンのSmartyのような本格的なテンプレートエンジンは、テンプレートをRubyやPHPのソースコードにコンパイルします。最近のWebサーバーでは、一度使用したプログラムを中間コードの状態で保持しておいて、実行する機能がありますので、よく使われるサービスであれば、サーバーの負荷を下げることができます。
個人的なツールであれば、生産性を上げるメリットのほうが大きくなります。
PHPのSmartyは良くできたテンプレートエンジンなのですが、開発者の生産性を上げるという側面から見ると、いくつか不満もあります。
(1) コントロールを生成する手段が弱い
MVC構造(モデル・ビュー・コントロール)の分離ということを考えるなら、入力部品の記述は本来テンプレート側に記述するべきなのだが、うまく記述できない。結果として、コントロール側のコードに書かざるを得ない。デザインとプログラムを分離できなくなっている。
(2) 拡張が簡単に書けない
一応insert構文というものがあり、指定した名称のPHP側のコードを呼び出してはくれるが、あまり使い勝手は良くない。出来ればクラス化するなどで拡張性のできる仕組みにしたい。
そこで、勉強をかねて自分で作ってみます。
個人で片手間に作るのですから、コード量を減らす必要があります。
Rubyは、レンタルサーバーで使えるところが増えてきていますが、まだまだです。その点、Perlはどこでも使えるというメリットがあります。もっともCPANに山のように、いいテンプレートエンジンがありますが、そこは勉強と割り切りましょう。
さて、作成に関して、いくつかの指針を決めましょう。
(1) 完璧を求めない
完璧を求めると、正規表現でちょこちょこと簡易パーサーを書くというようなことにはいかなります。あくまでも自分が相対的に楽をするためのものと割り切ります。
(2) コード側に極力HTMLの記述をしない
テンプレートエンジンの内部にHTMLコードが存在するのは、仕方が無いことです。一方本来HTML側に記述するようなstyleとかclassなどのHTMLのパラメータを、極力プログラム側で書かない。少なくともコントロールする必要がなければ、コード側で書かない。
(3) 簡単に拡張できるような仕組みを持つ
必要に応じて簡単に拡張できる仕組みをもつ。テンプレートエンジン自身は全体の9割までの必要を満たせばいいと割り切り、コードサイズを小さくします。コードサイズが小さければ、クラスを拡張するときなど、ちょっとソースコードを参照して、追加したり手直ししたりが出来ます。
[セクション]
ベースとなる考え方ですが、HTMLライクなテンプレートを読み込んで、必要な埋め込みを行ってそのまま、出力するという仕組みにします。テンプレート側に、制御構造を持つとプログラムが大変になるので、埋め込みデータで制御をするようにします。
前回の例で示したテンプレートのサブルーチンでは、制御を完全にプログラムで行うようにしていました。これではHTMLの形を変えると、プログラムで制御しないとなりません。たとえば、ループ構造が複数回出てくる形式で、出現順番が変われば、プログラム側も修正しないとなりません。
デザインとプログラムの分離ということでは、不完全すぎる仕様です。
幸いHTML自身は入れ子構造になっている仕組みなので、ブロックの単位も入れ子構造をとれば、都合がいいはずです。
前のテンプレートのサブルーチンでは、セクションは、単に連続して並んでいましたが、ここでは入れ子構造にします。
セクションの開始
セクションの終了
+— section A 開始
| 記述
| +– SECTION B 開始
| | 記述
| +– SECTION B 終了
| 記述
|—- section A 終了
一番外側のセクションですが、無名というもの使いづらいので、MAINというセクション名を割り当てています。
テンプレートエンジンは、読込を行った時点で新しいセクションがあれば、その位置にセクション用の埋め込みタグを埋め込みます。レンダリングするさいは、その記号を目印に置き換えます。
[埋め込み部品]
埋め込み部品とは、HTMLの<INPUT xxx>などのコントロール部品です。これらは、プログラム側で制御が必要な上に、テンプレート側にもclassやstyleなどのパラメータが必要な厄介な代物です。
<#{部品種別} {名称} [{パラメータ}={値}….]#>
名称で示された値をキーに、テンプレート側のパラメータとプログラムから渡されたハッシュ値を元に、コントロールを生成します。このコントロールの生成部分は、クラスメソッドになっており、変更や追加が簡単にできます。
以下のような部品を用意しました。
input | テキストボックス |
password | パスワードテキストボックス |
hidden | 隠しテキスト |
select | セレクトボックス |
textarea | テキストエリア |
check | チェックボックス |
radio | ラジオボタン |
ファイルアップロードやイメージボタンなどは、まだ作成していません。
[表示部品]
この部分は以前のテンプレートサブルーチンをそのまま踏襲しています。しかし使い勝手を考えると、Smartyのような修飾子が欲しいところです。ここについては、今後の課題にしたいです。
##{表示名称}##
さて、テンプレートを見てみましょう。
<html xmlns="https://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=ShiftJIS" /> <meta http-equiv="Content-Script-Type" content="text/javascript" /> <meta http-equiv="Content-Style-Type" content="text/css" /> </head> <body> <table border=1> <tr> <th width=240>商品名</th> <th width=100>単価</th> <th width=60>個数</th> <th width=100>小計</th> </tr> <!--#LOOP--> <tr> <td align="left">##s_name##</th> <td align="right">##n_unit##</th> <td align="right">##n_count##</th> <td align="right">##n_sum##</th> </tr> <!--##--> </table> <#input tx_loginid size="10"#> <#password tx_loginpw size="10"#> <br> <#select sel_kind style="width: 200px"#><br> <#textarea tx_memo cols="40" rows="10"#><br> <#check b_upload #>同時更新<br> <#button btnOpen value="開く" style="width: 100px;"#> <#submit btnSubmit value="送信" style="width: 100px;"#> </body> </html>
いたってシンプルな構造です。
# # my $stdcodepage = "cp932"; use lib '.'; use strict; use English; use Encode; use encoding "utf8", STDOUT=>"cp932", STDIN=>"cp932"; use utf8; use HtmlTemplate; BEGIN { my $encode = "cp932"; binmode STDERR, ":encoding($encode)"; # 標準エラーもcp932に変更 } #======================================================================= # sub showMain() { my $objTpl = HtmlTemplate::open( "test.tpl", "cp932" ); my %hash = ( "#LOOP" => [ { "s_name" => "りんご", "n_unit" => 100, "n_count" => 5, "n_sum" => 500 }, { "s_name" => "みかん", "n_unit" => 70, "n_count" => 4, "n_sum" => 280 }, { "s_name" => "梨", "n_unit" => 120, "n_count" => 3, "n_sum" => 360 }, ], "b_upload" => { "value" => "123", }, "tx_memo" => { "value" => "これが現実というものだよ。ヤマトの諸君。\\nガミラスで会おう去らばだ" }, "sel_kind" => { "mode" => 1, "list" => [ 1, "Perl", 2, "Ruby", 3, "Java", 4, "C++" ], "value" => 2, }, "tx_loginid" => { "value" => "bitop10", }, "tx_loginpw" => { "value" => "******", }, ); print $objTpl->render( \%hash ); }
コードのほうもデータの構造はちょっと複雑ですが、プログラム的にはいたってシンプルな構造になっています。
#LOOPというキーに紐付いたハッシュの参照があります。この参照先が偽であれば、LOOPで宣言されたセクションは展開しません。スカラー値で真であれば、空のハッシュで展開します。ハッシュへのリファレンスであれば、ハッシュ値を渡して展開します。配列への参照であれば、その配列の個数分だけ繰り返し展開を行います。
テンプレートエンジンは、こちらを参照してください。
renderをデータのパラメータを渡して展開します。renderLoopとrenderHashで実際のレンダリングをしています。
callFuncは、埋め込み部品を生成する部分の大本の関数です。ここで、各埋め込み部品の生成関数を呼び出しています。
html_xxxxxシリーズの関数は、埋め込み部品の関数です。
現在のところ300行程度です。エラーチェックなどが最低限なので、本格的な開発にはちょっと不安ですが、とりあえず勉強用ベースとしては十分でないでしょうか
結果はこんな感じになります。
yna