ふかくていWicket第3夜 on Rails

盟友ティッシ卿が、かの連載によってこの世界に対してすばらしい貢献をしているというのに、こちらはそれを享受するだけでよいのだろうか?

そういうわけで、俺も書こう、比較対象を。というわけで、ふかくていWicket第3夜の内容をRuby on Railsで実装してみることにした。これまでAJAXは軽視してきているので、まともに書いてみたことが無い。勉強半分。

準備

RoRはもう使えるという前提。モデルは使わないから、DBいらない。

プロジェクト作成
$ rails piyo_rails
(なんかいっぱいできる)
$ cd piyo_rails
日本語環境向け設定

config/environment.rbのトップに次の一行を追加する。

$KCODE = 'utf8'

app/controllers/application.rbを次のようにする。

この設定だと、AJAXを使ったアプリケーションを作成する際に問題が出る。この件についてはこちらの記事を参照。

# Filters added to this controller apply to all controllers in the application.
# Likewise, all the methods added will be available for all controllers.

class ApplicationController < ActionController::Base
  # Pick a unique cookie name to distinguish our session data from others'
  session :session_key => '_piyo_rails_session_id'

  # ココから下を足す
  before_filter :set_charset

  def set_charset
    headers["Content-Type"] = "text/html; charset=UTF-8"
  end
  # ココまで
end
デフォルトページ設定

デフォルトだとpublic/index.htmlになっているが、独自のページに差し替える。

config/routes.rbに次の一行を足す。

  map.connect ':controller/:action/:id.:format'
  map.connect ':controller/:action/:id'

  # ココの一行を追加
  map.connect '', :controller => 'home'
end

homeページ(というかコントローラ)はこれから作る。

public/index.htmlは邪魔なので消す。消さない限りこいつがデフォルトであり続けるので、消さないとダメ。

レイアウトファイル作成

これから作るページは全部AJAXを使うので、関係するjsファイルをインクルードするレイアウトファイルを作っておく。

views/layouts/ajax.rhtmlを作って、次のような中身にする。

<html>
  <head>
    <title><%= @title %></title>
    <%= javascript_include_tag "prototype" %>
  </head>
  <body>
    <%= @content_for_layout %>
    <%= link_to 'Back', '/' %>
  </body>
</html>
デフォルトページ作成

とりあえずコントローラ作る。Homeコントローラを、indexアクション付きで生成する、の意。

$ ruby script/generate controller Home index
(なんかいろいろできる)

app/views/home/index.rhtmlを編集する。

<h1>ホームページ</h1>
<ul>
  <li><%= link_to "AJAX時計", :controller=> 'clock' %></li>
  <li><%= link_to "Textfield Sample", :controller => 'textfield_sample' %></li>
  <li><%= link_to "Indicating Textfield Sample", :controller => 'indicating_textfield_sample' %></li>
</ul>

3種類のページを実装する次第である。

お試し

とりあえず、動くか見てみる。

$ ruby script/server

ブラウザでhttp://localhost:3000/にアクセス。

こんなんが出ればOK。

AJAX時計

src/piyo.web.page.form.AjaxClockPage相当。

なんでこいつからかというと、こいつが一番実装が簡単だからだ。

コントローラを作る。

$ ruby script/generate controller Clock index
(なんかたくさんできる)

app/views/clock/index.rhtmlを編集する。

<h1>AJAX時計</h1>

<%= periodically_call_remote(:url => { :action => 'update' },
                             :update => 'clock',
                             :frequency => 1.second) %>

<div id="clock">じかん</div>

簡単。periodically_call_remoteという、長ったらしくて覚えにくいメソッドを書いてやると、:frequency周期で非同期通信が走る。設定値は別に"1"だけでもいいのだが、せっかくActiveSupportがあるので"1.second"にしてみた。

app/controller/clock_controller.rbを編集する。

class ClockController < ApplicationController
  # 先に作っておいたlayoutを適用
  layout 'layouts/ajax'

  # @titleの設定値がページタイトルになる。
  # そのようにlayouts/ajax.rhtmlに書いた。
  def initialize
    @title = "AJAX Clock"
  end

  def index
  end

  # periodically_(ry から呼び出されるアクション。
  # 現在時刻を返すだけ。
  def update
    render :text => DateTime.now.strftime('%Y-%m-%d %H:%M:%S')
  end
end

できた。ブラウザのトップページから"AJAX時計"リンクをクリック。

表示できた?眺めていると、時間がカウントアップされていくのがわかる。

インジケータ付きテキストフィールド

src/piyo.web.page.form.TextFieldPage3相当。

実にAJAXらしい、インジケータ付きのテキストフィールドを書く。WicketのIndicating〜のような便利コンポーネントは無いので、自力で実装。

なによりも、インジケータになるアニメーションgifが必要だ。ネットから適当に探してくればよし。俺は面倒くさいので、Wicketが使ってるgif(indicator.gif)を引き抜いて使うことにする。

まず、indicator.gifを、public/images/に置く。

で、コントローラ作成。

$ ruby script/generate controller IndicatingTextfieldSample index

app/views/indicating_textfield_sample/index.rhtmlを編集する。

<h1>Indicating Textfield Sample</h1>

<div id="message">メッセージ</div>

<% form_remote_tag(:url => {:action => 'update'},
                   :update => 'message',
                   :loading => "Element.show('indicator')",
                   :complete => "Element.hide('indicator')") do %>

  <%= text_field_tag :newitem %>
  <%= image_tag "indicator.gif", :id => 'indicator', :style => 'display:none;' %>
  <br/>
  <%= submit_tag "送信!" %>

<% end %>

複雑に見えるので解説。

インジケータ(indicator.gif)は、image_tagメソッドで埋め込む。こいつはファイル名filenameを渡すとpublic/images/filenameのファイルをsrcにしたimgタグを生成する。このとき、CSS設定で非表示にしておく。

で、form_remote_tagの:loadingで通信開始、:completeで通信終了のイベントを拾って、Javascriptを使ってindicatorの表示/非表示を切り替える。これで、通信中のみインジケータが動いているように見える。

AWDwR本ではobserve_fieldを使っていたが、これでもいいよな?

app/controllers/indicating_textfield_sample_controller.rbを編集する。

class IndicatedTextfieldSampleController < ApplicationController

  layout 'layouts/ajax'

  def initialize
    @title = "Indicating Textfield Sample"
  end

  def index
  end

  def update
    # 通信中状態を作るために、少しスリープ。
    sleep 3.seconds
    render :text => params[:newitem]
  end
end

では、ブラウザに戻って、デフォルトページの"Indicating Textfield Sample"リンクをクリック。

ちゃんとインジケータが動く。なかなか格好いい。

テキストフィールド with Behavior

src/piyo.web.page.link.TextFieldPage4相当の処理。

とりあえず、コントローラ作る。

$ ruby script/generate controller TextfieldSample index

app/views/textfield_sample/index.rhtmlを編集する。

<h1>Textfield Sample</h1>

<div id="message">メッセージ</div>

<% form_remote_tag(:url => {:action => 'back_to'}) do %>
  <%= text_field_tag :newitem, '',
    "onblur" => remote_function(:update => 'message',
                                :url => {:action => 'update'},
                                :with => 'Form.Element.serialize(this)') %>
  <%= submit_tag "送信!" %>
<% end %>

onblurが今回の主役。フォーカスが外れるとAJAXな呼び出しがかかって、テキストフィールドの値をメッセージに反映させる。remote_functionメソッドを使うと、任意のコントローラ/アクションへのAJAX呼び出しを行うJavascriptコードを生成することができる。このとき、:withでFormをパラメータ化して渡すことを忘れずに。これにハマって結構時間を使った・・・。このくらい自動でやってくれないもんかな。

次、コントローラ。app/controllers/textfield_sample_controller.rbを編集。

class TextfieldSampleController < ApplicationController
  layout 'layouts/ajax'

  def initialize
    @title = 'TextField Sample'
  end

  def index
  end

  # onblurで呼び出されるのはコイツ。メッセージ書き換えるだけ。
  def update
    render :text => params[:newitem]
  end

  # formをsubmitするとコイツが呼ばれる。
  def back_to
    unless params[:newitem].downcase == "hello work!"
      render :update do |page|
        # 訂正。下の書き方のほうがスマート
        # page << "alert('Hello Work!と入れてほしい')"
        page.alert 'Hello Work!と入れてほしい'
      end
    else
      render :update do |page|
        # 訂正。下の書き方のほうがスマート
        # url = url_for :controller => "home"
        # page << "window.location = '#{url}'"
        page.redirect_to :controller => "home"
      end
    end
  end
end

updateアクションはどうでもいいとして、back_toアクションがちょい複雑か。

入力値の検証をサーバーサイドで実施して、エラーならJavascriptで警告ダイアログを表示させる。render :update do ...は、RJSをコントローラ内で直接記述するための記法だ。page << "javascript"とすると、クライアントサイドでJavascriptとして実行される。類似のメソッドにpage.callというものがあるが、これはfunctionを直接呼び出すという点で<<とは異なる。ここでもハマって時間を浪費したorz

で、悩んだのが、エラーが無かった場合にホームページにリダイレクトする処理。普通ならばredirect_toを使うのだが、受け取ったのがAJAXによる非同期リクエストなのでうまく機能しない。仕方がないので、リダイレクトするJavascriptコードを作ってクライアントに送り込むことにした。本当にこの実装方法でいいんかいな?

・・・不安はつのるが、とりあえず動かしてみる。

すまん、もっと賢いやり方があった。リダイレクトはpage.redirect_to urlでいける。メッセージボックスの表示も、page.alert messageでいける。やっぱりドキュメントには目を通さないといかんよな・・・。



画面を出していないが、ちゃんとリダイレクトする。まぁ、大丈夫そう。てか、hereじゃなくてhearだろう、俺。Coolすぎる。


・・・とまぁ、ここまで実装してみたわけだが。rhtmlはもはやhtmlじゃねぇな(汗)見た目の簡潔さはWicketが圧倒的だ。RoRといえばWeb2.0、ぐらいに言われることもあるのだが、ことAJAXに関してはあまりRoRにアドバンテージがあるように思えないなぁ。再利用も難しかろう。もっとも、再利用性については、あそこまでのコンポーネント化を成し遂げたWicketが特別なのかもしれない。

しかし、コントローラはRoRのそれが圧倒的に簡素だ。まぁ、Wicketがコントローラでやってることをビューでやっているのだから、そうなるのは自然だ。設計方針の違いが見える。

RoRはあまり下のレイヤを隠そうとしていない。ActiveRecordやRJSでラッピングできるようになってはいるが、SQLJavascriptも必要があればためらわずに書け、だ。使える技術を使わない理由は無い、ということか。実際、prototype.jsやらの使い方がわかってくると、さほど苦でも無くなってくる。なんにしろ、玄人向けのFWという印象がより強まった。