本日のお題 Text::XslateでViewを分離
おさらい
前提
- 本記事ではHTML5の生成に限って記述1します。(Text::Xslate自体は汎用のテンプレートエンジンですので、HTML5の生成に限らず様々な範囲で利用できます)
- CGI.pmの中にText::Xslateを理論上盛り込めます2ので、HTML5で動くCGIを作ることを否定しません。
- PlackとText::Xslateは相互に独立したモジュールですので、別にPSGIを書くにあたってText::Xslateを利用ことは必須ではありません。
- デフォルトのテンプレートである
Kolon
で記述3します。 - HTML5自体の解説は省略します。
- 私も入門したてです。ツッコミ大歓迎
Xslateを利用するメリット
- Model及びControllerからほぼ完全にViewを分離できる。プログラマとデザイナーが同一のリポジトリで作業するときなどの分業に最適
- XS製なので、下手な自作テンプレートより早い
- TTなどの従来のテンプレートからの移行も容易い4
- Markdownなどのモダンなマークアップを取り込むことが容易
- Hello, Perl6! 文法がPerl6ライクなのでゆくゆくPerl6に移行するときのコストが下がる
いくつかのメリットは、そのままデメリットかもしれません。
Xslateを利用するデメリット
- XS製なので、インストールできない環境(レンタルサーバーなど)がある
- 文法がPerl6ライクなので、導入時に戸惑うことがある。
- HTML5に関して何か特別なメソッドを有している訳ではない5ので、HTML5に関する知識は必要
ま、やってみましょう。
use strict;
use warnings;
use Carp;
use Plack::App::Path::Router::PSGI;
use Path::Router;
use Plack::Request;
use Encode; # Text::Xslateはデコード済みUTF-8が前提なのでEncodeの出番です。
use Text::Xslate;
use Text::Xslate::Bridge::MultiMarkdown;
# ↑宣言しなくても下記の記述で読み込んでくれるが、plackupの時点でインストールを検知したいので明示する
my $tx = Text::Xslate->new(
module => ['Text::Xslate::Bridge::MultiMarkdown'],
syntax => 'Kolon',
cache => 0,
verbose => 1,
);
use CGI::Session;
my $session = CGI::Session->new( undef, undef, { Directory => './sessions' } ); # ダミーのセッション
my $sid = $session->param('CGISESSID') || undef;
$session->delete(); # ダミーはすぐ消す
$session = CGI::Session->new( undef, $sid, { Directory => './sessions' } ); # 本セッション
if ( $session->is_expired() ) { # 期限切れを消して再発行
$session->delete();
$session = CGI::Session->new( undef, $sid, { Directory => './sessions' } );
}else{
$session->expire('+1d');
}
my $router = Path::Router->new;
$router->add_route( '/' => target => \&root );
$router->add_route( '/env' => target => \&env );
$router->add_route( '/session' => target => \&session );
$router->add_route( '/:action/:id' =>
validations => {
action => qr/^\w[\w\-]{0,9}$/,
id => qr/^\d{1,10}$/,
},
target => \&action
);
# now create the Plack app
my $app = Plack::App::Path::Router::PSGI->new( router => $router );
$app->to_app();
# 以下routerで呼ばれる要素
sub root {
my $env = shift;
my $title = 'Welcome to ' . $env->{'HTTP_HOST'};
my $render = $tx->render( 'Templates/root.tx', {
title => $title,
description => decode_utf8('ここはルートです。'),
} );
return response( $env, $render );
}
sub action {
my $env = shift;
my ( $action, $id ) = @{ $env->{'plack.router.match.args'} };
my $req = Plack::Request->new($env);
my $method = $req->method;
if( $method =~ /^(:?GET|HEAD)$/s ) {
my $render = $tx->render( 'Templates/action.tx', {
title => decode_utf8('動的ルーティングしてみる'),
action => $action,
id => $id,
method => $method,
} );
return response( $env, $render );
}elsif( $method eq 'POST' ){
$action = $req->body_parameters->{'action'};
$id = $req->body_parameters->{'id'};
my $render = $tx->render( 'Templates/action.tx', {
title => decode_utf8('動的ルーティングしてみる'),
action => $action,
id => $id,
method => $method,
} );
return response( $env, $render );
}
my $render = $tx->render( 'Templates/405.tx', {
title => decode_utf8('HTTPメソッドエラー'),
action => $action,
id => $id,
method => $method,
} );
return response( $env, $render, -status => 405 );
}
sub session {
my $env = shift;
my $req = Plack::Request->new($env);
my %param = %{ $req->body_parameters };
if( exists $param{'action'} and $param{'action'} eq 'forget' ) {
$session->param( 'str', '' );
}elsif( exists $param{'str'} ){
$session->param( 'str', $param{'str'} );
}
my $render = $tx->render( 'Templates/session.tx', {
title => decode_utf8('セッション管理してみる'),
str => $session->param('str') || '',
} );
return response( $env, $render );
}
sub env {
my $env = shift;
my @str;
while ( my ( $key, $value ) = each %$env ) {
push @str, "$key = $value" unless ref $value;
}
my $render = $tx->render( 'Templates/env.tx', {
title => decode_utf8('環境変数一覧'),
list => [sort @str],
} );
return response( $env, $render );
}
# サブルーチン
sub response {
my $env = shift;
my $body = encode_utf8(shift) || croak 'empty body!';
my %ARG = @_ if @_;
my $status = $ARG{'-status'} || 200;
croak "unvalid status: $status" if $status !~ /^\d{3}$/s;
my $mime = $ARG{'-MIME'} || 'text/html; charset=utf-8';
my $headers = $ARG{'-headers'} || {};
my $req = Plack::Request->new($env);
my $res = $req->new_response($status);
$res->content_length( length $body );
$res->content_type($mime);
$res->header(
'Set-Cookie' => $session->cookie,
%$headers
);
$res->body($body) unless $req->method eq 'HEAD';
$res->finalize;
}
スッキリしましたね!以下が必要なテンプレートファイルです。Templates/
に保存してください。
<!DOCTYPE html>
<html lang="ja-jp">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
: if $description {
<meta name="description" content="<: $description :>">
: }
: if $noindex {
<meta name="robots" content="noindex,nofollow,noarchive,noodp,noydir">
: }
<title><:$title:></title>
</head>
<body>
<div class="container">
<h1><: $title :></h1>
<hr>
: block main -> {}
<hr>
<footer class="col-sm-12">
<ul class="list-inline">
<li><a href="/">トップへ戻る</a></li>
<li><a href="/env">環境変数一覧へ</a></li>
<li><a href="/session">セッションのテストへ</a></li>
<li><a href="/action/1234567890">動的ルーティングのテストへ</a></li>
</ul>
</footer>
</div>
</body></html>
本記事の範疇を超えるので解説しませんが、cssやjsの追加、及びOGPなどの記載をしたいときはbaseをいじる必要がありますが、基本的にbaseは不変です。footerを別ファイルにしてコンテンツに追加があった場合もいじらなくていいようにすることはできます。
: cascade Templates::base
: around main -> {
<p>指定されたメソッドが不正です。method:<: $method :></p>
: }
基本的な継承は上記のように行います。
: cascade Templates::base
: around main -> {
<: markdown('
- [環境変数一覧へ](/env)
- [セッションのテストへ](/session)
- [動的ルーティングのテストへ](/action/1234567890)
') | mark_raw :>
: }
普通こういう書き方はしません6が、他に入れるところが思い当たらなかったのでmarkdownをここに入れました。
単にmarkdownを挿入するとXslateはhtmlエスケープをしてしまうので、パイプ演算子でmarkdown('*some text*') | mark_raw
とする必要があるところだけが味噌。
: cascade Templates::base
: around main -> {
<p>
: for $list -> $item {
<: $item :><br>
:}
</p>
: }
配列に基づく繰り返し構文などは上記です。
: cascade Templates::base
: around main -> {
: if ( $method == 'POST' ) {
<p><a href="/<: $action~'/'~$id :>">アクセスしてみる</a></p>
: }
<form method="POST" action="/<: $action~'/'~$id :>" enctype="multipart/form-data"><p>
指定されたmethod:<: $method :><br>
指定されたaction:<input type="text" name="action" value="<: $action :>">任意の英数字<br>
指定されたID:<input type="text" name="id" value="<: $id :>">10桁までの整数<br>
<button type="submit">変更する</button>
</p></form>
: }
Perl6での文字列の連結は.
ではなく~
で行います。
以下、特に補足すべき注意事項はありませんが、デモを走らせるために必要です。
: cascade Templates::base
: around main -> {
<form method="POST" action="/session" enctype="multipart/form-data"><p>
<input type="text" name="str" value="<: $str :>">
<button type="submit" name="action" value="remember">記憶</button>
<button type="submit" name="action" value="forget">消去</button>
</p></form>
: }
## Kolonの文法
- 関数で始まる行は
:
で始めます。 - HTML中に関数などを埋め込みたい場合は
<: function(some) :>
を利用します。 - コメントは行頭を
:#
とします。
## テンプレートへの値の渡し方
my $render = $tx->render( 'Templates/*.tx', {
title => decode_utf8('UTF-8な文字列はデコード必須'),
list => $arrayref || \@array,
hash => $hashref || \%hash,
str => $str,
} );
- ここにきて
decode_utf8()
が必須7になります。 - 返り値はデコード済みUTF-8ですので
encode_utf8()
処理が表示までに必要です。 - 配列やハッシュなどは参照渡しをする必要があります。
**ね、簡単でしょう?**あなたも始めよう!PSGI!
次章
PSGI入門第五章 Log::Dispatch::Configでデバッグログを取る
宣伝
本記事は自前で有料WebサービスをPerlで書いてリリースするに辺り、つまづいた点や気づいたことを共有する目的で書き始めました。
こちらのQRコードをスマホで読み取り、
興味を持った方はhttps://qrown.meまでぜひお立ち寄りください!