Mojoを使って自作ウェブアプリをよりポータブルに!

第3回サンプルフレームワーク:Mojolicious

2009年12月8日:サンプルコードを現在のバージョンで動作するよう修正しました。

Mojoliciousを使ってみよう

前回はすでにできあがったアプリケーションにMojoを組み込んで移植性を高める方法を見ました。今回はこれから新しいアプリケーションを構築する際のベタープラクティスのひとつとして、Mojoのパッケージに同梱されているMojoliciousというフレームワークを利用する方法を紹介します。

まずはひな形から

Mojoliciousのアプリケーションも、Mojoの場合と同じくまずはひな形をつくるところから始めます。今回は簡単なWikiもどきをつくってみましょう。例によってMojoをインストールしたディレクトリでこのようなコマンドを入力します。

> perl script/mojolicious generate app SimpleWiki
> cd simple_wiki

Mojoのひな形に比べていくらか余分にファイルが生成されます。開発用サーバの立ち上げ方はMojoアプリの場合と同じですので、初期画面を見たい方は各自ご確認ください。

lib/SimpleWiki.pmはこのようになっています。

package SimpleWiki;

use strict;
use warnings;

use base 'Mojolicious';

# This method will run once at server start
sub startup {
    my $self = shift;

    # Routes
    my $r = $self->routes;

    # Default route
    $r->route('/:controller/:action/:id')
      ->to(controller => 'example', action => 'welcome', id => 1);
}

1;

Mojoliciousアプリケーションの処理の流れ

Mojo単独で使う場合はhandlerというメソッドのなかにアプリケーションを入れましたが、Mojoliciousの場合、内部に隠蔽されているhandlerではなく、サーバ起動時に1回だけ実行されるstartupと、リクエストがあるたびに実行されるdispatchというメソッドを上書きしていきます。

dispatchメソッドのほうは、まずpublicディレクトリ以下の静的ファイルをチェックし、それがなければURLに応じたコントローラに処理を渡し、それでもだめならあらかじめ用意されている静的な404ファイルを返すようになっています。

startupメソッドでは必要に応じて独自のコンテキストを用意したり、例にはあがっていませんがデータベースなどの設定を行うのが一般的ですが、このパス(ルート)を解析する部分にはあまりなじみがないかもしれません。

Merbライクなディスパッチャ

Mojoのルータ(ディスパッチャ)は、最近Ruby on Railsが採用したことで話題になっているMerbのルータによく似たものです。基本的にはURLのパス部分から(あるいはそれとは独立して)コントローラ名とそのコントローラ内で実行するメソッド(アクション)名、必要に応じて任意のキャプチャを取得するためのルールをroute内に記述し、toにはデフォルト値を記述する(デフォルト値が用意されている項目は省略可能になる⁠⁠、と考えておけばよいでしょう。

生成されたひな形の例で言うと、http://127.0.0.1:3000/wiki/create/10にアクセスすれば、lib/SimpleWiki/Wiki.pmというコントローラのcreateというメソッドが呼ばれ、http://127.0.0.1:3000/にアクセスすると、デフォルトで指定されているlib/SimpleWiki/Example.pmというコントローラのwelcomeというメソッドが呼ばれる、という寸法です(キャプチャしたcontroller、action、idの値はコンテキストオブジェクト内部のstashというハッシュに格納されます⁠⁠。

今回はWikiもどきをつくりたいので、まずはルータの設定を多少変更しましょう。コントローラは固定で、CRUDのメソッドはなるべく省略しやすいよう後ろに回します。

@@ -13,8 +13,8 @@
     my $r = $self->routes;

     # Default route
-    $r->route('/:controller/:action/:id')
-      ->to(controller => 'example', action => 'welcome', id => 1);
+    $r->route('/:id/:action')
+      ->to(controller => 'entry', id => 1, action => 'read');
 }

 1;

モデルをつくる

続いてWikiのデータを扱うモデルをつくります。ここではデータベースは使わず、単純なファイルの入出力ですませてしまいます。エディタでこのようなlib/SimpleWiki/Model.pmを用意してください(以下、スペースの都合でupdateとdeleteに関する部分は省略しますが、余力のある方はぜひご自分で実装してみてください⁠⁠。

package SimpleWiki::Model;

use strict;
use warnings;
use File::Spec::Functions 'catfile';
use base 'Mojo::Base';

__PACKAGE__->attr(datadir => 'data');

sub datafile {
    my ($self, $id) = @_;
    return catfile($self->datadir, $id);
}

sub create {
    my ($self, $id, $data) = @_;
    open my $fh, '>', $self->datafile($id) or return;
    print $fh $data;
    return 1;
}

sub read {
    my ($self, $id) = @_;
    open my $fh, '<', $self->datafile($id) or return;
    return do { local $/; <$fh>; };
}

1;

モデルを登録する

モデルができたら、アプリケーションから呼び出せるようにしましょう。lib/SimpleWiki.pmをこのように修正します。

@@ -4,6 +4,9 @@
 use warnings;

 use base 'Mojolicious';
+use SimpleWiki::Model;
+
+__PACKAGE__->attr(model => sub { SimpleWiki::Model->new });

 # This method will run for each request
 sub startup {
@@ -15,6 +18,8 @@
     # Default route
     $r->route('/:id/:action')
       ->to(controller => 'entry', id => 1, action => 'read');
+
+    $self->model->datadir($self->home->rel_dir('data'));
 }

 1;

$self->homeにはアプリケーションのホームディレクトリを指し示すオブジェクトが格納されています(このようにホームディレクトリからの相対位置を明確にしておくと、mod_perlなどでカレントディレクトリが変わる場合も正しく対応できるようになります⁠⁠。また、modelのようなアクセサ/ミューテータのデフォルト値にオブジェクトを渡すときは遅延評価させるためにsub { }でくくるのがお約束です。

ここで、Catalystに慣れた方はモデルを直接コンテキストに入れてしまいたくなるかもしれません。が、Mojoliciousの場合、コンテキストはdispatchするときに毎回つくるものなので、コンテキストレベルでモデルの初期化を行うと非常に無駄が多くなります。モデルはあくまでもアプリケーション本体に格納し、コンテキストには必要に応じてsub model { shift->app->model(@_) }のようなショートカットを用意するのがMojoliciousの流儀です。

コントローラをつくる

さて、今度はコントローラをつくります。サンプルコントローラ(lib/SimpleWiki/Example.pm)を見ると、コントローラはMojolicious::Controllerをベースに持ち、$self->renderを実行するとtemplates/<コントローラ名>/<アクション名>.html.epというテンプレートがレンダリングされることがわかります。ここでもその方式を踏襲しましょう。

lib/SimpleWiki/Entry.pmはこのようになります。

package SimpleWiki::Entry;

use strict;
use warnings;
use base 'Mojolicious::Controller';

sub create {
    my $self = shift;
    if (uc $self->req->method eq 'POST') {
        $self->app->model->create($self->stash->{id}, $self->req->param('text'));
    }
    $self->render;
}

sub read {
    my $self = shift;
    $self->stash->{body} = $self->app->model->read($self->stash->{id});
    $self->render;
}

1;

Mojoliciousのコンテキスト

以前は$self->ctxに格納されていたコンテキストは、現在は$selfそのものにかわっています。ここには前回前々回と説明してきたMojoのトランザクションオブジェクトのほか、Mojoアプリケーション本体のインスタンスなどもおさめられています($self->appの中身がそれです⁠⁠。

テンプレートを用意する

続いて、テンプレートを用意しましょう。Mojoliciousは、デフォルトではMojo::Templateという、Rubyのerb、あるいはPerlのHTML::Masonに似たテンプレートエンジンを利用します。これはもともとひな形を生成するためにつくられたものですが、CPANにはMojoX::Renderer::TTというTemplate-Toolkitを使うレンダラも用意されていますので[1]⁠、お好みにあわせて使い分けてください。

ここではデフォルトで生成されるtemplates/example/welcome.html.epを参考に、このようなtemplates/entry/read.html.epを用意することにします。ご覧の通り、Mojo::Templateを使うと任意のPerlコードをテンプレートに埋め込めるのがポイントです。

<!doctype html>
    <head><title><%= $self->stash->{id} %></title></head>
    <body>
        <%= $self->stash->{body} %>
    </body>
</html>

同様に、templates/entry/create.html.epはこのようになります。

<!doctype html>
    <head><title><%= $self->stash->{id} %></title></head>
    <body>
    <form method="POST">
        <textarea name="text"></textarea>
        <input type="submit">
    </form>
    </body>
</html>

リダイレクトの扱い

これで最低限動作するようになりましたので、一度動作確認をしておきましょう。もちろん前回の例にならってテストを書いてもよいのですが、ここでは実際に開発用サーバを立ち上げ、ブラウザで動作を確認してみてください。http://127.0.0.1:3000/test_id/createでデータを作成したあと、http://127.0.0.1:3000/test_idにアクセスしてそのデータが表示できればひとまず成功ですが、データを入力したあと、わざわざ手作業で表示画面に移動するのは面倒な話。ここはリダイレクトをかけたいところです。

lib/SimpleWiki/Entry.pmをこのように修正しましょう。

@@ -8,6 +8,7 @@
     my $self = shift;
     if (uc $self->req->method eq 'POST') {
         $self->app->model->create($self->stash->{id}, $self->req->param('text'));
+        $self->redirect_to($self->url_for(action => 'read'));
     }
     $self->render;
 }
@@ -15,6 +16,7 @@
 sub read {
     my $self = shift;
     $self->stash->{body} = $self->app->model->read($self->stash->{id});
+    return $self->redirect_to($self->url_for(action => 'create')) unless defined $self->stash->{body};
     $self->render;
 }

Catalystをご存知の方は$self->url_forの扱いが異なるところに注意が必要かもしれません。Mojoliciousの場合、url_forはルータでキャプチャした内容を置き換えたURLが返ります。

Mojoliciousのログ

Mojoliciousを含むMojoアプリケーションにはログ機能がついています。動作確認したあとでlogディレクトリの下を見るとdevelopment.logというファイルができているはずですが、特に開発用サーバを使って開発している際は、ログメッセージはむしろ画面に出力してくれたほうがうれしいですし、本番環境ではデバッグログは出力したくないものです。Mojoliciousはそのような要望に応えるため、MOJO_MODEという環境変数を使ってモードを切り替えられるようになっています(デフォルトではdevelopmentモードになります⁠⁠。lib/SimpleWiki.pmをこのように修正しましょう。

@@ -22,4 +22,14 @@
     $self->model->datadir($self->home->rel_dir('data'));
 }

+sub development_mode {
+    my $self = shift;
+    $self->log->handle(*STDERR);
+}
+
+sub production_mode {
+    my $self = shift;
+    $self->log->level('error');
+}
+
 1;

コントローラ内部からログを出力したい場合は、$self->appを通します。ログの種類はdebug、info、warn、error、fatalの5種類です。

@@ -8,6 +8,7 @@
     my $self = shift;
     if (uc $self->req->method eq 'POST') {
         $self->app->model->create($self->stash->{id}, $self->req->param('text'));
+        $self->app->log->info('created '.$self->stash->{id});
         $self->redirect_to($self->url_for(action => 'read'));
     }
     $self->render;

Test::Mojoを使ったテスト

最後にMojoliciousアプリケーションのテストについても触れておきましょう。

Mojoliciousアプリケーションのテストは基本的に前回見たMojoアプリケーションのテストと同じで、アプリケーションのインスタンスをつくり、テストしたいトランザクションをアプリケーションのhandlerに食わせればよいのですが、いまではより簡単なTest::Mojoを使ったテストに移行しています。t/basic.tをこのように修正してみてください。

@@ -3,12 +3,17 @@
 use strict;
 use warnings;

-use Test::More tests => 5;
+use Test::More tests => 7;
 use Test::Mojo;

 use_ok('SimpleWiki');

 # Test
 my $t = Test::Mojo->new(app => 'SimpleWiki');
-$t->get_ok('/')->status_is(200)->content_type_is(Server => 'text/html')
-  ->content_like(qr/Mojolicious Web Framework/i);
+$t->post_form_ok('/test/create' => {text => 'test is ok'})
+  ->status_is(302);
+
+$t->get_ok('/test')
+  ->status_is(200)
+  ->content_type_is('text/html')
+  ->content_like(qr/test is ok/);

Mojoliciousはあくまでサンプルにすぎません

さて、ここまで駆け足でMojoliciousアプリケーションの一例を紹介してきましたが、Mojoliciousというフレームワークは、実はMojoを使ったフレームワークのサンプルにすぎません。見るべきところはたくさんありますが、Mojoを使うならかならずMojoliciousを使わなければならない、ということはありません。

次回はMojoが誕生した経緯を振り返りながら、今後Mojoとどのようにつきあっていくのがよいかを考えてみます。お楽しみに。

おすすめ記事

記事・ニュース一覧