Ruby
Rails

Turbolinksさんと上手く付き合う10の方法

More than 3 years have passed since last update.

Ruby on Rails Advent Calendar 2014の6日目です。

概要

Turbolinks ONのRails案件を2つこなして得たTurbolinksを使うときの知見を共有します。

おまだれ

  • ハートレイルズという会社で働く受託開発Railsエンジニアです。
  • Rails歴1年ぐらい。お仕事で半年ぐらい。
  • その前は選ばれた人だけの転職サイトをjavaで作ってました。

Turbolinksさんと上手く付き合う10の方法

1. 学ぶ。

Turbolinksについて

  • rails newしたら真っ先にGemfileから外すやつでしょ?某podcastの最近の回でゲストの人が言ってたよね」
  • $(document).ready」が呼ばれないアレでしょ?」
  • $(document).on 'ready page:load'使えばとりあえず問題なくなるアレでしょ?」

という認識で気軽に使い始めると泣きを見ます。
まずTurbolinksが実際に何をやっているのかをjsレベルでざっくりと知っておきましょう。

すでに詳細な日本語の解説エントリーがあるのでこれらを読みます。

僭越ながら私も自分のブログに簡単にまとめたエントリーを書いています。
* 今更ながらTurbolinksを初めて仕事で使ってみたので色々調べてみた

2. チームのメンバーにも基本的な知識をつけてもらう

一緒に開発するチームのメンバーにもある程度知識を持ってもらいます。何も知らないメンバーがRails4以外の言語/FWのノリでjsを書いた結果、他のページから遷移してきた時に動かないイベントが量産されてあなたが泣きながら修正して回る羽目になります。

最低限以下のことは抑えておいてもらいます。

  • 同一ドメイン内のリンククリックでの移動は全てAjaxでHTMLをロードして差し替える処理にturbolinksのjsが差し替えている
  • リンククリックでページ遷移してURLが変わってもwindowオブジェクトやdocumentオブジェクトの状態はそのままになる
  • 一旦ランディングしたらPOST/PATCH/DELETEなどで遷移しない限りjsの再ロードは実行されない
  • js使いまくってて超複雑なSPAっぽいページがあって、そのページに来たらオフにしたいような場合は対応出来るので分かる人に相談しよう
  • 自分でjsを実装したら他のページからリンククリックで遷移してきてもちゃんと動作するか?また他のページに行って往復してきても動作するか必ず確認する

3. setIntervalは慎重に使う

とあるページでsetIntervalするとどのページに行っても止まらない、POSTしたりして止まっても今度は再開すべきページで再開しない、という事故が簡単に起きます。これはPOSTやPUTをしてjsの再読み込みが行われるまでページ遷移してもjsのプロセスがずっと生き続ける為です。

もし、特定の条件を満たすページではsetIntarvalで繰り返し処理をしたいが、それ以外のページに行ったら動いてほしくない、というような場合は、setIntarvalの返り値のオブジェクトをグローバル変数に持たせて(或いは$適当なクラスのインスタンスのプロパティとして持たせて)管理する必要が出てきます。

$(document).on 'page:change' ->
  if repeatable_condition # setIntarvalをセットする条件。特定のDOMがあるとか
    if window.set_timer_on == null 
      window.timer = setInterval('something_method()',3000)
  else if window.set_timer_on? # setIntarvalを動かしたくないページに来てtimerが動いる場合    
    clearInterval(window.timer)
    window.timer = null

setIntarvalを多用しないと作れないRailsアプリの場合、Turbolinksさんに別れを告げた方が懸命です。

4. $(document)へのevent bindは何も考えずに$(document).readyでやる。

ajaxで後から生成されるDOMに対するイベントのbindではよく

$(document).on 'click', '#target', (event)->
  # 諸々の処理

こんなのを書きますが、page:changeは毎ページ遷移、page:loadもlanding以外では毎ページ遷移で実行されるため、割と簡単に多重bind事故が起きます。

逆に多重bindされても問題ないようにコールバック内で実行される処理を実装する手もありますが、解決策としては本質的ではありません。

色々試行錯誤した結果、以下のように$(document).readyでまとめてbindしてしまうのが一番確実ではないかと考えています。

$(document).ready ->
  $(document).on 'click', '#target1', (event)->
    # ...
  $(document).on 'click', '#target2', (event)->
    # ...

最終的にはapplication.js1ファイルにまとめられて一回のjs読み込みで諸々のイベントがまとめて実行される訳なので、だったら最初から$(document).ready一つにまとめておいた方が良いですし、余計なことを考えなくて済むので事故を起こしにくいです。今のところこれで問題は起きていません。

余談ですが、イベントのbindは$(document).onでbindするのがかなり高速であるというベンチマーク結果もあったりするので、この結果が正しいのであれば$(document)に大量にbindしても大きな負荷はかからず、js読み込み時のパフォーマンスへの影響はそれほど無いものであると考えられます。
参考: 高速で安全なjQueryを書くために今できること

5. Turbolinks独自のイベントが発火する順番を知っておく

また、ランディング時とリンククリック時で発火するイベントの種類が異なるので注意が必要です。
以下、簡単なサンプルを作って実験した時にまとめたものです。

ランディング時

  1. テンプレートにベタ書きしたjsの即時関数
  2. assets配下のCoffeeScriptに書いた$(document).ready
  3. テンプレートにベタ書きした$(document).ready
  4. page:change
  5. page:update
  6. assets配下のCofeeScriptに書いた$(window).load
  7. テンプレートにベタ書きした$(window).load

リンククリック時

  1. page:before-change
  2. page:fetch
  3. page:receive
  4. テンプレートにベタ書きしたjsの即時関数
  5. テンプレートにベタ書きした$(document).ready
  6. page:change
  7. page:update
  8. page:load

6. $(document).on 'page:change'は慎重に使う。

IE6,7などの古いブラウザだと発火しないらしいです。詳しくは Turbolinksをオフしないためにやった事を参照。

7. テンプレートファイルにjsをベタ書きしない。

テンプレートファイルが描画される度に実行されるので$(document)へのイベント多重bind事故などの思わぬトラブルのトリガーになります。

8. location.href=URLよりもTurbolinks.visit(URL)を選ぶ

href.locationに遷移先のURLを突っ込んでしまうとTurbolinksのページ遷移ではなく、通常のページ遷移が実行され、jsが再ロードされてしまいTurbolinksの'jsを再ロードしないことで高速化する'という恩恵が受けられなくなってしまいます。

Turbolinks.visitを使うとリンククリックで遷移するのと同等の処理が行われるので、特に理由がなければ、jsでのGETでのページ遷移ではTurbolinks.visitを使うようにします。

9. 使わない

既存のjsがごっそりあるようなRails3アプリをRails4にマイグレーションするようなケースであれば頑張って使わない方が懸命です。

10. jsはなるべく使わないでサーバ側(controller + view)で解決できることはサーバ側になるべく寄せる

Turbolinksを使うときは上記の様に神経を使うので、なるべくjsはシンプルになっていた方が精神衛生上良いです。

たまにjsのメソッド内にHTML文字列をずらずら持たせてイベントのコールバックでコンテンツを書き換えたりとか、ページ遷移毎にcssのclassを動的に変えるとかの処理をjsでゴリゴリ書いてしまう人がいますが、それって本来サーバサイドでやれるよね!?っていうものはサーバサイドに寄せていった方がいいです。

これはTurbolinks使ってても使ってなくても通用する話ですが、特にデザイン周りはcssのclassをテンプレート上で出し分けて、表示はcssで制御するという方に寄せていかないと、結果的にjsの責務が大きくなりすぎて後々のメンテがきつくなります。それに、開発者側がコントロールできないクライアントのブラウザに処理をどんどこ丸投げするのはパフォーマンスを測定して監視できない(GA使えば何となくできるけど)リスクが高くなります。

私がサーバサイド寄りのエンジニアだからこう思うのかも知れませんが、複雑なことはなるべくサーバサイドで処理してView側はできるだけシンプルにしておいた方が経験上幸せになれた気がします。