« アプリケーションとサーバサイドマッシュアップの統合 | メイン | 失敗しない Rails が動かせるホスティングサービス選びと環境構築 »

JiftyでWebアプリをつくる - JiftyからMVCを考えるはてなブックマークに追加 livedoorクリップに追加 Yahoo!ブックマークに追加 del.icio.usに追加 イザ!ブックマーク ニフティクリップに追加

お久しぶりです、Perl担当の西山です。

前回に続きDRY(Don't Repeat Yourself)を追求するWebフレームワークJiftyを取り上げます。
手元の環境にPerlの実行環境やJiftyをインストールされていない方はこれまでの記事を参照してください。
今回はJiftyと一緒に配布されている"ShrinkURL"というサンプルアプリケーションを題材に、
Jiftyに従ってアプリケーションを開発する場合のモジュール構造や考え方を見ていきたいと思います。

これまでの記事:
  1. JiftyでWebアプリをつくる - Windowsにインストール
  2. JiftyでWebアプリをつくる - ログイン機能を作る

Jiftyの考え方

一般的にWebアプリケーションのフレームワークなどで使用されるアプリケーション構造はMVC(Model-View-Controller)と呼ばれる形に機能は分類されますが、 Jiftyでは本家サイトのBasic concepts of Jiftyで説明されているように「Dispatcher」「Action] 「Template」「Model」という4つの要素でアプリケーションを構成しています。 それぞれの要素は主に以下のような役割で開発するように設計されています。
  • Dispatcher: URLに対してActionやTemplateへのマッピングを定義。
  • Action: 画面内での操作やリクエストなどをトリガーに発生するビジネスロジックを定義。
  • Template: 表示系のデータ取得処理や、UIのレイアウト情報を定義。
  • Model: 操作対象となるデータモデルの構造や操作を定義。

通常のMVCではControllerでまとめて扱われることの多いDispatcherとActionの役割を、Jiftyではフレームワークレベルで明確に分けています。
また、表示系の処理もViewが使用するhtmlテンプレートやControllerに分散しやすいですが、JiftyではTemplateのPerlモジュール内で一括管理するように設計されているのが特徴だと思います。

サンプルアプリShrinkURLを見る

新しいモジュールやフレームワークの使い方を理解するには サンプルをてっとり早く動かしてみていろいろいじってみるのが早いと思います。 サンプルアプリケーションであるShrinkURLを見てみましょう。
セットアップ
先ほどサンプルはJiftyと一緒に配布されていると書きましたが、 Windows版のPPMには含まれていないようです。 TortoiseSVNなどのSubversionクライアントをインストールしている方は下記レポジトリから、 チェックアウトしてください。
http://svn.jifty.org/svn/jifty.org/jifty/trunk/examples/ShrinkURL

ブラウザ経由でもCPANのJiftyのページからダウンロードできます。
解凍後のフォルダのexamples\ShrinkURLが今回使用するアプリケーションフォルダです。
適当な作業フォルダにShrinkURLをコピーしてください。

また、ShrinkURLが依存するモジュールとしてNumber::RecordLocatorが必要になります。
別途Package Managerを使ってインストールしてください。

最後に、以下のようにコマンドプロンプトから ShrinkURLフォルダ配下でコマンドを実行するとセットアップ完了です。
C:\tmp\ShrinkURL>perl bin\jifty schema --setup
動作確認
まずは素の状態の動作を見てみます。 以下のようにjiftyコマンドにserverオプションを指定して実行するとアプリケーションサーバが起動し、 デフォルトではhttp://localhost:8888/のURLで アクセスできるようになります。
C:\tmp\ShrinkURL>perl bin\jifty server

ShrinkURLは、入力されたURLにユニークなキーを割り当て、短縮したURLでサイトにアクセスできるようにするアプリケーションです。
登録したURLはDBに登録され、短縮したURLはいつでも利用することができます。


1.URLを入力してShrink it!ボタンをクリック

sc0000.png

2.短縮したURLが生成される

sc0001.png

生成されたURLにブラウザからアクセスすると、元の入力したURLへリダイレクトされます。

ソース概要
動作を把握したところで、次はオリジナルのソースを見てみましょう。 Dispatcher、Action、Template、Modelにあたるモジュールを簡単に説明します。
  • Dispatcher: ShrinkURL\lib\ShrinkURL\Dispatcher.pm
    ShrinkURL配下の各URLにアクセスされた場合の処理を定義しています。 アプリケーションルートにアクセスされた場合、shrinkテンプレート(ShrinkURL\lib\ShrinkURL\View.pm内に定義)を呼び出してURL入力画面を表示させます。 ルート以外にアクセスがあった場合は、短縮したURLとしてのアクセスと判断し、該当するURLを検索してリダイレクトさせます。
    #!/usr/bin/env perl
    package ShrinkURL::Dispatcher;
    use strict;
    use warnings;
    use Jifty::Dispatcher -base;
    
    # visiting / will let users create new shrunken URLs
    on '/' => show 'shrink';
    
    # any other URL (that has no path separator) is potentially a shrunken URL
    on '*' => run {
        my $url = $1;
    
        my $shrunkenurl = ShrinkURL::Model::ShrunkenURL->new;
        $shrunkenurl->load_by_shrunken($url);
    
        if ($shrunkenurl->id) {
            redirect($shrunkenurl->url);
        }
    
        # if there's no valid URL, just let the person create a new one :)
        redirect('/');
    };
    
    1;
    
  • Action: ShrinkURL\lib\ShrinkURL\Action\CreateShrunkenURL.pm
    URL入力画面でShrink it!ボタンがクリックされた後に実行される処理が記述されています。 ここではJifty::Action::Record::Createを利用してModelの登録処理を行っています。
    #!/usr/bin/env perl
    package ShrinkURL::Action::CreateShrunkenURL;
    use strict;
    use warnings;
    
    use base qw/Jifty::Action::Record::Create/;
    sub record_class { 'ShrinkURL::Model::ShrunkenURL' }
    
    # have we already shrunk this URL? if so, no need to do it again!
    sub take_action {
        my $self = shift;
        my $url = $self->argument_value('url');
    
        my $shrunkenurl = ShrinkURL::Model::ShrunkenURL->new;
        $shrunkenurl->load_by_cols(url => $url);
    
        if ($shrunkenurl->id) {
    
            # for the benefit of report_success
            $self->record($shrunkenurl);
    
            # for the benefit of the template that displays new shrunken URLs
            # this is called in a superclass which we bypass
            $self->result->content(id => $shrunkenurl->id);
    
            # this too is called in a superclass
            $self->report_success;
    
            # Create actions return object's ID
            return $shrunkenurl->id;
        }
    
        return $self->SUPER::take_action(@_);
    }
    
    # display a nice little message for the user
    sub report_success {
        my $self = shift;
        $self->result->message(_("URL shrunked to %1", $self->record->shrunken));
    }
    
    1;
    
  • Template: ShrinkURL\lib\ShrinkURL\View.pm
    テンプレートエンジンとしてTemplate::Declareを使って、URL入力画面のレイアウト情報を定義しています。Template::Declareの記述方法については以前テンプレートの記述もPerlで - Template::Declareを使うというエントリーを書きましたので参考にしてみてください。 また、Jiftyでは画面上の表示領域をregionという単位で分割して管理することができます。regionの詳細はマニュアルを参照してください。
    #!/usr/bin/env perl
    package ShrinkURL::View;
    use strict;
    use warnings;
    use Jifty::View::Declare -base;
    
    template 'shrink' => page {
    
        # render the "shrink a URL" widget, which we can put on any page of the app
        render_region(
            name => 'new_shrink',
            path => '/misc/new_shrink',
        );
    
        # render an empty region that we push results onto
        render_region(
            name => 'new_shrinks',
        );
    };
    
    template '/misc/new_shrink' => sub {
        my $action = new_action(class => 'CreateShrunkenURL');
        form {
            Jifty->web->form->register_action($action);
            render_action($action => ['url']);
    
            form_submit(
                submit  => $action,
                label   => _('Shrink it!'),
    
                onclick => [
                    { submit => $action },
                    {
                        # prepend this result onto the empty region above
                        region => 'new_shrinks',
                        prepend => '/misc/shrunk_region',
                        args => {
                            id => { result_of => $action, name => 'id' },
                        },
                    },
                ],
            );
        };
    };
    
    template '/misc/shrunk_region' => sub {
        my $id = get 'id';
        my $shrunken = ShrinkURL::Model::ShrunkenURL->new;
        $shrunken->load($id);
    
        if ($shrunken->id) {
            div {
                strong { a { attr { href => $shrunken->shrunken } $shrunken->shrunken  } };
                outs _(" is now a shortcut for %1.", $shrunken->url);
            }
        }
    };
    
    1;
    
  • Model: ShrinkURL\lib\ShrinkURL\Model\ShrunkenURL.pm
    URLを管理する為のDBのテーブル定義と、テーブル上のデータに対する操作が記述されています。JiftyではDBアクセスにはJifty::DBIモジュールをデフォルトで使用します。
    #!/usr/bin/env perl
    package ShrinkURL::Model::ShrunkenURL;
    use strict;
    use warnings;
    use Number::RecordLocator;
    my $generator = Number::RecordLocator->new;
    
    use Jifty::DBI::Schema;
    use Jifty::Record schema {
        column url =>
            is distinct,
            is varchar(1000);
    };
    
    # shrunken URL is just an encoding of ID
    sub shrunken {
        my $self = shift;
        Jifty->web->url(path => $generator->encode($self->id));
    }
    
    # helper function so we can easily change the internal representation of
    # shrunken URLs if we desire
    sub load_by_shrunken {
        my $self = shift;
        my $shrunken = shift;
        my $id = $generator->decode($shrunken);
    
        return $self->load($id);
    }
    
    # prepend http:// if the scheme is not already there
    sub canonicalize_url {
        my $self = shift;
        my $url = shift;
    
        $url = "http://$url"
            unless $url =~ m{^\w+://};
    
        return $url;
    }
    
    1;
    



  • ShrinkURLをいじる

    構造の概要が分かったところでソースに実際に手を加えて機能を追加してみましょう。

    フォーム仕様を変更
    URL入力フォームの形式の変更、入力チェックの追加を行います。

    JiftyではDBのスキーマ(テーブル)定義とそれに対応するフォームの項目の定義を一箇所に定義することが可能です。

    ShrinkURL\lib\ShrinkURL\Model\ShrunkenURL.pmのスキーマ定義を以下のように変更してください。
    use Jifty::Record schema {
        column url =>
            is distinct,
            is varchar(1000),
            is mandatory,           # 必須項目に指定
            label is 'URL:',        # 入力フィールドのラベル
            render as 'textarea',   # 形式をテキストエリアに
            hints is _('Please enter URL.'), # 入力フィールドの補助的な説明文
            ajax validates,         # 入力チェックなどを非同期に実行
    };
    

    簡単な画面仕様であればテーブル項目と画面の入力項目は一致することが多いと思うので、
    同じような項目定義をViewやModelなどに分散して記述しないで一元管理できるのは便利ですね。

    また、独自に入力チェックを定義したい場合は、「validate_カラム名」という命名規則でメソッドを定義すれば自動で呼び出されます。

    # URLのフォーマットの妥当性チェック
    sub validate_url {
        my $self = shift;
        my $url = shift;
    
        # 手抜きですが'.'の有無だけチェック
        if ($url !~ m{[.]}) {
            return (0, _('Your URL is invalid.'));
        }
        return 1;
    }
    

    ちなみにActionが実行された時にDBにアクセスしない場合などは、Actionにフォーム仕様を記述することも可能です。
    その場合は、Action(ここではShrinkURL\lib\ShrinkURL\Action\CreateShrunkenURL.pm)に以下のように定義を記述してください。

    use Jifty::Param::Schema;
    use Jifty::Action schema {
        param url =>
            type is 'text',
            ajax validates,
    };
    

    その他に指定できるパラメータや詳しい仕様はJifty::DBI::SchemaJifty::Param::Schemaに書かれています。


    ■実行結果
    sc0002.png

    登録済みリストを表示
    元のアプリでは登録済みのURLが表示されないのでフォーム下部に一覧表示させてみます。 Modelに一覧データ取得用のメソッドを、Viewに一覧のレイアウトを追加します。

    1. ShrinkURL\lib\ShrinkURL\Model\ShrunkenURL.pm に以下のメソッドを追加

    sub list {
        my $self = shift;
    
        my $urls = ShrinkURL::Model::ShrunkenURLCollection->new;
        $urls->unlimit;
    
        my @shrunkens;
        while (my $url = $urls->next) {
            push @shrunkens, {
                url => $url->url,
                shrunken => Jifty->web->url(path => $generator->encode($url->id))
            };
        }
        return @shrunkens;
    }
    

    2. ShrinkURL\lib\ShrinkURL\View.pm に以下のテンプレートを追加・修正

    # URLリスト表示用テンプレートを追加
    template '/shrink_list' => sub {
        my @urls = ShrinkURL::Model::ShrunkenURL->list;
    
        for my $url (@urls) {
            ul {
                li {
                    strong { a { attr { href => $url->{shrunken} } $url->{shrunken}  } };
                    outs _(" : shortcut for %1", $url->{url});
                };
            }
        }
    };
    
    # new_shrinkテンプレートの最後に上記shrink_listテンプレートの呼び出しを追加
    template '/misc/new_shrink' => sub {
        my $action = new_action(class => 'CreateShrunkenURL');
        form {
            Jifty->web->form->register_action($action);
            render_action($action => ['url']);
    
            form_submit(
                submit  => $action,
                label   => _('Shrink it!'),
    
                onclick => [
                    { submit => $action },
                    {
                        # prepend this result onto the empty region above
                        region => 'new_shrinks',
                        prepend => '/misc/shrunk_region',
                        args => {
                            id => { result_of => $action, name => 'id' },
                        },
                    },
                ],
            );
        };
        # show registered URL;
        show '/shrink_list';
    };
    


    ■実行結果
    sc0003.png

    メッセージの日本語化
    最後に、英語で表示されている各種メッセージを日本語化します。 JiftyではLocale::Maketext::Extractモジュールを使用して アプリケーションで使用されるメッセージを管理しています。 ここまでサンプルを読んでいる時に _('some message.') という関数があちこちで呼ばれていたのに気がつきましたか? この関数で指定された文字列をキーに外部ファイルで各言語でのメッセージを設定することができます。

    以下のコマンドを実行すると、日本語用のファイルの雛形が生成されます。
    ※ShrinkURLには生成先のフォルダが作成されていないのでShrinkURL\share\poフォルダを予め作成しておいてください。
     (雛形生成スクリプトでイチからアプリケーションを作っていればデフォルトで用意されています。)

    C:\tmp\ShrinkURL>perl bin\jifty po --language ja
    

    コアプリケーション内のメッセージを抽出したファイルがShrinkURL\share\po\ja.poという名前で生成されます。
    ファイル内のヘッダー部にあるContent-Typeのcharsetをutf-8に変更したあと、
    メッセージ部に日本語を以下のような要領で書き加えてからサーバを起動すると、日本語のメッセージに置き換えられて実行されます。

    #: lib/ShrinkURL/.View.pm.swp:7 lib/ShrinkURL/View.pm:68
    #. ($url->{url})
    msgid " : shortcut for %1."
    msgstr ": %1のショートカット"
    
    #: lib/ShrinkURL/.View.pm.swp:7 lib/ShrinkURL/View.pm:56
    #. ($shrunken->url)
    msgid " is now a shortcut for %1"
    msgstr "は%1のショートカットです。"
    
    #: lib/ShrinkURL/Model/.ShrunkenURL.pm.swp:23 lib/ShrinkURL/Model/ShrunkenURL.pm:16
    msgid "Please enter URL."
    msgstr "URLを入力してください。"
    
    #: lib/ShrinkURL/.View.pm.swp:7 lib/ShrinkURL/View.pm:29
    msgid "Shrink it!"
    msgstr "実 行"
    
    #: lib/ShrinkURL/Action/.CreateShrunkenURL.pm.swp:15 lib/ShrinkURL/Action/CreateShrunkenURL.pm:34
    msgid "This URL is already exists."
    msgstr "そのURLは登録済みです。"
    
    #: lib/ShrinkURL/Action/.CreateShrunkenURL.pm.swp:15 lib/ShrinkURL/Action/CreateShrunkenURL.pm:41
    msgid "URL shrunked to "
    msgstr "URLのショートカットが作成されました。"
    
    #: lib/ShrinkURL/Action/.CreateShrunkenURL.pm.swp:15 lib/ShrinkURL/Action/CreateShrunkenURL.pm:50
    #. ($self->record->shrunken)
    msgid "URL shrunked to %1"
    msgstr "URLのショートカットが作成されました。"
    
    #: lib/ShrinkURL/Model/.ShrunkenURL.pm.swp:23 lib/ShrinkURL/Model/ShrunkenURL.pm:57
    msgid "Your URL is invalid."
    msgstr "正しいURLを入力してください。"
    


    ■実行結果
    sc0004.png

    まとめ

    サンプルアプリケーションの実行や改造を通して、Jiftyベースで開発する場合のアプリケーション構造を駆け足で見てきました。
    TemplateまでPerlで記述する部分は慣れが必要かもしれませんが、DispatcherとActionがフレームワークレベルで明確に分かれていてイベントドリブンな感覚で作れるのは個人的にとても分かりやすいと思いました。
    あとはAction・Template・Modelに関連するフォーム定義をDRYに記述できるのも素晴らしいです。

    Webアプリケーションを開発するといっても、規模や用途によって最適なアプリケーション構造は変わってくると思います。
    最小構成ではHTML+Javascript又はPHPのファイル1個で済む場合もありますし、大きなシステムではCatalystのような重厚なフレームワークを使って拡張性やカスタマイズ性を求めるケースもあると思います。
    またDBのテーブルと画面仕様が1対1になるようなシンプルなアプリケーションではRailsなどでサックリ作るのが最適でしょう。

    エリック・レイモンドがなるべく複数の異なるプログラミング言語を学ぶべきと語っていたように、もっと上のレイヤーのアプリケーションフレームワークについても1つに依存せずにいろいろ試して実際に開発してみることが大切なように思います。様々な設計思想に触れてみることでもしかしたら素晴らしい悟り体験に出会えるかもしれません:)

    特にPerl使いの方にはフルスタックのフレームワークに接する機会は少ないと思うので、Jiftyを使ってサクサクしたWebアプリケーション開発を体験してみるときっと楽しいですよ。