3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

[Perl]PSGI入門第四章 Text::XslateでViewを分離

Last updated at Posted at 2017-02-16

本日のお題 Text::XslateでViewを分離

おさらい

  1. PSGI入門第一章 環境変数の一覧を出す
  2. PSGI入門第二章 CGI::SessionをPSGIで無理やり使う
  3. PSGI入門第三章 動的ルーティングを実装する

前提

  • 本記事では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に関する知識は必要

ま、やってみましょう。

Myapp.pl
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/に保存してください。

Templates/base.tx
<!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を別ファイルにしてコンテンツに追加があった場合もいじらなくていいようにすることはできます。

Templates/405.tx
: cascade Templates::base
: around main -> {
<p>指定されたメソッドが不正です。method:<: $method :></p>
: }

基本的な継承は上記のように行います。

Templates/root.tx
: cascade Templates::base
: around main -> {
<: markdown('
- [環境変数一覧へ](/env)
- [セッションのテストへ](/session)
- [動的ルーティングのテストへ](/action/1234567890)
') | mark_raw :>
: }

普通こういう書き方はしません6が、他に入れるところが思い当たらなかったのでmarkdownをここに入れました。
単にmarkdownを挿入するとXslateはhtmlエスケープをしてしまうので、パイプ演算子でmarkdown('*some text*') | mark_rawとする必要があるところだけが味噌。

Templates/env.tx
: cascade Templates::base
: around main -> {
<p>
: for $list -> $item {
    <: $item :><br>
:}
</p>
: }

配列に基づく繰り返し構文などは上記です。

Templates/action.tx
: 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での文字列の連結は.ではなく~で行います。

以下、特に補足すべき注意事項はありませんが、デモを走らせるために必要です。

Templates/session.tx
: 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で書いてリリースするに辺り、つまづいた点や気づいたことを共有する目的で書き始めました。

worthmine-qiita.pngこちらのQRコードをスマホで読み取り、
興味を持った方はhttps://qrown.meまでぜひお立ち寄りください!

  1. HTML4で書きたいというニーズが見当たらないし、それならCGI.pmで生成したHTMLをPSGIで表示したって構わない。

  2. 私がたどり着いた順番がPlack->Xslateだったので、この可能性は未テストです

  3. おそらくそれが一番文献が多いので

  4. TTを使いたい場合はこちらを参照してください

  5. builderモジュールを探せば良い気はする

  6. 最もアクセスが多いであろうルートは軽い方がいいので、動的記述は少ない方が良い。

  7. use utf8している場合を除く。この場合の検証はしてません。

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?