Railsでの基本的な開発スタイルといえば、ページはリンクとフォーム送信、そしてリダイレクトの基本構成だろう。確かにこれでWebアプリケーションを作ることができる。
ただ今回はよりリッチなWebアプリケーション、具体的にはAjaxを駆使した開発について、Railsでどうやって開発していけばグチャグチャにならずに簡潔に書けるのか、私が心がけている点を紹介したいと思う。これを読めば、別にクールなJavaScriptフレームワークを使わずとも、シンプルなjQueryで作れることを知ることができるだろう。
そもそもなぜリッチにするのか
リッチなWebアプリケーションにすれば、以下のような点のメリットがある。
- 毎回application.js や application.css、共通画像などを読み込む必要がなくなるため、サーバー負荷に優しい。
- ユーザーはページ遷移を意識せずにWebアプリを利用できるため、UXが高まる。クールなWebアプリが作れる。
リッチWebアプリはユーザーにとっても開発者にとっても利点がある。しかし、このメリットの背景には以下のようなデメリットがある。
- JavaScriptで状態を管理しなければならないため、JavaScriptコードが煩雑になる。
- グチャグチャなソースコードになりがち。どこでAjax呼んでるの?など
上記デメリットを以下に簡潔に書くか。それをRails上でどのように実現するのか。今回はそこに焦点を当ててノウハウをまとめる。
まずはコントローラ毎にコンポーネントを分ける
これめっちゃ大事。そして全体の基本になる話だ。例えば、 UsersControllerを作成した場合、Railsでは users.css.scss
, users.js
, users/index.html.erb
などが関連ファイルとして生成されることだろう。まずは以下のように<div>
もしくは<section>
タグで区切りをつけよう。
//scss
#userCmp {
...
}
// js
$(function() {
$("#userCmp..").click(function() {
....
})
});
// html
<div id="userCmp">
...
</div>
プログラミングの世界ではネームスペースとも言える概念だが、これをすると他のコントローラの影響を考えずにuserCmp
に集中してソースを書くことができる。これをやらないと、なぞのイベントが発生したり、CSSがグチャグチャになったりする。
htmlを返すアクション、jsonだけを返すアクションなどを使い分ける
Ajaxというと、どうもJSONだけでやりとりする、という考えを持っておられる方も多いようだが、別にhtmlをAjaxのレスポンスにすることも可能だ。これを知っておくだけでかなり開発の幅は広がる。例えば、グローバルタブがあって、Ajaxでレスポンスをhtmlにし、そのhtmlをcontentの中身として書き換えてみよう。
//html
<ul class="tab">
<li data="/items">Tab1</li>
<li data="/carts">Tab2</li>
</ul>
<div id="contents"></div>
// js
var ajaxHtml = function(url) {
$.ajax({
type: "GET",
url: url,
success: function(content){
$("#contents").html(content);
}
});
}
$(".tab li").click(function() {
var url = $(this).attr("data");
ajaxHtml(url);
});
// controller
def index
render layout: false
end
// index.html
<div id="itemCmp">
....
</div>
これがリッチWebアプリのベースとなる部分だ。ItemsController側も同様に itemCmp
を作成し、css, js はそれぞれitemCmpで予め限定しておこう。これだけでRailsのTurboLinksもどきの機能を実装できる(戻る機能を除く )。ちなみにタブの移動などは共通で呼び出すAjaxとなるので、本当ならちゃんとレスポンスコードによるエラー処理を書くべき。
場合によってはJSONを返すパターンももちろんあっていい。その際にHTMLをどこかに挿入したい、といった場合に全部jQueryでDOMを構築するのは超絶面倒なので、以下のようにdisplay: none
なテンプレートを用意してそれに当てはめるのがグッド。
<div id="template">
<!-- 複雑なHTML -->
<span id="name"></span>
</div>
var $template = $("#template").clone();
$template.find("#name").html(res.name);
$("#contents").content($template);
clone
しないと連続で読んだ時にtemplateが置き換わったグチャグチャのHTMLを再度取ってきてしまうので、この呼び出しが必要だ。
コンテンツ書き換え後のJavaScript処理に対応する
さて、ここまででタブによってAjaxでコンテンツを切り替える、というテクニックを見てきた。リッチアプリではコンテンツを切り替えた後のHTML要素もJavaScriptで動くように実装しなければならない。ただここで普通に $("#mybtn").click(..)
と書くだけでは、ロード時にこの要素は無い要素なので正しく動作しない。イベントに関してはon
を利用して以下のように書く必要がある。
//items.js
$(document).on("click", "#itemCmp .element", function() {
// code
});
これで動的に読み込んだ要素にもjQueryのイベントを発火させることができるようになる。
ここでさらなる問題が出てくる。それは"Ajaxで取ってきたコンテンツの初期処理をしたい"というパターン。RailsでAjaxでhtmlを返してすぐにJavaScriptを実行したい場合だ。その場合、assets/javascripts/内に
$(function() {
// 初期処理...
});
と書く方法は正しく動作しない。すでにドキュメントはページ全体で読み込まれた後にAjax呼び出しをするからだ。これを正しく動作するようにするには、Ajaxで取ってきたHTML内にscriptタグを書いて動作させる必要がある。ここでは trigger
でイベントを発火させ、js内でそのカスタムイベントを補足するようなコードを書く。
<div id="itemCmp">
....
</div>
<script>
$("#itemCmp").trigger("itemCmp:loeaded")
</script>
//js
$(document).on("itemCmp:loeaded", function() {
$("#itemCmp .item").append(...);
});;
再喝だが、このパターンを使わなければならないのはAjaxでHTMLを呼び出して呼び出し後にすぐJavaScriptを実行したい時だけ、ここに書くべきである。イベントの処理はここではなく、 $(document).on
にしてassets内に書くべきだ。なぜかというと、ご存知のとおりRailsはassets内のJSはプリコンパイルによってメソッド名が書き換えられるので、そうした共通処理はこのHTML内で呼び出すことができなくなるためである。
Railsの変数をJavaScriptで取得したい
JavaScriptを書いていると、Railsの変数の値をJavaScriptで使いたい、というパターンが頻出する。これに対応する最も簡単な方法は、"display: none"なスタイルで変数をまとめたHTMLを書くことだ。
<div class="hidden">
<span id="user_id"><%= @current_user.id %></span>
<span id="status"><%= @current_user.status %></span>
</div>
//js
parseInt($("#user_id").text());
ただ、この書き方で注意しなければなら無いのは、悪意のあるユーザーがこのHTMLの内容を書き換えた後にJavaScriptを実行させる、ということもできてしまう点にある。そのためRails側にリクエストを投げる時は必ずそのリクエスト内容が正しいかどうか検証するコードをお忘れなく。
HTMLが汚れて醜い、という方には、RailsとJSを共存させるためのGem gon があるので調べてみるといいだろう。
HistoryAPIで戻る処理を実現する
ここまでで、サーバーとAjax通信をしてインタラクティブなページをjQueryだけで作成することができるようになった。最後の関門は "ブラウザバック"をJavaScriptに対応させることだ。これにはHTML5からのHistoryAPIを利用する。ていっても最近はHistoryJSがあるので、これで楽々だ。
HistoryAPIの基本的な考え方としては、Historyスタックに履歴をどんどんプッシュしていく感じだ。プッシュされた場合や、戻るボタンを押された場合には、共通のイベント(statechange)が呼ばれる。この時にAjaxを呼び出してその状態を表示するような実装を行う。
// pushStateや戻るボタンが押された時に呼ばれる
History.Adapter.bind(window,'statechange',function(){
var State = History.getState();
ajaxHtml("/" + State.data.action);
});
// ページ移動時
History.pushState({action: "items"}, "Items Title", "?" + "items");
pushState
メソッドは3つの引数を持つ。一つ目がその状態でのデータ(JSON)、二つ目がタイトル、三つ目が遷移後のURLだ。 データの中にAjaxで置き換えるべきurl情報を格納しておけば、戻る時にそのデータを読み込んでAjaxを送ってHTMLを書き換えることで、まるでページが戻った可能ような挙動を実現できる。遷移のたびにURLを変えたい場合は第三引数を正しくしていしてあげることで、たとえばそのリンクをブックマークしてまた来た時に、その状態をAjaxで予め表示しておくことができるようになる。
終わりに
今回はRails x jQuery で特に重要なAjax処理について詳しく説明した。AngularJSを使えば確かにこれらリッチなアプリを作るには簡単に実現できることはわかるが、自分で一から作る分にはjQueryで作った方がハマる確率は圧倒的に減る。というかDOM操作とAjaxだけなのでハマる要素がない。Railsで搭載されているTurbolinksに関してもそうだ。あれも仕組みをしっかり理解していないとハマりやすいが、jQueryで構築すればその心配もないというわけだ。
Rails は標準で jQuery を搭載してくれているが、Railsはこのような開発のためにコントローラごとにjsやcssを分けてくれているわけだ。その設計指針通りに開発すれば、依然としてRailsによる圧倒的開発効率をリッチWebアプリでも実現できる、と私は考えている。
長文に付き合っていただき、ありがとうございました。