天然パーマです。

Mojolicious 8-Tips


依然としてPerlのWeb Application Framework=WAFは Mojolicious推し です。ボケてほどの大きなトラフィックを集めるようになったサービスでも使っている実績がありますし、自分で使っていてたまにバージョンアップの互換性で問題が出るくらいで、すっごく困った事が無いので重宝しております。今回は備忘録的な意味も兼ねて、Mojoliciousをある程度使用した時に便利なTips 8個を個人的にまとめてみます。え、何?「Mojoliciousっていったいどういうものなの?」「Mojoliciousって名前を聞いた事があるけれど使った事がないんだけど...」そんな方は9月に開催されるYAPC::Asia 2013で僕が入門チュートリアルな発表をやろうと企んでいるので、そちらへ足を運んでください!ちなみにトークはまだacceptされたわけではないので、以下のページのソーシャルボタンで応援してもらえるといいかもしれません!

さて、これでマーケティングな記事であることが暴露されたのですが8個のTipsを紹介します。



前提

mojoコマンドを以下のように叩いたプロジェクトをベースに解説します。

$ mojo generate app MyApp::Web

つまり Mojolicious::Lite のアプリでは無いです。

1. plackup で起動する

.psgi ファイルをつくって plackup もしくは本番環境ならば starman / starlet で Mojolicious アプリを動かしています。Plack::Middleware::* が使えたりして便利です。こんな感じで .psgi を書けばOK!

use strict;
use FindBin;
use lib "$FindBin::Bin/lib";
use Mojo::Server::PSGI;
use Plack::Builder;
use MyApp::Web;

my $psgi = Mojo::Server::PSGI->new( app => MyApp::Web->new );

builder {
    enable "Runtime";
    $psgi->to_psgi_app;
};

2. ログを出力する

Controller の中でこうすればログが出力されます。ちなみに開発環境でデフォルトだと「log/development.log」に流れます。

$self->app->log->warn('Hello, this is warning text.');

ほらこんな感じ。

[Mon Jun 24 07:32:24 2013] [debug] GET "/".
[Mon Jun 24 07:32:24 2013] [debug] Routing to controller "MyApp::Web::Example" and action "welcome".
[Mon Jun 24 07:32:24 2013] [warn] Hello, this is warning text.
[Mon Jun 24 07:32:24 2013] [debug] Rendering template "example/welcome.html.ep".
[Mon Jun 24 07:32:24 2013] [debug] Rendering template "layouts/default.html.ep".

Mojo::Log のPODを見れば分かる通りログレベルには以下があります。

  • debug
  • info
  • warn
  • error
  • fatal

3. helper を活用する

よく使うルーティンを helper メソッドとして生やしておくと何かと役に立ちます。例えば、数字をカンマ付きで表示したい時があります。「10000」という数字だったら「10,000 」になって欲しいですね。今回のプロジェクトの場合ならば「MyApp::Web」のstartupメソッド内で以下のように実装することで「comma」helperメソッドが出来上がります。

$self->helper(
    comma => sub {
        my $text = $_[1];
        $text = reverse $text;
        $text =~ s/(\d\d\d)(?=\d)(?!\d\.)/$1,/g;
        return scalar reverse $text;
    }
);

これはテンプレート内でも使えるので、

<%= comma(10000) %>

と記述すれば、レンダリング後「10,000」と表示されます。

4. DELETE/PUTメソッドに対応させる

Mojoliciousのルータ記述は「DELETE」「PUT」メソッドにも対応しています。つまり

$r->get('/entry/:post_id')->to('entry#show');
$r->delete('/entry/:post_id')->to('entry#delete');

なんて書けるのです。ただ、ご存知の通りブラウザがDELETE/PUTに対応していないケースがほとんど。そこでPUTメソッドのリクエストでかつ「_method」パラメータに指定するメソッド名が入っていた場合にDELETEメソッドなどを使うっていう仕様にしてみると以下のようにhookのbefore_dispatch内でルータのdeleteメソッドが使えるようになります。

$self->hook(
    before_dispatch => sub {
        my $c = shift;
        my $_method = $c->req->param('_method');
        if($c->req->method eq 'POST' && $_method) {
            my $methods = [qw/GET POST PUT DELETE/];
            if ( grep { $_ eq uc $_method } @$methods ) {
                $c->req->method( $_method );
            }
        }
    }
);

5. テンプレートディレクトリパスを追加する

プロジェクト直下の「templates」ディレクトリがテンプレートを置く場所としてデフォルトなのですが、追加することが出来ます。

unshift @{$self->app->renderer->paths},
     File::Spec->catfile(Bokete->base_dir, 'templates/others');

6. モデルへのアクセス

Model は特にMojo::Baseなどは使わずMouseを使って「MyApp::Model::*」な名前空間に置いてあります。さてそれをControllerなどから呼び出す場合なんだけど、これまたhelperを活用しています。と、その前にモデルローダーをつくります。こんな感じ。

package MyApp::Model;
use Mouse;
use Plack::Util;

has 'instances' => (
    traits  => ['Hash'],
    is      => 'rw',
    isa     => 'HashRef',
    default => sub { +{} },
    handles => {
        set_instance => 'set'
    }
);

sub load {
    my ( $self, $name ) = @_;
    my $instance = $self->instances->{$name};
    return $instance if $instance;
    my $class = Plack::Util::load_class( $name, 'MyApp::Model' );
    $instance = $class->new;
    $self->set_instance( $name, $instance );
    return $instance;
}

1;

そしてMyApp::Webのstartupメソッド内にhelperを定義します。

use MyApp::Model;
$self->helper(
    model => sub {
        my ($self, $name) = @_;
        MyApp::Model->new->load($name);
    }
);

これでControllerから

my $entries = $self->model('Entry')->get_entries();

と呼べるし、モデルインスタンスはキャッシュされます。

7. HTMLをキャッシュさせる

あまり変更の無いサイドバー部分などをレンダリングしたHTMLごとmemcachedなどにキャッシュさせておいてそれを都度利用すると速いです。

my $html = $self->render( template => 'template_name', partial => 1 );

Controllerの render メソッドに partial=>1 を渡すとHTMLがそのまま取れます。

if(my $html = $self->cache->get('right_container_html')) {
    $self->stash->{right_container_html} = $html;
}

もしキャッシュにhtmlがあったらstash経由で渡してテンプレートで描画します。

<%== $self->stash->{right_container_html} %>

8. Basic認証をアプリ内で実装する

Basic認証を実現するにはnginxなどのフロントに置いたサーバで処理するか、Plack::Middleware::Auth::Basicを利用する手がありますが、Mojoliciousアプリ内で動的にユーザー名、パスワードを指定したかったりする時がありました。そこでhelperに以下を記述。

$self->helper(
    basic_auth => sub {
        my ($c, $u, $p) = @_;
        my $realm = 'restricted area.';
        my $unauthorized = sub {
            $c->res->headers->www_authenticate("Basic realm=$realm");
            $c->res->code(401);
            $c->res->body('Authorization required');
            return $c->rendered;
        };
        if ( my $auth = $c->req->url->to_abs->userinfo ) {
            my ( $user, $pass ) = split /:/, $auth, 2;
            $pass = '' unless defined $pass;
            $c->req->env->{REMOTE_USER} = $user if $u eq $user && $p eq $pass;
            return;
        }
        return $unauthorized;
    }
);

Controllerではこのようにします。

my $username = 'username';
my $password = 'password';
my $ref = $self->basic_auth($username, $password);
return &$ref() if ref $ref eq 'CODE';

これでBasic認証をMojoliciousアプリ内で実現出来ます。



まとめ

Mojoliciousは最低限の必要十分な機能を備えているのでこのような工夫しどころがあり面白いですね。コアモジュールのみに依存しているというポータビリティも評価すべき点で、Web Application作成の入門としてはちょうどいいWAFなので、YAPC::Asia 2013のトークが採択されたならば分かりやすく効果的なチュートリアルを用意したいと思います!ってことでトークへの応援よろしく〜