目次


作って学ぶ、今どきのWebサービス

第7回 Webアプリケーション開発にチャレンジ(後編)

Comments

コンテンツシリーズ

このコンテンツは全#シリーズのパート#です: 作って学ぶ、今どきのWebサービス

このシリーズの続きに乞うご期待。

このコンテンツはシリーズの一部分です:作って学ぶ、今どきのWebサービス

このシリーズの続きに乞うご期待。

WWW::OpenSearchを使った検索Webアプリケーション

前回「Webアプリケーションを作るための仕組み」は、フレームワークとしてCGI::Applicationが用意されていることを解説し、その使い方も紹介しました。これまでの内容を踏まえて、目的のWebアプリケーションは次のような仕様にしました。

  • 検索語句を指定して検索
  • 検索エンジンを一覧から選んで検索できる

まずは、リスト1のようにCGI本体を記述します。これをapp.cgiとしましょう。app.cgiではCGI::Application::Dispatchを使うので、リスト中の1の部分のようにして自作クラスを置くライブラリディレクトリを指定しておきます。

リスト1 CGI本体(app.cgi)
#!/usr/local/bin/perl
use strict;
use lib qw (/path/to/lib);            ←1
use CGI::Application::Dispatch;

CGI::Application::Dispatch->dispatch(
    PREFIX => 'UU',
    DEFAULT => 'Index',
);

そして、指定したライブラリディレクトリに、CGI::Applicationを継承したクラスを作っていきます。たとえばUU::Foobarクラスなら、/path/to/lib/UU/Foobar.pmになります。

トップページのapp.cgi/indexに対応するUU::Indexから作っていきましょう*リスト2)。まず3行目ですが、CGI::Application::Dispatchを使った場合でも、ロジックを書くクラスがCGI::Applicationのサブクラスなのは一緒です。「use base」で継承します。

リスト2 app.cgi/indexに対応するUU::Index(便宜上、行番号を示しています)
1 package UU::Index;
2 use strict;
3 use base qw (CGI::Application);
4 use CGI::Application::Plugin::TT;
5
6 sub cgiapp_init {
7     my $self = shift;
8     $self->tt_config(
9         TEMPLATE_OPTIONS => {
10             INCLUDE_PATH => "/path/to/template",
11         },
12     );
13 }
14
15 sub cgiapp_prerun {
16     my $self = shift;
17     $self->header_props( -charset => 'utf-8' );
18 }
19
20 sub setup {
21     my $self = shift;
22     $self->start_mode('index');
23     $self->run_modes(
24         index => 'do_index',
25     );
26 }
27
28 sub do_index {
29     my $self = shift;
30     my $q = $self->query;
31     $self->tt_process('index.tt', { query => $q });
32 }
33
34 1;

4行目ではCGI::Application::Plugin::TTモジュールをuseで読み込んでいます。CGI::ApplicationからTTを使うには、TTのインスタンス生成から出力作成までを自分で行うという手もありますが、プラグインを使うことで、もっと簡単に扱うことができます。プラグインはuseするだけでOK、必要なメソッドがインポートされます。

6行目から13行目にかけて、TTにオプションを渡す処理を追加しています(INCLUDE_PATHでテンプレートのディレクトリを一括指定しています)。CGI::Applicationでは、アプリケーションの起動時にcgiapp_initというメソッドが呼ばれることになっています。アプリケーションの初期化処理などを書きたい場合は、このcgiapp_initをオーバーライドしてそこにロジックを追加していきます。

15行目から18行目のcgiapp_prerunはページへのリクエストごとに呼ばれるロジックです。ここでは出力の文字コードをUTF-8に指定するため、header_propsでヘッダーに出力のcharsetを追加しています。

CGI::Applicationインスタンスのtt_processを呼ぶと、TTを使った出力が可能です。28行目から32行目のように、INCLUDE_PATHオプションでテンプレートのディレクトリを/path/to/templateに設定し、tt_processでファイル名をindex.ttと指定すると、このクラスに対応したテンプレートとして/path/to/template/index.ttが使用できるようになります。今回はリスト3のようなテンプレートを記述してみました。

リスト3 テンプレートindex.tt
<html>
<head><title>Open Search</title></head>
<body>
<h1>Open Search</h1>

<form action="./app.cgi/search" method="get">
  <input type="text" name="word">
  <select name="osxml">
    <option value="http://search.hatena.ne.jp/osxml">Hatena</option>
    <option value="http://bulkfeeds.net/opensearch.xml">Bulkfeeds</option>
  </select>
  <input type="submit" value="search">
</form>

</body>
</html>

ただし、リスト3ではマクロを使っていません。マクロを使わないのならテンプレートエンジンなど使わず、単なるHTMLでもいいのですが、将来的にたとえばtitleを外出しの設定ファイルから拾ってきたいといった拡張もありそうなので、TTで出力しておきます。

これで最小限のプログラムは出そろいました。app.cgiを、CGIが実行できる環境に置き、クラスやテンプレートをプログラムの中で指定したとおりの場所に配置して実行(app.cgiにアクセス)します。すると図1が表示されます。

図1. トップページを出力
図1. トップページを出力
図1. トップページを出力

まだ検索フォームから検索語を打っても動きません。そこで、検索フォームのアクション先であるapp.cgi/searchに相当するクラスとテンプレートを用意しましょう。トップページがapp.cgi/indexでIndex.pmだったのと同じく、app.cgi/searchなのでクラス名はUU::Searchになります。

と、ここでちょっと戻ってUU::Index(リスト2)を良く見てみると、4行目からの処理は、どうやらUU::Searchでも同じように書く必要がありそうです。同じロジックが2つのクラスに存在しているのはいまいちなので、ここは継承を使って書き換えます。UU::Appというスーパークラスを作って、UU::IndexとUU::Searchはそれを継承させましょう。UU::Appはリスト4のようになります。また、UU::Indexを書き換えて、UU::Appを使うようにします(リスト5)。ずいぶんスッキリしましたね。続けてUU::Searchを見ていきましょう(リスト6)。

リスト4 UU::App
package UU::App;
use strict;
use base qw (CGI::Application);
use CGI::Application::Plugin::TT;

sub cgiapp_init {
    my $self = shift;
    $self->tt_config(
        TEMPLATE_OPTIONS => {
            INCLUDE_PATH => "/path/to/template",
        },
    );
}

sub cgiapp_prerun {
    my $self = shift;
    $self->header_props( -charset => "utf-8" );
}

1;
リスト5 UU::Appを使うよう書き換えたUU::Index
package UU::Index;
use strict;
use base qw (UU::App);

sub setup {
    # 以前と同じ
}

sub do_index {
    # 以前と同じ
}

1;
リスト6 UU::Search
package UU::Search;
use strict;
use base qw (UU::App);
use WWW::OpenSearch;

sub setup {
    my $self = shift;
    $self->start_mode('search');
    $self->run_modes(
        search => 'do_search',
    );
}

sub do_search {
    my $self = shift;
    my $q = $self->query;
    my $osxml = $q->param('osxml') || "http://search.hatena.ne.jp/osxml";
    my $engine = WWW::OpenSearch->new($osxml);
    $engine->pager->current_page($q->param('page') || 1);    -----(A)
    $self->tt_process('search.tt', {
        query => $q,
        word => $q->param('word'),
        osxml => $osxml,
        engine => $engine,
        result => $engine->search($q->param('word')),
    });
}

1;

do_searchでやっていることはコマンドラインのスクリプトとほとんど同じです。入力がコマンドライン引数からCGIのクエリパラメータに変わっただけですね。異なる個所といえば、(A)の部分でしょうか。これはページングの処理で、検索結果に2ページ目以降がある場合にそれを表示できるよう、パラメータをセットしています。

一方、テンプレートのsearch.ttですが、リスト7のような感じになります。テンプレートについて少し解説しておきましょう。

リスト7 検索のテンプレートsearch.tt(便宜上、行番号を示しています)
1 <html>
2 <head><title>Open Search - [% word | html %]</title></head>
3 <body>
4 <h1><a href="../app.cgi">Open Search</a></h1>
5
6 [% USE FillInForm %]
7 [% FILTER fillinform fobject => query %]
8 <form action="search" method="get">
9   <input type="text" name="word">
10   <select name="osxml">
11     <option value="http://search.hatena.ne.jp/osxml">Hatena</option>
12     <option value="http://bulkfeeds.net/opensearch.xml">Bulkfeeds
13 </option>
14   </select>
15   <input type="submit" value="search">
16 </form>
17 [% END %]
18
19 [% pager = engine.pager -%]
20 <p>Search results [% pager.first %] - [% pager.last %] of [% pager.total_entries %] 
for <strong>[% word | html %]</strong> from [% engine.ShortName %]</p>
21 <dl>
22 [% FOREACH item IN result.items %]
23   <dt><a href="[% item.link | html %]">[% item.title %]</a></dt>
24   <dd>[% item.description %]</dd>
25 [% END %]
26 </dl>
27
28 <p>
29 [% IF pager.previous_page %]
30 <a href="search?word=[% word | uri %]&page=[% pager.previous_page %]
&osxml=[% osxml | uri %]">prev</a>
31 [% END %]
32
33 [% FOREACH num = [pager.first_page .. pager.last_page] %]
34 [% IF num == pager.current_page %][[% num %]]
35 [% ELSE %]<a href="search?word=[% word | uri %]&page=[% num %]
&osxml=[% osxml | uri %]">[[% num %]]</a>[% END %]
36 [% END %]
37
38 [% IF pager.next_page %]
39 <a href="search?word=[% word | uri %]&page=[% pager.next_page %]
&osxml=[% osxml | uri %]">next</a>
40 [% END %]
41 </p>
42
43 </body>
44 </html>

Googleなどの検索エンジンでは、トップページから検索して検索結果の画面に来たときも、フォームの中には使った検索語句が表示されます。細かい話なのですが、使い勝手に大きく影響する部分です。そこでGoogle同様に、指定された検索語句と検索エンジンの結果を、検索結果の画面でも保持するようにしたい、ということでTemplate::Plugin::FillInFormというTTのプラグインを使っています(6行目~16行目)。

TTのプラグインは、CPANでインストールした後、このようにテンプレートの中で「[% USE FillInForm %]」などとすれば使うことができます。FillInFormプラグインを使うと、「[% FILTERfillinform …… %]」~「[% END %]」で囲まれた個所にあるformの各値が、fobjectで与えられたクエリを自動で保持するようになります。

19行目~20行目では、WWW::OpenSearchのインスタンス(engine)からpagerメソッドでインスタンスを取り出し、これに対してメソッドを使って検索結果の件数などを表示しています。WWW::OpenSearchでは、pagerメソッドでData::Page(ページング処理を管理するCPANモジュール)のインスタンスが取得でき、このインスタンスが検索結果の件数やページ数などの情報を持っています。

最後に、29行目~40行目の部分はいわゆるページャを表示するための個所です。マクロがちょっとたくさんありますが、よく見るとやっていることは簡単で、要は前のページがあったら「prev」のリンクを表示、次のページがあったら「next」のリンクを表示、後は各ページへのリンクを表示しているだけです。

さて、このクラスとテンプレートも適宜配置して、先のトップページから適当な語句で検索してみましょう。エンジンをBulkfeedsに変えると、検索結果がはてな検索からBulkfeedsのものに変わることが確認できるかと思います。FillInFormプラグインで、クエリの内容や選んだエンジンも保持されていて、いい感じですね。スタイルシートで見た目を調整してやればもう少し格好いい表示ができるかと思います。

MVCモデルによるWebアプリケーションの構築

足早でしたが、CGI::Application::DispatchやTT、それからWWW::OpenSearchを使ってWebアプリケーションを作成してきました。いまどきのWebアプリケーションプログラミングがどのようなものかを感じ取っていただけたでしょうか。

途中に紹介した、いわゆる「いまどき」でないパターンでのプログラムに比べて、今回紹介したプログラムはファイルの数も多いし、コードの量も多いしであまり効率的でないのではないか、と思われた方もいらっしゃるかもしれません。

そういう場合は、後に機能拡張あるいはメンテナンスをすることを考えてみてください。あそこを変えるにはここを変えればいいんだ、というのが、次のようにまとまってそれぞれ分離されているのがお分かりいただけるかと思います。

  • トップページのデザインを変更するならindex.tt
  • 検索結果のデザインを変更するならsearch.tt
  • Open Searchによる検索の仕方を変更するならWWW::OpenSearch
  • トップページに渡すデータを増やすならUU::Index
  • 検索ページに渡すデータを増やすならUU::Search

ではなぜ、きれいに分離できたのでしょうか。実は「MVCモデル」というアプリケーションの設計手法を意識して作ったからでした。MVCは、

  • M = Model(処理の中核)
  • V = View(見た目)
  • C = Controller(MとVの制御)

の3層に分けてアプリケーションを作りましょう、という考え方です。今回は、

  • Model = WWW::OpenSearch
  • View = テンプレートファイル
  • Controller = CGI::Applicationのクラス群

といったように分離されているのが、お分かりいただけるかと思います。このようにプログラムの処理を3層に分けることで、機能の拡張やメンテナンスを行うのに、その対象範囲が限定されるとともに、担当者をべつべつに割り当てることができるようになります。今回「MVCを意識した」と言っても、やったことと言えばTTとCGI::Application::Dispatchを使っただけです。つまりフレームワークに身を任せただけ、ということです。

このように、いまどきのWebアプリケーション作成では、MVCモデルを前提にしたフレームワークを使ってきれいに3層に分けて開発する、というのが主流だったりします。うまく設計されたフレームワークをきちんと使うと、よりエレガントで機能追加のしやすいアプリケーションを作ることができます。フレームワークが注目を浴びる理由はここなんですね。

終わりに

これまで7回にわたって、Webを題材にした「いまどきのPerlプログラミング」を紹介してきました。総じて言えるのは、昨今のプログラミングにおいては中核処理のロジックを考えるということ以上に、アプリケーションを構成する部品をどこから集めてどう組み合わせるか、が重要だということです。

いまどきのWebプログラマに必要なのは、そういった部品を集めるアンテナやセンスと言えるでしょう。

このページで出てきた専門用語

トップページのapp.cgi/indexに対応するUU::Indexから作っていきましょう
なお、app.cgiではDEFAULTに「Index」を指定しているので、app.cgiに直接アクセスするとUU::Indexにディスパッチされ、app.cgi/indexと同様の画面が表示されます。

ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Web development, Open source, Java technology
ArticleID=247531
ArticleTitle=作って学ぶ、今どきのWebサービス: 第7回 Webアプリケーション開発にチャレンジ(後編)
publish-date=04202007