Kentaro Kuribayashi's blog

Software Engineering, Management, Books, and Daily Journal.

Catalyst で作る簡単 Web アプリケーション: Feed2JS 解説

さて、激しく頭の悪そうなタイトルですが。先日のエントリ「Catalyst 事始め」 で書いた、Catalyst を用いて作成した Feed2JS のソースを公開せよという圧力(違)がかかったので、手順をちと書いてみます。つーか、せっかくの機能をまったく活かされていないというアレなのであんまり意味ないかとは思いますが、そもそも Catalyst を用いたコード例自体があんまりない現状なので、こういうことしちゃバカ呼ばわりされますよ! という悪い例としてでも、まぁないよりはマシかな、と。

動作確認した環境(つーか、このサーバです)は以下の通り。

OS
debian linux
Apache
1.3.33
Perl
5.8.4
mod_perl
1.29

ほんでもって、httpd.conf(というか、ヴァーチャルホスト用の設定箇所)に、こんな感じの記述を適当に付け足す。

<Location />
    PerlSetEnv      PERL5LIB /home/kentaro/subtech/SubTech/lib
    PerlInitHandler Apache::StatINC
    SetHandler      perl-script
    PerlHandler     SubTech
</Location>

Apache2 + mod_perl2 にはやく移行したいなー、と思いつつ、なかなか踏ん切りがつかなくて停滞中。なんてことはどうでもよくて、まずはお定まり、ヘルパースクリプトであれこれ生成(パッケージのトップは SubTech)。

$ catalyst.pl SubTech

ver 5.10 からは、script ディレクトリ以下に自動生成されるヘルパースクリプト群に、この例でいうと subtech_create.pl みたいな感じで、prefix がつくようになったみたいです。んでもってついでに view も作りますよ。

$ script/subtech_create.pl view TT TT

これで lib/SubTech/V/TT.pm が作成されます。これは作りっぱで、以降は放置。あー、そうだ。後述する Ajax 用の prototype.js も最初に作っておきます。

$ script/subtech_create.pl Prototype

root ディレクトリ下に prototype.js という JavaScript ファイルが生成されます。後ほど利用。

んでもって、ここからコード書き。catalyst.pl によって作成された lib/SubTech.pm を適当に変更。

lib/SubTech.pm

package SubTech;

use strict;
use Catalyst qw(Static Prototype);

SubTech->config(
    name => 'subtech.zapto.org',
    root => '/home/kentaro/subtech/SubTech/root',
    templates => {
        index   => 'templates/index.tt',
        feed2js => {
            index  => 'templates/feed2js/index.tt',
            result => 'templates/feed2js/result.tt',
        },
    }
);

SubTech->setup;

sub default : Private {
    my ($self, $c) = @_;

    if ($c->req->path =~ m|\.[^/]+$|) {
        $c->serve_static;
    } else {
        $c->stash->{template} = SubTech->config->{templates}{index};
        $c->forward('SubTech::V::TT');
    }
}

1;

SubTech->config() にテンプレ等を置くディレクトリ(root)を指定したりとか、あれこれ。もっとちゃんとした設定の書き方があるんじゃないかという気がするけど、気のせいということに。use Catalyst qw(Static Prototype); として、Catalyst::Plugin::StaticCatalyst::Plugin::Prototype を読み込みます。前者は Catalyst から CSS とか画像とかを直接 serve するためのプラグイン。後者は prototype.jsCatalyst によるアプリケーションで利用しやすくするためのプラグイン。

んで、上の

if ($c->req->path =~ m|\.[^/]+$|) {
    $c->serve_static;
}

とか書いてるところで、.css とかそんな URL がリクエストされたら、通常はモジュール / アクションにマップされるところを、root ディレクトリ下にあるファイルをそのまんま吐きますよ、という処理。httpd.conf の記述を工夫すればこんなことしなくてもいいかもしれないけど。つーか上の記述、これじゃ全然ダメです。超適当過ぎる(w

ともあれ、とりあえずトップページのテンプレート。

root/templates/index.tt

<?xml version="1.0" encoding="utf-8"?>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
	<head>
		<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
		<link rel="stylesheet" href="/styles/main.css" type="text/css" media="all" />
			<title>[% name %]</title>
	</head>

	<body>
		<h1>[% name %]</h1>
		<p>サブカルテクノロジ実験場</p>

		<h2>contents</h2>
		<dl>
			<dt><a href="/feed2js" title="Feed2JS">Feed2JS</a></dt>
			<dd>RSS の各ヴァージョンや Atom による XML feed を、Blog のサイドバー等に貼り付けられるような JavaScript に変換するサービスです。</dd>
		</dl>

		<address>kentaro: <a href="kentarok☆gmail.com" title="メールを送信するに際しては、☆を@に書き換えてください">kentarok☆gmail.com</a></address>

</body>
</html>

特になんということもない感じ。ともあれ、これで http://subtech.zapto.org/ でアクセスするとトップページが表示されるようになります。

んで、トップページを作ったので、今度は Feed2JS を作ります。というか、この文章は僕がやったことに基づいて書いているので lib/SubTech.pm を書いて、おもむろに Feed2JS を作るとかゆってますが、その辺はそれぞれのやりたいように。とゆわけで、Feed2JS のコントローラを作成。

$ script/subtech_create.pl controller Feed2JS

これで、lib/SubTech/C/Feed2JS.pm が作成されます。ここにあれこれ書いていきます。

lib/SubTech/C/Feed2JS.pm

package SubTech::C::Feed2JS;

use strict;

use Cache::FileCache;
use XML::Feed::JavaScript;
use URI::Escape qw(uri_escape);

use base qw(Catalyst::Base);

sub default : Private {
    my ( $self, $c ) = @_;

    $c->stash->{template} = SubTech->config->{templates}{feed2js}{index};
    $c->forward('SubTech::V::TT');
}

sub serve : Local {
    my ($self, $c) = @_;
    my $max = $c->req->parameters->{max} || '10';
    my $enc = $c->req->parameters->{enc} || 'utf8';
    my $uri = $c->req->parameters->{uri};
    my $errstr;
    my $js;

    unless ($uri) {
        $errstr = 'Feed の URI が入力されていません。';
    } else {

        my $cache = new Cache::FileCache({
            namespace          => 'feed2js',
            default_expires_in => 60*60*6,
        });

        my $id = $max.$enc.$uri;
        $js = $cache->get($id);

        unless ($js) {
            my $feed = XML::Feed::JavaScript->parse(URI->new($uri));

            unless (XML::Feed::JavaScript->errstr) {
                $js = $feed->as_javascript({
                    max      => $max,
                    encoding => $enc,
                });
                $cache->set($id => $js);
            } else {
                $errstr = 'Feed を解析できませんでした。';
            }
        }
    }

    if ($c->req->parameters->{check}) {
        $c->stash->{template} = SubTech->config->{templates}{feed2js}{result};
        $c->stash->{param}    = {
            max => $max,
            enc => $enc,
            uri => uri_escape($uri),
        };
        $c->stash->{errstr}   = $errstr;
        $c->forward('SubTech::V::TT');
    } else {
        $c->res->content_type('application/x-javascript');
        $c->res->output($errstr || $js);
    }
}

1;

特に何をいうこともない駄コードなので、投げ遣りな塩梅。http://subtech.zapto.org/feed2js というような URL だと普通に index.tt テンプレをプロセスして返し、http://subtech.zapto.org/feed2js/serve だと serve メソッドが実行され、キャッシュチェックの後 XML Feed を取得、チェックし JavaScript に変換、パラメタにより JavaScirpt の URL や、あるいは JavaScript そのものを返します。つーか、TT フィルタですればいいものをわざわざ URI::Escape::uri_escape() で URI エスケープしてるのがアレなのですが、なんかよくわかりませんが、テンプレ側で [% param.uri | uri | html %] とかしてもエスケープされなかったのですよね……。めどいので、てけとう対処。

ここで使うテンプレートは以下のふたつ。root/templates/feed2js/index.tt は Feed2JS のトップページ。root/templates/feed2js/result.tt は、Feed のチェック等を行うバックエンド(serve メソッド)が返すデータのためのテンプレ。

root/templates/feed2js/index.tt

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <link rel="stylesheet" type="text/css" href="/styles/main.css" media="all" />
        <script src="/prototype.js" type="text/javascript"></script>
        <script type="text/javascript">
        <!--
            function nowLoading () {
                var target = document.getElementById('result');
                target.innerHTML = '<p><img src="/images/arrow.gif" /></p>';    
            }
        -->
        </script>
        <title>Feed2JS</title>
    </head>

    <body>

        <p id="navi"><a href="/" title="subtech.zapto.org">Top</a> -> Feed2JS</p>

        <h1>Feed2JS</h1>
        <p>指定した Feed (RSS 0.91, 1.0, 2.0 or Atom) を JavaScript に変換します。<strong>このサービスは実験的なものです</strong>。</p>

        [% c.prototype.form_remote_tag({
            url     => '/feed2js/serve',
            update  => 'result',
            loading => 'nowLoading()',
        })%]
            <input type="text" name="uri" size="50" value="Feed の URI を入力" onfocus="this.value='';">
            <select name="max">
                <option value="3">3</option>
                <option value="5">5</option>
                <option value="10" selected="selected">10</option>
                <option value="15">15</option>
            </select> 件
            <select name="enc">
                <option value="shiftjis">Shift_JIS</option>
                <option value="eucjp">EUC-JP</option>
                <option value="utf8" selected="selected">UTF-8</option>
            </select>
            <input type="hidden" name="check" value="1" />
            <input type="submit" value="送信" /><br />
        </form>

        <div id="result"></div>

        <address>kentaro: <a href="mailto:kentarok☆gmail.com" title="メールを送信するに際しては、☆を@に書き換えてください">kentarok☆gmail.com</a></address>

    </body>
</html>

root/templates/feed2js/result.tt

[% IF errstr %]
    <p>[% errstr %]</p>
[% ELSE %]
    <p>以下の URI をコピーして使用してください。</p>
    <pre>http://subtech.zapto.org/feed2js/serve?&uri=[% param.uri | html %]&max=[% param.max | html %]&enc=[% param.enc | html %]</pre>
[% END %]

ここで見ていただきたいのが、root/templates/feed2js/index.tt の以下の箇所。

[% c.prototype.form_remote_tag({
    url     => '/feed2js/serve',
    update  => 'result',
    loading => 'nowLoading()',
})%]

テンプレにこのように書いておくだけで、prototype.js を用いてフォームの各パラメタを XMLHttpRequest で /feed2js/serve に送信、load 中は loading で指定した関数を実行、バックエンドから返ってきたデータを update で指定した id(ここでは result)で示される要素につっこむための form 要素開始タグを生成してくれます。具体的には以下のような感じ。

<form onsubmit="new Ajax.Updater( 'result',  '/feed2js/serve', { parameters: Form.serialize(this),asynchronous: 1,onLoading: function(request){nowLoading()} } ) ; return false;">

input の内容を onsubmit や onkeyup イベント時に XMLHttpRequest でバックエンドに送信して、処理をした結果をそのまま書き出すみたいな、よくある Ajax 的なものなら、こんなもんでさくっと作れます。他にもいろいろできるみたいですが、よくわからないので Catalyst::Plugin::Prototype のドキュメント等を御覧になってください。

これでようやっと http://subtech.zapto.org/feed2js で Feed2JS にアクセスでき、また、http://subtech.zapto.org/feed2js/serve?[パラメタ] で Feed を JavaScript に変換したものが書き出されるようになります。

というか、DB 使ったあぷぷだとこの文章ももすこしは意味のあるものになる気がしますが、こんなんじゃ面白くもなんともないなぁという気がしますよ。まぁ、アウトプット主義的には、なにもないよりはマシかなということでひとつ。