HTMLテンプレート作成のツボ

はじめに

Webアプリケーション開発において、テンプレートの作成はプログラミングと同等にセンスを問われる作業です。

「たかがテンプレート」と侮っていると、何処からリファクタリングすれば良いのか頭を抱えるようなテンプレートを量産しかねません。 正しい知識を身に着ける事で、理想的なテンプレートを自然に書けるようになりましょう。

この記事ではサンプルコードとしてPerl + Sledge + Template Toolkitを用いていますが、 記事で言及している内容は、PHP + Smarty等ほぼ全ての言語・テンプレートエンジンにも共通しております。

それでは4種類のアンチパターンを例に挙げて、リファクタリング方法を解説していきます。

目次

  1. テンプレートエンジンへの変数の割り付け方
  2. 修飾子の使い方
  3. ループ処理
  4. 冗長なテンプレート

1. テンプレートエンジンへの変数の割り付け方

1-1. メソッド実行回数が無駄に多い

複数の変数をそれぞれバラバラにテンプレートエンジンへ割り付けるのは非効率的です。

一般的なテンプレートエンジンは、複数の変数を一括で割り付ける機能を持っています。 バラバラに割り付けるのはソースコードの可読性を悪くするだけで何一つメリットはありません。

リファクタリング

$self->tmpl->param( name => 'Terui' );
$self->tmpl->param( id   => 'teruism' );
$self->tmpl->param( pref => 'Kanagawa' );

リファクタリング

$self->tmpl->param(
    name => 'Terui',
    id   => 'teruism',
    pref => 'Kanagawa',
);

1-2. 無計画な変数名の割り当て方

nameやidなど、何の名前やIDなのか特定できないような変数名を割り当てるのは事故に繋がります。 テンプレートを作成した時点では問題が無いのかもしれませんが、似たような変数名が追加された場合、どちらが目的の変数なのか区別できなくなります。

下記例では、userという連想配列としてテンプレートエンジンに割り当てるようにリファクタリングしています。

リファクタリング

$self->tmpl->param(
    name => 'Terui',
    id   => 'teruism',
    pref => 'Kanagawa',
);

リファクタリング

$self->tmpl->param(
    user => {
        name => 'Terui',
        id   => 'teruism',
        pref => 'Kanagawa',
    },
)

1-3. 変数の値による文字列の出し分け

テンプレートを作成していると、ある値に対応する文字列を表示するケースが非常に多い事に気付くでしょう。 そういった場合、値によって条件分岐で表示内容を切り替えるのは非効率的です。

文字列を配列に格納し、割り当てた変数の値を添字として目的の文字列を表示するように実装しましょう。

リファクタリング

$self->tmpl->param(
    pref  => 0,
    pref_name0 => '北海道',
    pref_name1 => '青森県',
    pref_name2 => '岩手県',
);
[% IF pref == 0 %]
  [% pref_name0 %]
[% ELSIF pref == 1 %]
  [% pref_name1 %]
[% ELSE %]
  [% pref_name2 %]
[% END %]

リファクタリング

$self->tmpl->param(
    pref => 0,
    pref_names => [ '北海道', '青森県', '岩手県' ],
);
[% pref_name.$pref %]

2. 修飾子の使い方

2-1. ロジック側でフィルタ処理を実行

Webアプリケーションでは、クロスサイト・スクリプティングなどの攻撃から守るためにデータベースから取得した値をエスケープする事が求められる他、 改行コードを
タグに置換するなど、様々なフィルタ処理を行う必要があります。

但し、フィルタ処理を行うのはロジックではなくテンプレートエンジン側であるべきです。 ロジックで実装してしまうと、テンプレートエンジンに割り当てる変数の数が無尽蔵に増えてしまうだけでなく、 フィルタ処理を忘れた変数がそのままWebブラウザに表示されてしまう可能性があります。

次のサンプルでは、テンプレートエンジン側でHTMLエンティティをエスケープさせるようにリファクタリングしています。

リファクタリング

$self->tmpl->param(
    userid_espaced => encode_entities( $userid, q{ &<>'" } ),
);
<td>
  [% userid_escaped %]
</td>

リファクタリング

$self->tmpl->param(
    userid => $userid,
);
<td>
  [% userid | html %]
</td>

2-2. 「独自の修飾が必要」という理由で修飾処理をロジック側に実装

Webアプリケーションを開発していると「独自の修飾が必要なのでロジック側で実装する」と主張する人が少なからず存在しますが、 絶対にその主張に同意してはいけません。 テンプレートエンジンには自作の修飾子を使用するための仕組みが必ず用意されていますので、ロジック側で修飾する必要は一切無いのです。

次のサンプルでは、変数$useridをMD5ハッシュに変換していますが、 ロジック側では修飾せずにテンプレート側で修飾するようにリファクタリングしています。

リファクタリング

$self->tmpl->param(
    md5_userid => MD5->hexhash( $userid ),
);
<td>[% md5_userid %]

リファクタリング

$self->tmpl->param(
    userid => $userid,
);
[% USE md5 %]
<td>[% userid | md5 %]</td>

3. ループ処理

3-1. イテレータを使わないループ処理(1)

データベースから取得したリストを表示する際、添字となる変数を使ったループ処理をテンプレートに書く必要はありません。 ほぼ全てのテンプレートエンジンには「イテレータ」が実装されていますので、積極的にイテレータを活用しましょう。

次のサンプルでは、変数counterを用いて0から変数item_size-1までの値でループ処理していたものを、 FOREACHでシンプルにリファクタリングしています。

リファクタリング

[% counter = 0 %]
[% WHILE counter < item_size %]
  <td>
    [% items.$counter %]
  </td>
  [% counter = counter + 1 %]
[% END %]

リファクタリング

[% FOREACH item = items %]
  <td>
    [% item %]
  </td>
[% END %]

3-2. イテレータを使わないループ処理(2)

3-1でイテレータの使用を推奨しましたが、「任意の行で何か表示する場合は変数が必要なのでは?」と言う人がいるでしょう。 イテレータには、リストの要素数や現在のインデックスを取得できるメソッドが用意されているだけでなく、 現在のインデックスが最初の要素なのか、最後の要素なのかを知ることができるメソッドが用意されています。

次のサンプルでは、変数counterを使う代わりにイテレータのメソッドを使い、およびを適切な位置に出力するようリファクタリングしています。

リファクタリング

[% counter = 0 %]
[% WHILE counter < item_size %]
  [% IF counter == 0 %]
    <tr>
  [% END %]
  <td>
    [% items.$counter %]
  </td>
  [% IF counter = item_size - 1 %]
    </tr>
  [% END %]
  [% counter = counter + 1 %]
[% END %]
[% UNLESS item_size %]
  商品はありません
[% END %]

リファクタリング

[% FOREACH item = items %]
  [% IF loop.first %]
    <tr>
  [% END %]
  <td>
    [% item %]
  </td>
  [% IF loop.last %]
    </tr>
  [% END %]
[% END %]
[% UNLESS items.size %]
  商品はありません
[% END %]

4. 冗長なテンプレート

4-1. 同一内容を複数出力するために同一の記述を行う

下記は一覧表示ページによくある「前の10件」「次の10件」といったページネーションのサンプルです。 一覧表示の上下にページネーションを表示しているのですが、全く同じ事を2箇所に書くのは冗長であり、 将来的に「両方を修正したつもりが片方しか修正していなかった」といったミスを招いてしまいます。

リファクタリング後は、paginationという名前でページネーションのブロックを宣言し、 リストの上下でブロックの内容を展開しています。

リファクタリング

<div>
  <a href="?page=[% page - 1 %]">前の10件</a> <a href="?page=[% page + 1 %]">次の10件</a>
</div>
<ol>
  [% FOREACH item=items %]
    <li>[% item %]</li>
  [% END %]
</ol>
<div>
  <a href="?page=[% page - 1 %]">前の10件</a> <a href="?page=[% page + 1 %]">次の10件</a>
</div>

リファクタリング

[% BLOCK pagination %]
<div>
  <a href="?page=[% page - 1 %]">前の10件</a> <a href="?page=[% page + 1 %]">次の10件</a>
</div>
[% END %]
[% PROCESS pagination %]
<ol>
  [% FOREACH item=items %]
    <li>[% item %]</li>
  [% END %]
</ol>
[% PROCESS pagination %]

4-2. 複数のページで同一の情報を表示する

4-1で紹介した手法は、同一ページ内に限った事ではありません。 パーツとしてインクルードファイルを用意し、使用したいテンプレートからインクルードする事で自由に使用できます。

リファクタリング後は、テーブル内の文言を変数msgで指定する事で、任意の文字をテーブルの中に出力できるようにしてあります。

リファクタリング

html/top.html

<div>
  <table>
    <tr>
      <th>header</th>
    </tr>
    <tr>
      <td>トップ</td>
    </tr>
  </table>
</div>

html/search.html

<div>
  <table>
    <tr>
      <th>header</th>
    </tr>
    <tr>
      <td>検索</td>
    </tr>
  </table>
</div>

リファクタリング

html/inc/table.inc

[% BLOCK table %]
  <table>
    <tr>
      <th>header</th>
    </tr>
    <tr>
      <td>[% msg | html %]</td>
    </tr>
  </table>
[% END %]

html/top.html

[% INCLUDE table.inc %]
<h1>top</h1>
<div>
[% PROCESS table msg="トップ" %]
</div>

html/search.html

[% INCLUDE table.inc %]
<h1>search</h1>
<div>
[% PROCESS table msg="検索" %]
</div>

最後に

今回紹介したコードは、どれもリファクタリング前と後で全く同じ出力結果を得られますが、そこが非常に重要なポイントです。

プログラミングとは確実な正解の無い仕事です。 それ故に、己の心に嘘をつかず、常に一定以上の速度でクオリティの高いアウトプットをする事が求められています。

エンジニアとしてのプライドを賭けて真摯に取り組めば、嘘などつけないはずです。

この記事があなたの成長や気付きのきっかけになれば幸いです。

[2016/10/16 20:52] 3-2のリファクタリング前後のテンプレートを修正(0件の場合に「商品はありません」と表示)。