2011.06.23
yna 開発

テンプレートエンジンの話(2)

 さて前回はテンプレートエンジンの前史のようなレベルの話だったのですが、もう少しましなレベルにしましょう。
 テンプレートエンジンを使用するということは、二つの側面があります。ひとつはプログラマが楽をすること(=生産性をあげること)、もうひとつは、サーバーの負荷を下げることです。
 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

一覧に戻る