天然パーマです。

jVideosのWebアプリケーション部分技術解説

先日公開した「jVideos」という(エロ)サイト。

Webアプリとしての技術的な観点で言えば、クローラーが収集したデータをただデータベースから取ってきて表示するだけの簡単なものになっている。基本的なところではJavaScriptも使ってない。そこで、「単純な上、構造がわかりやすい」いい題材と思ってこのjVideosを用いてWebアプリケーションの基本的な技術を解説したい。

1. Perlを使っています

まず、Webアプリケーションを含むバックエンドでは全てPerlを使っている。まぁ何故Perlかと今更聞かれると一番手になじむ言語だったということなんだけど、クローラーやWebのための要素は全てPerlで揃っているので問題はない。というかむしろテキスト処理が得意なPerlはテキストをベースとしたWebを扱うのに優れていると思っている。

実はアプリケーションサーバもPerl製のStarmanという物を使っている。次に出てくるWebアプリケーションフレームワーク(WAF)で構築されたアプリをStarmanで動かしフロントエンドにはnginxを置いている。なんかこういうイメージ。

クライアントブラウザ => nginx => Starman

2. WAFにはMojolicious

Webアプリケーションフレームワーク(WAF)という言葉はよく聞くと思うが、これはWebアプリを作るためのひな形とそのクラス、スクリプト群である。安全で効率的に開発を進めたければ既存のよくメンテナンスされたWAFを使うのがよろしい。最近では、MojoliciousというWAFをよく利用させてもらっている。依存性が極端に少なく、いわゆるフルスタックな作りなんだが、基本機能に焦点を当てれば使えるので気に入っている。Mojoliciousを入れたら、mojoコマンドが使えるので、

$ mojo generate app JVideos::Web

等としてアプリケーションのひな形を作る。「プロジェクト名::Web」と::Webを付けているのはディレクトリ構造を綺麗にしたいためである。

3. PSGIファイル

Mojoliciousにも起動スクリプトが付いていて簡易サーバが立ち上がるんだけど、plackup使いたいのと、PSGIで制御したいのと、結局PSGIファイルを作るのでまずjvideos_web.psgiみたいなファイルを作っちゃっている。リバースプロキシに対応させるなら以下のようなもので、Middlewareなど足したいものがあればお好きにどうぞ。

use Mojo::Server::PSGI;
use File::Spec;
use File::Basename;
use lib File::Spec->catdir(dirname(__FILE__), 'lib');
use Plack::Builder;

my $psgi = Mojo::Server::PSGI->new( app_class => 'JVideos::Web' );
my $app = sub { $psgi->run(@_) };

builder {
    enable_if { $_[0]->{REMOTE_ADDR} eq '127.0.0.1' }
        "Plack::Middleware::ReverseProxy";
    $app;
};

これで普通に「plackup jvideos_web.psgi」すれば動くだが、確かStarmanの場合だと、

$ starman -MFindBin jvideos_web.psgi

としなくてはいけないので注意。

4. APIモジュールを使う

Mojoliciousは一般的なMVCになっているのだけれども、Mの部分の指針がない(と思う)ので独自で試行錯誤しながらやっている。なんとなく「API」と呼ばれる層を作ってデータを統合的に扱わせている。例えば「JVideos::API」や「JVideos::Web::API」のようなモジュール(クラス)を経由させてコントローラから操作するわけだ。このAPIはフレームワークに依存しないためにテストできる点やコマンドラインインターフェースから扱うことが容易だ。

例えば、複数の記事を取得するためのAPIのメソッドは以下のようになる。

sub get_entries {
    my ( $self, $cond , $attr ) = @_;
    $attr->{pager_logic} ||= 'MySQLFoundRows';
    $attr->{page} ||= 1;
    $attr->{limit} ||= 5;
    my ( $iter, $pager ) = $self->db->search_with_pager('entry', $cond, $attr);
    my @entries;
    while ( my $entry = $iter->next ) {
        push @entries, $entry;
    }
    if( wantarray ) {
        return (\@entries, $pager);
    }else{
        \@entries;
    }
}

これを使い、最新記事とページャを取得するコントローラ部分はこのようになった。

sub index {
    my $self = shift;
    my $page = $self->req->param('page') || 1;
    my ($entries, $pager) = $self->app->api->get_entries(
        {},
        { order_by => { created_on => 'desc' }, page => $page }
    );
    $self->stash->{entries} = $entries;
    $self->stash->{pager} = $pager;
}

ちなみに、Mojoliciousのクラスジェネレータは貧弱なので、API群にはMouseを使っている。

5. 設定の引き回し

DBの設定など設定ファイルに環境個別で書きたい時があるが、Mojoliciousのその機能ではあまり満足できなかった気がしたので自分でその処理を書いた。大抵「JVideos->config()」といったメソッドで呼び出すことができる。コードはちょっと長いので割愛するがAmon2などのWAFを参考にした。

6. ディスパッチ

URLとコントローラをマッピングして適切に処理するためには、Mojoliciousでは一つのファイルにそれを記述することになる。「mojo generate app JVideos::Web」としてひな形を作ったら、「JVideos::Web」にそれを書く。現行のjVideosの「JVideos::Web」のコードは以下だ。

package JVideos::Web;
use Mojo::Base 'Mojolicious';
use JVideos::API;

sub startup {
    my $self = shift;
    $self->attr( api => sub { JVideos::API->new } );
    my $r = $self->routes;
    $r->route('/')->to('root#index');
    $r->route('/amature')->to('root#amature');
    $r->route('/about')->to('root#about');
    $r->route('/entry/:entry_id')->to('entry#entry');
    $r->route('/actress/:name')->to('actress#actress');
    $r->route('/actress')->to('actress#index');
    $r->route('/tag/:name')->to('tag#tag');
    $r->route('/tag')->to('tag#index');
}

1;

シンプルである。

7. テンプレート処理

いわゆるViewであるテンプレート部分ではMojo::Templateに従って書く。これはPerlのコードが結構そのまま書ける。このような具合である(テンプレートにオブジェクトをそのまま渡すべきかの議論は置いておく)。

% for my $entry ( @$entries ) {
<h3><%= $entry->title %></h3>
% }

Mojo::Templateを調べていくと便利なヘルパーが見つかるがあまり使っていない。が、はまったものとしてURI Escapeをどうするかというのがあったのでやり方を紹介。これでできる。

<a href="/actress/<%= b($actress_name)->encode('UTF-8')->url_escape %>">
    <b><%= $actress_name %></b></a>

以上、jVideosを参考にWebアプリ構築でなりそうなところを脈絡もあまりなく紹介してきた。というよりか、最近はこんな感じでWebを作っていますという報告に近い。また気づいた点があれば本ブログで紹介していこうと思う。