Edited at

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処理の概要

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

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

非同期なAjaxチェーンのレスポンス

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


RailsでのAjax処理のおさらい

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


Viewの例:


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の例:


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テンプレートの例:


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


_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/ 内)


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の実装によるところが大きかった。

本記事では、その jquery-ujs の仕様に依存することなく、jQuery自体の推奨実装で処理を書いていこうと思う。


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

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


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


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

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

$ 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に初期データ作成用のスクリプトを書く。


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にデータ入ってるか確認してみよう。

$ rake db:seed

$ rails db
> .table
data schema_migrations
> select count(*) from data;
10000

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


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)。


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 だ。


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 である。


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 だ。


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


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前)

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

Ajaxテスト用サンプルアプリ(Ajax後)

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

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

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