Ruby on RailsのAjax処理のおさらい

何気に、アプリケーションで取り扱うデータ量が増えた場合や、大容量データをデータベースとやり取りする時なんかは、Ajaxを使ってデータ通信をバックエンド側に押しやることで、WEBフロント側のUXからパフォーマンスの悪さを改善できたりする。また、ネットワーク経路的にproxyサーバとかを中継するような環境間でデータのやり取りをする場合などに、proxy側で接続時間にリミットがかけてあったりすると、通常アクセスではデータ通信時間がリミットに達して503エラーとかになってしまうような処理でもAjaxで通信をバックエンド化することで、回避できたりもするのだ。
通信帯域が小さいスマートデバイスが主力である今のご時勢、Ajaxによる非同期処理は、言語やフレームワークを問わずに必須な技術になっている。
私の主力スキルはPHPなので、PHP＋JavaScript（jQuery）やWordPressでのAjax処理なら特に造作もなく作れていたんだが、今回Rubyでのアプリケーション開発をして、Ruby on Rails（バージョン 4.2.0）でのAjax処理の開発にかなり手を焼いた次第。確かに、広く云われているようにRailsでのAjax処理はお手軽で生産性が高いのだけれど、私の知見的にはかなり癖がある作り方が必要だった。応用を利かそうとすると毎度シビれる状況に陥ったりしてなかなかに大変だった──という経験から、一度ここらでRails＋Ajaxについての情報を整理しておこうと思ったのだ。  

## 環境情報

まずは本記事における私のRails環境情報をまとめておく。RubyやRailsではバージョンによって結構コーディング条件や使える関数が異なったりするので、これは重要。

- Ruby 1.9.3
- Ruby on Rails 4.2.0
- jquery-rails 4.0.3 （※ Railsに依存するgemなのでインストール時に自動で入るハズ？）
- jQuery 1.11.2（※ 2015/1/19時点。Rails の Turbolinkに よって自動で最新版が読み込まれる）
- coffee-script 2.3.0 (※ Railsに依存するgemの coffee-rails は 4.1.0)


## 一般的なAjax処理のおさらい

Railsに限らず、世間一般のAjax処理の全体な流れを図にすると下記のようになるのかな──と。

![Ruby on RailsでのAjax処理の概要](http://ka2.org/assets/2015/01/ajax-general.png "Ruby on RailsでのAjax処理の概要")

古典的なWEBアプリケーションだと、ボタンアクションや画面遷移などのユーザー側からの明確なイベント後に、おもにWEBページの読込処理を伴ってサーバーサイドとやり取りをするんだが、Ajaxだとユーザーからの明示的なアクションがなくても、およそJavaScript側で取得できるすべてのイベントから発火させることができ、さらにページ遷移などを伴わずにリアルタイムにフロントエンドに結果を反映できるのが大きな特徴だ。非同期で実行すれば、ブラウザ側の機能がロックされることもないので、データがすべて読み込まれるまでユーザーに待ちを強いるようなこともないし、都度最小限のデータアクセスでアプリケーションを動かすことができるようになるので、ユーザビリティやアクセシビリティを最大化させられる。

あと重要なのが、Ajaxの非同期処理はイベント発火順のシーケンシャルには処理されないということだ（下図参照）。

![非同期なAjaxチェーンのレスポンス](http://ka2.org/assets/2015/01/ajax-general_2.png "非同期なAjaxチェーンのレスポンス")

JavaScript側で、1つのイベントに対して非同期設定のAjaxで複数のリクエスト送信を行うような場合、そのレスポンスがリクエスト順には戻ってこないのだ。これは、Ajax処理を連結してAjaxチェーンを作る時などに注意しておかないと、意図しない結果になったりする。  

## RailsでのAjax処理のおさらい

Ruby on Railsでは View 側のヘルパータグ（`form_tag`）等で `:remote => true` を指定するだけで該当フォームでのsubmitアクションをAjax化してくれる。

##### **Viewの例:**

```erb:index.html.erb
<%= form_tag({:controller => '<コントローラ名>', :action => 'index'}, :remote => true, :class => 'form-ajax') do -%>
<%= hidden_field_tag :ajax_handler, 'handle_name1' -%>
<%= submit_tag('Start Ajax', :class => 'btn btn-primary btn-sm') -%>
<% end %>
```

Controller側ではその アクションに対応する処理を行い、`render` でレスポンスを指定する。

##### **Controllerの例:**

```rb:XXXXs_controller.rb
class XXXXsController < ApplicationController
  def index
    ajax_action unless params[:ajax_handler].blank?
    
    # Ajaxリクエストではない時の処理
    
  end

  def ajax_action
    if params[:ajax_handler] == 'handle_name1'
      # Ajaxの処理
      @data = Datum.all
      if @data.size > 0
        render
      else
        render json: 'no data'
      end
    end
  end
end
```

デフォルトではレスポンスとしてフロントエンド側にJavaScriptの処理が戻ってくる。そのレスポンス用JavaScriptは View 側にJavaScriptテンプレートを用意する。

##### **JavaScriptテンプレートの例:**

```js:index.js.erb
if ($('#ajax-response-wrapper').size() === 0) {
  $('body').append('<div id="ajax-response-wrapper"></div>');
} else {
  $('#ajax-response-wrapper').html('');
}
$('#ajax-response-wrapper').html('<%=j render partial: "ajax_container", collections: @data %>');
```

この例では、レンダリングされたインスタンス変数`@data`を整形したHTMLとして表示するため、さらにJavaScriptから partial を使ってhtmlテンプレートを呼んでいる。

##### **partialテンプレートの例:** （※ `_<partial名>.html.erb` ）

```erb:_ajax_container.html.erb
<ul>
<% @data.each do |datum| %>
<li><%= datum.slug %></li>
<% end %>
</ul>
```

これだけで（…といっても結構手間だが）Ajax処理（正常系のみ）は完結してしまうのだ（この辺の詳しい解説は色々なサイトに紹介されているので本記事では割愛する）。一番凄いと思ったのが、Ajax関連のJavaScriptなんぞ一切コーディングせずともAjax処理ができてしまうという点だ。このあたりがRailsでのAjax処理の生産性が高い理由でもある。
ただ、実用的なアプリケーション開発を行う場合、このような単純な処理だけでは機能不足なことが多くて、エラー処理やAjax中のview制御などもう少し突っ込んだハンドリングが最低限必要になってくる。
例えば、前述の例だと、controllerでDatumモデルにレコードがない場合エラーにしているが、フロントエンド側でエラー用のJSONをキャッチする処理がないため、ユーザーがエラーを認識できないのだ。ではAjaxのエラー処理のサンプルを書いてみる。

##### **Ajaxエラーキャッチ用JavaScriptの例:** （※ app/assets/javascript/ 内）

```coffeescript:XXXXs.coffee
XXXXs = ->
  $('.form-ajax').on 'ajax:error', (e, xhr, status, error) ->
    alert xhr.responseText
  
$(document).ready(XXXXs)
$(document).on('page:load', XXXXs)
```

RailsのJavaScript側でのAjax制御の実装は、上記のようにAjax通信の発火フォームのイベントハンドラとして定義する。しかしまぁ、ここまで書いてきて改めて思ったが、単純な1つのAjax処理に対してあまりにも手を入れなければならないファイルが多い。これだと管理が大変だ。前出のJavaScriptテンプレートやpartialテンプレートを使わず、controllerからは処理結果を一律JSONでレンダリングして、コントローラ用のCoffeeScriptにAjaxの後続処理を一元化してしまった方が全体の処理として見通しが良くなる気がする。
──ということで、この記事では前出のようなRailsの独自実装型仕様に極力依存せずに、フロントエンドのAjax関連処理はJavaScript（jQuery）に集約するような建付けでやってみる。

## jQueryでのAjaxイベント

そもそもJavaScriptでAjaxを取り扱う際は、素のJavaScriptよりjQueryでやった方が圧倒的に簡単だ。コードの見通しが良いのはもちろん、コード量も抑えられるためコーディングコストが低いというメリットもある。幸いにもRailsはデフォルトでjQueryが使える環境になっているし、さらにCoffeeScriptで書けるので、スクリプト部分のコーディングがとても楽だ（CoffeeScriptの作法に馴れるまではちょっと違和感があったが…）。
──そんなわけで、本記事でのJavaScriptコードはCoffeeScript+jQueryで書いてあるのでご注意を。

さて、まずは一般的なjQueryでのAjaxイベントをまとめてみた。

| イベント名 | Railsでのイベント名 | 呼び出しタイミング | 第1引数 | 第2引数 | 第3引数 | 備考 |
|:--------:|:---------:|:----------------|:------:|:------:|:------:|:----|
| Before | ajax:before | 全Ajaxリクエストの開始前 | - | - | - | イベント停止時、Ajaxリクエストは中断 |
| BeforeSend | ajax:beforeSend | Ajaxリクエストが送信される直前 | XHR | settings | - | イベント停止時、Ajaxリクエストは中断 |
| Send | ajax:send | Ajaxリクエストの送信直後（通信成功時） | XHR | - | - |  |
| Error | ajax:error | 通信成功後、サーバーがエラーを返した時 | XHR | status | error | jQuery 1.7系まで |
| Success | ajax:success | 通信成功後、HTTPレスポンスが成功した時 | data | status | XHR | jQuery 1.7系まで |
| Complete | ajax:complete | （HTTPレスポンスの結果に関係なく）Ajaxリクエストが完了時 | XHR | status | - | jQuery 1.7系まで |
| Fail | fail() | 旧errorの後継イベント。sendイベントでのXHRオブジェクトのメソッドとして呼び出す | XHR | status | error | jQuery 1.8以降推奨 |
| Done | done() | 旧successの後継イベント。sendイベントでのXHRオブジェクトのメソッドとして呼び出す | data | status | XHR | jQuery 1.8以降推奨 |
| Always | always() | 旧completeの後継イベント。sendイベントでのXHRオブジェクトのメソッドとして呼び出す | XHR/data | status | error/XHR | jQuery 1.8以降推奨。HTTPレスポンスがエラーか正常かで引数が変化する |
| Aborted | ajax:aborted:required | Ajaxリクエスト中断時（空リクエスト型） | elements | - | - | 中断された際でも空リクエストとして送信される |
| Aborted | ajax:aborted:file | Ajaxリクエスト中断時（ファイル破棄型） | elements | - | - | 中断された際、ファイルアップロード用フィールドのバイナリデータは送信されない |
| Then | then() | イベントのレスポンス結果取得時 | doneCallbacks | failCallbacks | progressCallbacks | Ajax専用イベントではないが、イベントのレスポンス結果によってCallback処理を行えるため、Ajaxのチェーン処理には欠かせない |

よくWebでRailsのAjax系記事を調べると、旧（ふる）いjQueryのAjaxイベントである「success」や「error」を使って書いてあるのが多くて、今どきのjQuery＋Ajaxの推奨実装と合ってないよなぁ…とか思っていたんだが、その原因はRailsのjQuery＋Ajaxの処理実体である[jquery-ujsの実装](https://github.com/rails/jquery-ujs/wiki/ajax)によるところが大きかった。
本記事では、その jquery-ujs の仕様に依存することなく、jQuery自体の推奨実装で処理を書いていこうと思う。

## RailsでjQuery推奨仕様に準じてAjax処理を書く

さて、具体的な処理例があった方がわかりやすいので、サンプルとしてそこそこ実用的（？…かもしれない）なAjax処理を定義してみる。

> X軸Y軸が可変である範囲座標系テーブル内に、データベースから該当するデータのみをAjaxで取得してきて表示する。
> 

いやぁ、文字で書くと**まったくもって実際の仕様がピンとこない**もんだな…実用的なのかも怪しいもんだが、まぁいいや（笑）

とりあえず、Ajax処理のサンプルアプリ用のrailsアプリを作成する。

```bash:
$ rails new ajax-test
$ rails g controller coordinates index show
$ rake db:create
$ rails g model datum name:string axis_x:integer axis_y:integer axis_slug:string description:text
$ rake db:migrate
```

ここはあえて詳しく解説しないが、`ajax-test`というrailsアプリを新規作成して、`coordinates`というコントローラと`Datum` というデータベースを作った次第。次にDBに入れ込むデータが欲しいので、`db/seeds.rb`に初期データ作成用のスクリプトを書く。

```rb:db/seeds.rb
require 'digest/md5'
for i in 1..10000 do
  x = rand(100) + 1
  y = rand(100) + 1
  slug = "#{x}-#{y}"
  md5_hash = Digest::MD5.new.update("#{i}").to_s
  Datum.create(name: md5_hash, axis_x: x, axis_y: y, axis_slug:slug, description: nil)
end
```

初期データの説明としては、100×100の座標範囲内にランダムに10000個の点を打った感じ。それぞれの点にはmd5ハッシュ形式のnameが付けてある。`axis_slug`はフロントエンドのHTML側にデータを表示する時に利用する、数値座標のエイリアス的なものだ。
で、seedができたので、流し込む（10000レコードあるのでちょいと時間がかかる）。流し込んだら、コマンドラインでDBにデータ入ってるか確認してみよう。

```bash:
$ rake db:seed
$ rails db
> .table
data               schema_migrations
> select count(*) from data;
10000
```

次に、Ajaxリクエスト発火時のルーティングを設定する（`config/routes.rb`）。すでに定義済みのルートを利用してコントローラ側でAjax処理を振り分けてもいいのだが、コードの見通しが悪くなるので、Ajaxのルートは別途定義した方が後々保守しやすくなる。

```rb:config/routes.rb
Rails.application.routes.draw do
  get 'coordinates/index'
  get 'coordinates/show'
  post 'coordinates/get_area'
end
```

実際に追加するのは、最後の一行の `post 'coordinates/get_area'` だけ。
そして、Ajaxレスポンスの処理実体であるコントローラを作る（`app/controllers/coordinates_controller.rb`）。

```rb:app/controllers/coordinates_controller.rb
class CoordinatesController < ApplicationController
  def index
    @range = {
      min_x: 	params[:min_x] 	||= 0,
      max_x: 	params[:max_x] 	||= 20,
      min_y: 	params[:min_y] 	||= 0,
      max_y: 	params[:max_y] 	||= 20,
    }
  end

  def show
  end

  def get_area
    @data = Datum.where('axis_x >= ? AND axis_x <= ? AND axis_y >= ? AND axis_y <= ?', params[:min_x], params[:max_x], params[:min_y], params[:max_y])
    
    if @data.size > 0
      render json: @data
    else
      render json: "No data"
    end
  end
end
```

通常アクセス時は `/coordinates/index` であり、Submitボタンを押すとAjax開始で `get_area` のメソッドが呼ばれるというルーティングになっている。Ajax処理のサーバーサイド側の実装は `get_area` メソッドで、データベースから指定範囲内に属するデータのみを取得して、JSONレスポンスとしてフロントエンドに返すというものだ。

今度はフロントエンド側を作って行く。まずはベースとなるHTMLテンプレートとして、HTML文書の大枠を定義する `app/views/layouts/application.html.erb` だ。

```html+erb:app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Ajax test app</title>
  <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track' => true %>
  <link href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css" rel="stylesheet">
  <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
  <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
  <%= csrf_meta_tags %>
</head>
<body>

<%= yield %>

</body>
</html>
```

ここはそのままでも問題ないのだが、テーブルなどの見た目を整えたいので bootstrap をCDN経由で読み込むようにしている。そして、次にRailsアプリのフロントエンドのテンプレートを作る。パスは `app/views/coordinates/index.html.erb` である。

```html+erb:app/views/coordinates/index.html.erb
  <div class="container">
    <h2>Coordinates#index</h2>
    <table class="table table-striped table-bordered table-condensed table-hover">
<% @range[:max_y].downto(@range[:min_y]) do |row| %>
      <tr class="row-<%= row %>">
  <% @range[:min_x].upto(@range[:max_x]) do |col| %>
    <% if col == 0 %>
      <% if row == 0 %>
        <th><%= col %></th>
      <% else %>
        <th><%= row %></th>
      <% end %>
    <% else %>
      <% if row == 0 %>
        <th><%= col %></th>
      <% else %>
        <td class="rc<%= col %>-<%= row %>"></td>
      <% end %>
    <% end %>
  <% end %>
      </tr>
<% end %>
    </table>
    <%= form_tag coordinates_get_area_path(), :method => :post, :class => 'form-get-coordinates form-inline' do %>
      <div class="form-group">
        <label class="sr-only" for="commit">Set Range</label>
        <div class="input-group">
          <div class="input-group-addon">X-Min</div>
          <%= number_field_tag :min_x, @range[:min_x], in: 0...100, :class => 'form-control', :readonly => true %>
        </div>
        <div class="input-group">
          <div class="input-group-addon">X-Max</div>
          <%= number_field_tag :max_x, @range[:max_x], in: 1..100, :class => 'form-control', :readonly => true %>
        </div>
        <div class="input-group">
          <div class="input-group-addon">Y-Min</div>
          <%= number_field_tag :min_y, @range[:min_y], in: 0...100, :class => 'form-control', :readonly => true %>
        </div>
        <div class="input-group">
          <div class="input-group-addon">Y-Max</div>
          <%= number_field_tag :max_y, @range[:max_y], in: 1..100, :class => 'form-control', :readonly => true %>
        </div>
      </div>
      <%= submit_tag 'Get Coordinates', :class => 'btn btn-primary submit-ajax' %>
    <% end %>
  </div>
```

テーブルを動的に生成するところがちょいと見づらいが仕方ない。あと、本当はテーブルの可視範囲を下部の入力欄から指定できるようにしようと思っていたのだが、上のテーブル部分と連携させるのが結構面倒そうだったので、とりあえず `readonly` 化して入力できないようにしてある（今回やりたいAjax処理のおさらいとはちょっと外れちゃうんで割愛ッス…）。
そして、ようやくAjaxリクエスト部のJavaScript（jQuery）をCoffeeScriptで書く。パスは `app/assets/javascripts/coordinates.coffee` だ。

```coffeescript:app/assets/javascripts/coordinates.coffee
coordinates = ->
  
  $('.submit-ajax').on 'click', (e) ->
    e.preventDefault()
    getCoordinatesInRange()
  
  getCoordinatesInRange = ->
    post_data = {
      min_x: $('#min_x').val()
      max_x: $('#max_x').val()
      min_y: $('#min_y').val()
      max_y: $('#max_y').val()
    }
    jqXHR = $.ajax({
      async: true
      url: $('.form-get-coordinates').attr('action')
      type: $('.form-get-coordinates').attr('method')
      data: post_data
      dataType: 'json'
      cache: false
      beforeSend: (xhr, set) ->
        $('td[class^="rc"]').html('')
    })
    
    jqXHR.done (data, stat, xhr) ->
      console.log { done: stat, data: data, xhr: xhr }
      data.forEach (obj) ->
        $(".rc#{obj.axis_slug}").append("<div class=\"center-block\"><a href=\"/coordinates/show/#{obj.id}\">#{obj.id}</a></div>")
    
    jqXHR.fail (xhr, stat, err) ->
      console.log { fail: stat, error: err, xhr: xhr }
      alert xhr.responseText
    
    jqXHR.always (res1, stat, res2) ->
      console.log { always: stat, res1: res1, res2: res2 }
      alert 'Ajax Finished!' if stat is 'success'

$(document).ready(coordinates)
$(document).on('page:load', coordinates)
```

最後に、見た目をさらによくするためのスタイルを追加する。パスは `app/assets/stylesheets/coordinates.scss`

```scss:app/assets/stylesheets/coordinates.scss
.table th, .table td {
  text-align: center;
  vertical-align: middle;
  min-width: 3em;
}
.table th {
  background-color: #e8e8e8!important;
  color: #888888;
}
```

これで完成だ。早速、ブラウザで `<Railsが動いている環境のホスト名>/coordinates/index` にアクセスしてみると、下記のようなページが表示される。

![Ajaxテスト用サンプルアプリ（Ajax前）](http://ka2.org/assets/2015/01/ajax-sample-app1.png "Ajaxテスト用サンプルアプリ")

テーブル下の「Get Coodinates」ボタンを押すとAjax処理が開始し、データベースから20×20の範囲内座標を持っているデータを全て取得してきて、テーブル内に表示される（下図参照）。

![Ajaxテスト用サンプルアプリ（Ajax後）](http://ka2.org/assets/2015/01/ajax-sample-app2.png "Ajaxテスト用サンプルアプリ")

もしこのページが、テーブルのセルを作成しながら、同時に合致するデータを各セルに入れ込みつつレンダリングするような仕様だった場合、ちょっと考えただけでもかなり面倒そうな処理を書かないといけなくなりそうで気が重くなる。なので、テーブルの枠だけサクっと作ってから、Ajaxでデータを流し込むというやり方は、結構スマートな方法なんではないだろうか…。

本当はこのサンプル、先にテーブルの表示範囲を定義するAjax処理を行ってから、今回のデータ取得処理をチェーンさせて、連続するAjax処理の進捗率をリアルタイムに表示させる例としてまとめておきたかったんだが…この記事自体が想像以上に長文になったので、ここいらでやめておく。途中Railsアプリの作り方みたいな手順が入って本筋から外れてしまったのが痛かったかな…（笑）

まぁ、Ajaxチェーンのおさらいはまた別の機会にやろうかと。