モダンPerlの世界へようこそ

第22回Mojolicious::Lite:本当に簡単なウェブアプリがあればいいときは

あれから1年

Mojoについては2009年1月1日から4回にわたって特集記事を連載しました。ちょうど執筆を開始した直後に作者リーデル氏が不幸な医療事故にあい、一時はどうなることかと思いましたが、連載を終了する直前に開発続行の宣言が出て、ほっとしたのをよく覚えています。

あれから1年。Mojoを取り巻く環境はずいぶん変わりましたが、いま、Mojoはいったいどうなっているのでしょうか。今回は今年最後の記事として、Mojo界隈の近況をお届けすることにします。

大きく変わったといわれていますが……

昨年12月にバージョン0.9に到達したMojoは、途中事故の後遺症で開発が停滞した時期はあったものの、この1年でかれこれ30回以上のリリースが行われたことからもわかるように、いまもなお着実に開発が続けられています。この「ベータテスト」期間中にいくつか後方互換性が失われる変更があったため批判を浴びたこともありましたが、これはおもに実装が進んだことによるファイルの再配置や、不要と判断された機能の除去によるものなので、巷で言われているほど大きな変更だったわけではありません。実際にどのような変更があったのかについてはこれから説明しますが、少なくともCatalystMoose化されていく過程で起こった騒動に比べればよほど軽微なものですので、まずはご安心くださいと申し上げておきましょう[1]⁠。

HTTPパイプラインの実装による修正点

Mojoが当初からインターネットの標準を強く意識して書かれてきたことは前回の特集記事でも触れた通りですが、Mojoが後方互換性を失ったひとつの原因は、これまで多くのPerl製HTTPサーバが見て見ぬ振りをしていたHTTPパイプラインの仕様を愚直に実装したからでした。この機能が実装されていると、コネクションをいくつも張り巡らせなくても、ひとつのコネクションのなかで複数のリクエストをサーバ側からのレスポンスを待つことなく連続して送信できるようになるのですが、当初はMojoもほかのPerl製HTTPサーバの実装と同じように一組のリクエスト/レスポンスにしか対応しておらず、パイプライン処理はあとからMojo::Pipelineという別の名前のモジュールを追加して行っていました。そのため、モジュール名が実態とずれてきたり、重複したコードが出たりと、全体的な見通しが悪くなってきたので、既存のMojo::TransactionはMojo::Transaction::Singleに、既存のMojo::PipelineはMojo::Transaction::Pipelineに移動したうえで、あらためてMojo::Transactionというクラスを共通のベースクラスにして関連性をより明確にした、というのがその後方互換性喪失(というか、過去の不完全な実装からの決別)の顛末です。

一般的なエンドユーザのレベルでは、テストなどでトランザクションをロードするときにuseするモジュールの名前が変わった(Mojo::Transactionではなく、ふつうはMojo::Transaction::Singleを呼ばなければならなくなった)ことと、従来テストに利用していたMojo::ClientのAPIががらっと変更になったこと、テスト用にはあらたにTest::Mojoというモジュールが用意されるようになったことを覚えておけば事足りる話ではありますが、フレームワークやサーバの実装を書いている方は、実際にHTTPパイプラインに対応するかどうかはさておき、Mojoがこのような道をたどった(そして気をつけないと自分も二の轍を踏むことになる)ということも覚えておいていただければと思います。

Mojolicious::Lite

Mojoが後方互換性を捨てたもうひとつの理由は、デフォルトのサンプルフレームワークがCatalystを強く意識していたMojoliciousから、よりシンプルな(RubyのSinatraによく似た)Mojolicious::Liteに変わったことから説明できます。

もともとリーデル氏はMojoをCatalystのエンジンに、という希望を持っていたため、当初はいささか冗長ながらも拡張性を重視した(Catalystの流儀にもあわせられるようにした)設計をしていましたが、受け入れられる芽がすっかりなくなってしまったのであれば、無闇に拡張性を持たせるよりは、健全なデフォルトを与えるほうがコードがきれいになりますし、初心者にもやさしくなります。そのため、たとえば当初はCatalystと同じように「my ($self, $c) = @_;」のような形でたらい回しにしていたコンテキストオブジェクトは(前回特集記事を連載していた時にもすでにその傾向が見えていたように)いまでは完全に(コントローラの)$selfのなかに統合されてしまいましたし(stashやreq、paramといったメソッドはコントローラから直接アクセスできるようになっています⁠⁠、アトリビュートへのアクセサ/ミューテータも整理され、アトリビュート名とデフォルト値のみ書けばよいようになりました(遅延評価させたい場合はsub { ... } のなかに入れておきます⁠⁠。

__PACKAGE__->attr(name => 'default value');
__PACKAGE__->attr(name => sub { Some::Module->new });

テンプレートの拡張子も当初の.phtmlから、.html.epliteなどを経て、いまは.html.epへと変更になっています。テンプレートそのものの表記はあまり変わっていませんが、従来の.eplテンプレートなどに比べると、.epテンプレートでは$selfなどをいちいち引き渡さなくてもよくなり、より簡潔に書けるようになったという違いがあります。

CGI.pmのようなMojolicious::Lite

このあたりの違いは言葉で説明するだけではわかりづらいでしょうから、実際にひとつサンプルアプリケーションをつくってみましょう。最新のMojoをインストールしたら、適当なディレクトリにnopasteというファイルをつくって、エディタでこのようなコードを書いてください。

#!/usr/bin/env perl

use Mojolicious::Lite;
use Mojo::Asset::File;
use Encode;

our $datadir = app->home->rel_dir('data');
mkdir $datadir unless -d $datadir;

app->log->level('error');
app->types->type(html => 'text/html; charset=utf-8');

get '/' => 'form';

post '/' => sub {
    my $self = shift;
    my $id   = time;
    my $file = Mojo::Asset::File->new(path => "$datadir/$id");
    $file->add_chunk(encode_utf8($self->param('code')));
    $self->redirect_to('page', id => $id);
};

get '/:id' => [ id => qr/\d+/ ] => sub {
    my $self = shift;
    my $id   = $self->stash('id');
    my $path = "$datadir/$id";
    return $self->render('not_found') unless -f $path;

    my $file = Mojo::Asset::File->new(path => $path);
    $self->stash(code => decode_utf8($file->slurp));
} => 'page';

shagadelic;

__DATA__

@@ form.html.ep
% layout 'default';
<form method="POST">
<textarea name="code"></textarea>
<input type="submit" value="post">
</form>

@@ page.html.ep
% layout 'default';
<div><pre>
<%= $self->stash('code') %>
</pre></div>

@@ not_found.html.ep
% layout 'default';
<h1>not found</h1>

@@ layouts/default.html.ep
<!doctype html><html>
<head><title>nopaste</title></head>
<body>
<%== content %>
<p><a href="<%= url_for 'form' %>">home</a></p>
</body>
</html>

書き終わったら(コピー&ペーストしたら⁠⁠、コマンドラインから「perl nopaste daemon」とタイプして、スタンドアロンサーバを立ち上げてみましょう。ブラウザでhttp://localhost:3000/にアクセスして、適当なコードを投稿してみてください。専用のページに移動して、投稿したコードが表示されれば成功です。

Mojolicious::Liteアプリケーションのテスト

いまはブラウザを使って動作確認しましたが、もちろんテストのたびにブラウザを起動するのはばかげた話ですから、自動テストもできるようにしておきましょう。これもやり方は何通りかあるのですが、ここではアプリケーション本体のなかにいきなりテストを書いていく方法を紹介します。このようなパッチをあてたうえで、コマンドラインから「prove -v nopaste」を実行してみてください。

@@ -30,6 +30,27 @@
     $self->stash(code => $file->slurp);
 } => 'page';

+if ($ENV{HARNESS_ACTIVE}) {
+    require Test::More;
+    require Test::Mojo;
+    require File::Path;
+
+    local $datadir = app->home->rel_dir('testdir');
+    mkdir $datadir unless -d $datadir;
+
+    my $t = Test::Mojo->new;
+    $t->get_ok('/')->status_is(200)->content_like(qr/form method/);
+    $t->post_form_ok('/', { code => 'sample text' })
+      ->status_is(302);
+    my $location = $t->tx->res->headers->location;
+    $t->get_ok($location)->status_is(200)
+      ->content_like(qr/sample text/);
+
+    Test::More::done_testing();
+    File::Path::rmtree($datadir);
+    exit;
+}
+
 shagadelic;

 __DATA__

done_testing()というのはTest::More 0.88以降で導入された新しいコマンドです。proveやmake testなどを使って自動テストを実行するとHARNESS_ACTIVEという環境変数が有効になることを利用して、自動テストのときはデータディレクトリを変更しつつ、Test::WWW::Mechanizeに似たインタフェースを持つTest::Mojoを使ってそれぞれのパスに正しくアクセスできるか、フォームから投稿した内容が次のページで表示されているかなどのチェックをしています。

今回のサンプルアプリケーションは小さいので、⁠shagadelic;」でコマンド/サーバに処理を返す直前にテストをまとめて書いていますが、アプリケーションが大きくなってきたらHARNESS_ACTIVEでくくったブロックをいくつかに分割すれば、実際のコードとテストが近い、見通しのよいものになるでしょう。

Mojolicious::Liteと日本語の扱い

先にもちらっと書きましたが、この原稿を執筆している時点でCPANにあがっているバージョン0.999914にはマルチバイトの対応に問題が残っています。最近のMojoはフォームからPOSTされてきたパラメータを自動的にdecodeしてくれるようになっているのですが、このとき、utf-8以外の文字コードを使っていると(たとえばWindows環境でシフトJISを扱うアプリケーションを書こうとすると⁠⁠、POSTした内容が(decodeに失敗した結果)消失してしまうようになっていたのですね。

この問題はリポジトリ上ではすでに対策を取り込んでもらっているので、次のバージョンがリリースされたときには解決しますが、どうしてもいますぐCPAN版を使う必要がある場合はmultipart/form-dataでPOSTすることで問題解決する場合があることも覚えておくとよいでしょう(このあたりの挙動はやや整合性がとれていないので、将来的にはまだ変更の可能性があるかもしれません⁠⁠。

また、Mojoliciousには最近プラグイン/トリガの機構が追加されました。これも次のバージョンで導入される予定ですが、charsetプラグインを使うとレンダリング時やパース時などの文字コードの設定を簡単に書けるようになります。

@@ -8,7 +8,7 @@
 mkdir $datadir unless -d $datadir;

 app->log->level('error');
-app->types->type(html => 'text/html; charset=utf-8');
+plugin(charset => { charset => 'Shift_JIS', encoding => 'shift_jis' });

 get '/' => 'form';

後者のencodingは省略可能ですが、携帯などでContent-Typeに設定するcharsetと入出力時の文字コードが微妙に異なる場合などは明示的にencodingを設定しておくことで文字化けを防ぐことができます(このあたりをもっと動的に変更したい場合はMojolicious::Plugin::Charsetを参考にプラグインを書いてください⁠⁠。

なお、Mojoliciousのテンプレートは、この例のようにアプリケーションに同梱することもできますし、外部のファイルを読み込むこともできるのですが、⁠__DATA__以下の)アプリケーション内部のテンプレートを読み込む場合は(use utf8して)utf-8で、外部のテンプレートファイルを読み込む場合はcharsetプラグインなどで設定した(app->renderer->)encodingの値にあわせた文字コードで記述する必要があることも覚えておいてください(シフトJISで出力するからといってアプリケーションをシフトJISで記載すると化けますのでご注意ください⁠⁠。

外部テンプレートの置き場所はapp->renderer->rootで設定できます。

app->renderer->root('./templates');

外部テンプレートには「@@ page.html.ep @@」のような区切り文字は不要です。

Mojoの現状とこれから

MojoはもともとPerl 5.8.1さえ入っていればあとは何もいらない、FTPなどでも、あるいはWindowsなどでも簡単にインストールできるフレームワークというのがひとつのセールスポイントでしたが、Mojolicious::Liteのアプリケーションも、従来ならCGI.pmを使ってつくっていた一枚もののCGIアプリケーションと同じくらい手軽に書け、アップロードできるようになったという意味で、Mojo(licious)は以前にもまして「⁠⁠FastCGIやmod_perlにも対応した)モダンなCGI.pm」としての性格が色濃くなったといえます。

予定されていたドキュメントの整備についてはまだ進んでいないようですが、ドキュメントを整備してバージョンを1.0にあげてしまったらそれ以上の大規模改修はむずかしくなってしまいますから、その前に、ベータテストでしっかり使い込んで、出せるうみは出しておきたい、という気持ちもあるのでしょう。日頃から生煮えのモジュールを使うことに慣れてしまった人のなかにはベータテスト中の仕様変更に目くじらをたてる人もいますが、あまりに早い段階から使われるようになってしまった結果、途中で問題が出てきてもうかつに仕様変更できなくなって大変な苦労をしている(あるいは古いものを投げ出して新しいものに切り替えてしまう)コミュニティも少なくないことを思えば、この1年のMojoの変化は十分理性的なものだったように思えます。

また、ときどき「Mojoは遅くてだめだ」といった声も耳にしますが、いまのMojoにとって最優先事項はすべてのAPIを固めて、十分なテストを行うこと。APIの設計そのものがおかしいときにいくら高速化をしても、その努力はより適切なAPIが固まった時点で無駄になるのですから、現状では的はずれな批判というしかありません。

本当はこの連載でMojoを取り上げるのはMojoが無事バージョン1.0を迎えてから、と決めていたのですが、巷でMojoの話題が出るたびに「特集記事のコードが動かない」という嘆きの声が聞こえてくるので今回は特集記事のコードを修正するかたわら中間報告をしてみた次第。あらためて書いておきますが、Mojoは現在ベータテスト中です。ベータ版だからといって使えないということはありませんし、実際筆者はさまざまな場面でMojoアプリケーションを書いていますが、まだしばらくは(今回見つけた文字コードまわりの問題のように)不具合が見つかって修正が必要になる場面も出てくる可能性があることは覚えておいてください。

ただし、必要以上にこわがるのはなしですよ。Mojoはやみくもに機能拡張を続けて、どこにゴールがあるのかわからないというモジュールではありません。そこに到達するまでにあとどのくらい時間がかかるかはまだわかりませんが、きちんとバージョン1.0を目指して開発が進められているのですから。

おすすめ記事

記事・ニュース一覧