おどおどしつつCatalystをモバイルサイト開発に導入してみた:モバイルコントローラー編

せっかくようやっと本題であるモバイル開発の話がでると思っていたんですが、エントリの内容の正当性に自信がないのでいろいろ悩んでいたら、更新が滞ってしまいました。。まだちょっと悩み中ですがとりあえず公開してみます。

文字コードの扱い方針

文字コード取り扱いの基本ルールは以下のようにしました。

具体的には、まずルートコントローラー(MyApp::Conteroller::Root)で、$c->req->paramから、utf8フラグなしのUTF-8エンコーディング文字列に変換します。変換した値は、$c->stash->{params_utf8_encoded}に保存しています。

sub begin : Private {
    my ( $self, $c ) = @_;
    # ...
    my $enc = $agent->getEncoding; # x-utf8-docomo とか
    $c->stash->{params_utf8_encoded} = {
        map {
            (   $_ => encode(
                   'utf8', decode( $enc, ( $c->req->param($_) )[0] )
                )
                )
            } $c->req->param
    };
    # ...
}


出力については、ビューレンダリングの後に、処理を追加すべく、endメソッドをrenderメソッドとendメソッドに分割。

sub render : ActionClass('RenderView') {
}

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

    # エラー処理
    $c->forward('error');

    # レンダリング
    $c->forward('render');

    $c->fillform();
}

ここはCatalyst知ってる人だと定番の書き方だけど、初心者にはホントわからなかった。


次に、モバイルサイトを構築する場合、PCサイトだけ特設ページにすることが多く、違うテンプレートからレンダリングしたりすることがあります。そのため、View::TTのprocessメソッドをオーバーライドして、クライアントに応じてinclude_pathを書き換えて、選択的にテンプレートをピックアップできるようにしました。


さらにこのprocessの時点で端末に応じたエンコーディングにencodeするようにしています。*1


擬似コードで書くと、こんな感じ↓

package MyApp::View::TT;

use strict;
use warnings;
use base 'Catalyst::View::TT';

use NEXT;

use HTTP::MobileAgent;

use Encode;
use Encode::JP::Mobile qw/:props/;
use Encode::JP::Mobile::Charnames;

sub process {
    my ($self, $c) = @_;
    
    my $agent = HTTP::MobileAgent;
    my $is_mobile = !($agent->is_non_mobile);

    if ($is_mobile) {
        # モバイルの場合テンプレート探索パスを適切に上書きする
        @{ $self->include_path } = $c->path_to( qw/root tt Mobile / );
    }
    else {
        # Pcの場合はPcのテンプレートを使用する
        @{ $self->include_path } = $c->path_to( qw/root tt Pc/ );
    }
    
    # 本来の処理
    $self->NEXT::process($c);
    
    # モバイルの場合文字コード変換
    if ($is_mobile) {
        my $encoding = $agent->encoding;
        my $charset = ($encoding =~ /sjis/) ? 'Shift_JIS' : 'UTF-8';
        my $content_type = ($agent->is_docomo) ? 'application/xhtml+xml' : 'text/html';
        $c->res->content_type($content_type . '; charset=' . $charset);
        
        # bodyの文字コード変換
        $body = $c->res->body;
        
        # 絵文字変換しつつ文字コード変換
        # ※実際は絵文字マッピングカスタマイズしてるがここでは省略
        $c->res->body(encode($encoding, decode('utf8' ,$body));
    }

    return 1;
}

課題

この間にPerl大御所のTomita氏がなんとタイムリーなことにCPAN モジュールを使って楽に携帯サイトを作る方法として、MobileCatを公開したことを知りました。

こちらを、参考にしたところ、いろいろと課題が浮かんできました。

パラメータのstash使用

前述のとおり、$c->req->paramから、端末ごとのエンコーディングからdecodeして$c->stash->{params_utf8_encoded}に保存していますが、これはCatalyst全体で内部的に変数をどのように処理していくか理解していなかったため、、$c->req->paramを直接書き換えるのがイヤだったので、$c->stash->{param_utf8_encoded}に別途格納していましたが、MobileCatのソースコードを見ていたら、そのまま書き換えていました。

sub begin : Private {
    my ( $self, $c ) = @_;
    #...
    for my $value (values %{ $c->req->{parameters} }) {
        next if ref $value && ref $value ne 'ARRAY';
         
        for my $v (ref($value) ? @$value : $value) {
            next if Encode::is_utf8($v);
            $v = $self->encoding->decode($v);
        }
    }
    #...

確かに、別の変数に格納してしまうとCatalyst::Plugin::FormValidator::Simpleをそのまま利用できず、わざわざFormValidator::Simpleを自前で導入するという手間を招いていました。今後は上記のとおり、入力時にそのまま変更してしまえばよさそうです。

utf8フラグの問題

これも同じ箇所なのですが、入力時の変数をutf8フラグなしのUTF-8エンコーディング文字列に変換しています。


従来の経緯からこのように扱っていたのですが、MobileCatではdecodeのみ行い、utf8フラグを立てたままで後の処理に渡しています。



ここのソースを見て、web上の資料を読み漁っていたところ、自分がそれまでしていた勘違いをしていたようです。Perl本来の方法としては、多言語を扱う場合、

  • 入力時にdecodeして、Perl内部で処理している限りUNICODEとして扱い、
  • 外部に出力する際に、出力先に応じてencodeする

というのが正当のようですね。

参考:


もちろんこの現在のやり方でも文字化けなどは発生しないのですが、

まとめ

  • スカラー変数は(リファレンス等は別として)下記のものを格納できる
    • (A) 文字列(内部表象: UTF-8
    • (B) 文字列(内部表象: ISO-8859-1)
    • (C) バイナリ列
      1. 純粋なバイナリストリーム(画像ファイル等)かもしれないし,
      2. UTF-8 octet stream かもしれないし,
      3. CP932 octet stream かもしれないし,etc, etc ...
  • Perl は(後方互換性確保などの理由から)ISO-8859-1 の文字範囲内に収まる文字列リテラルを記述すると,デフォルトで UTF8 フラグなし文字列(in ISO-8859-1 encoding)とする
  • スカラー変数が……
    • UTF8 フラグが立っている場合,(A) であると断定可能
    • UTF8 フラグが立っていない場合,(B) か (C) は断定できない(開発者の意図次第)
    • ただし,「文字列」であるなら (A) か (B) である,とみなすべき(Perl の慣例にしたがうと)
  • もし (A), (B), (C) を引数にとる関数を書くのであれば,引数が文字列か否かによって API をわけるべき;つまり,(A) や (B) を受け取る関数と (C) を受け取る関数にわけるべき
    • (Web プログラマは特に)UTF8 フラグがついていない文字列を(その対称性から)UTF-8 octet stream とみなすと便利だなーと考えがちであるが,その認識は誤り(A と C-2 をごっちゃにして扱っているから)


UTF8 フラグあれこれ - daily dayflower

上記でいうと、常に(C)-2として扱ってしまっていた、という訳です。もちろん通常は異常がないんでしょうが、上記を前提にしたAPIでうまく動かなくなってしまうと言うわけですね。


また、自分のフレームワークを要約すると、

  1. 入力をutf-8 octet streamに変換して別変数に格納、処理もその前提で実装
  2. RenderView時点でクライアントごとの文字コードでのoctet streamに変換
  3. sub endではクライアントごとの文字コードでのoctet streamとして処理する

となっているので、非常に歪ですね。この意味からも、前述のとおり$c->res->paramをdecodeしてしまった方が理にかなっていると言えます。

コントローラーってPCとモバイルで同一でいいのか

前述のとおり、テンプレートのみモバイルとPCで分けられるようにしていますが、これは従来のシステムでコントローラーをPCとモバイルで分けていたことから煩雑になったことがありますが、PCページがモバイルへの誘導(QRコードのみ)の場合は、特に分ける必要もないかなと思いました。


むしろ、コントローラにPC/モバイルのdispatch機能を持たせようとしてコードが煩雑化するのであれば、分割したほうがいいのかも。これはケースバイケースと言ってしまえばそれまでなんですが・・

*1:ここでは絵文字処理もやってるんですが、これについては、改めてエントリを割きます。