Turbolinksをオフしないためにやった事

## この記事は Turbolinks v2.2.0 時点のものです
https://github.com/rails/turbolinks

## Turbolinks Classic is now deprecated

Rails4系で使われていたTurbolinksは[turbolinks-classic](https://github.com/turbolinks/turbolinks-classic)と命名が変更されました。
Rails5ではVersion 5 として [turbolinks](https://github.com/turbolinks/turbolinks) のversion ~> 5.0 が使われます。動作としては似てますが内部の実装が大きく変わっているので注意してください。


## Turbolinksとは
* TurbolinksはRails4.0からデフォルトで導入されたgem  
* railsアプリケーションを **簡単に** pjaxっぽくすることが出来る。
* ajaxとhistoryAPI(popState, pushState)を利用して画面遷移
* Turbolinksで遷移する場合は、titleとbodyとcsrfトークンを変更する
  * csrfトークンは変更された場合のみ
* js, cssの読み込みを初回時に行い次回以降の読み込み処理を省略することで高速化する。


## よく言われるTurbolinksの問題
* `$(document).ready()`が発火しない
* `$(window).load()`が発火しない
* metaタグが更新されない


## 主なTurbolinksの動作は2つ

#### fetchReplacement
* `<a>`タグのリンクをクリックした時の遷移をajax化して画面遷移なしにページ遷移する。
* clickイベントの挙動を変えている

#### fetchHistory
* 戻るボタンなどを押した時のイベントを書き換えている
* fetchReplacement時にpushStateでキャッシュしたページをpopStateで復元する。

## Turbolinks対応ブラウザ
公式では
> This includes Safari 6.0+ (but not Safari 5.1.x!), IE10, and latest Chromes and Firefoxes.

と紹介されている。

* **主な判定はhistoryAPI(`pushState`, `replaceState`)をブラウザがサポートしているかどうか**
* その他はソースを参照
* 後述するが、`page:change`の判定は別なのでTurbolinksは対応していないけど`page:change`が発火することがありえる。


## 実はTurbolinksのソースは

* js   約330行
* ruby 約80行

#### 1時間もあればだいたい仕様がつかめる。

## Turbolinksが提供しているtrigger 

* 'page:before-change'
* 'page:receive'
* 'page:change'
* 'page:update'
* 'page:load'
* 'page:restore'
* 'page:expire'


## fetchReplacement
### `<a>`タグのリンクをクリックの挙動をajax化する

1. `<a>`タグをクリック
2. `'page:before-change'`。`'page:before-change'`を設定しているeventがfalseを返す場合、通常の画面遷移を行う。
3. fetchReplacement がcallされる
4. pushStateで現在のページをpageCashにキャッシュ
5. `'page:fetch'`
6.  GETのajax request を送信 
7. responceが返ってくる
8. `'page:receive'`
9. responceをチェック。statusが400~599でない, content-typeが正しいか, ヘッダが変わってないか 
10. 9が正しくなければ `document.location.href = url` で遷移
11. body, title をごっそり変える
12. `'page:change'`
13. `'page:update'`
14. `'page:load'`

## fetchReplacement tips
* fetchReplacementが成功した場合、既に前のページから読み込まれているjs, cssを読み込む必要がないのでその分高速。
* ajaxで画面遷移が行われるので`$(document).ready()`や`$(window).load()`が発火しない。
* headが変化してるかのチェックは`data-turbolinks-track`が付与されたnodeが前のページと内容が同じかをチェックする
* レスポンスが正しくない場合は、サーバに2度リクエストする。redirectを挟む場合は4回リクエストが飛ぶ。
* サーバーがクロスドメインへのリダイレクトを返す場合、403を返す **※ここ重要**
* consoleで`Turbolinks.visit('/hoge')`と実行することでfetchReplacementを実行できる。
* リンクをクリックした時の視覚的レスポンスがないため、ユーザーには不親切。`page:fetch`, `page:recieve`間で視覚的にローディングしている事を伝えた方がユーザーフレンドリー。


## fetchReplacementが行われないパターン
* 初めてそのサイトに訪れた時
* ページをリロードした時
* ignoreClickの条件に一致する時
* submit時はそもそも`<a>`タグではないので動作しない
* その場合、従来の画面遷移が行われる。


## ignore Click
Turbolinksが動作しないリンク

* crossOriginLink
* anchoredLink
* nonHtmlLink
* noTurbolink
* targetLink
* nonStandardClick

## 主なignore Click
* crossOriginLink
  * location.protocolとlocation.hostが現在のページと異なるページへのリンク
  * サブドメインが異なる場合、fetchReplacementは動作しない

* nonHtmlLink
  * htmlで無いリンク
  * 画像などへのリンクの場合、fetchReplacementは動作しない

* noTurbolink
  * 唯一明示的に開発者が設定できる
  * リンクから親を逆上って`data-no-turbolink` 存在するリンクは、fetchReplacementは動作しない

* nonStandardClick
  * `shift`, `command`, `alt`, `control`キーなどが押されている場合は、動作しない
  * 別ウィンドを開く時など

## fetchHistory
### 戻るなどの遷移をpageCashからpopStateで復元する

1. pageCashに目的のページがある場合、popStateで復元する
2. 1で復元出来ない場合、`document.location.href = url`
3. `'page:change'`
4. `'page:update'`
5. `'page:restore'`

## fetchHistory tips
* ブラウザのキャッシュから復元するので高速
* ブラウザの更新がないため、readyなどが発火しない
* キャッシュに該当のページがない場合はブラウザによりサーバーにアクセスする。
* デフォルトでは10ページ分キャッシュする
* consoleで`Turbolinks.pageCached();`を実行することでキャッシュするページ数を取得出来る。

## 主なtriggerと`$(document).ready()`

#### ``$(document).ready()``
* jQueryが提供
* どんなブラウザでも発火
* DOMが構成された後に発火

#### `'page:load'`
* fetchReplacementが完了した時に発火

#### `'page:restore'`
* fetchHistoryが完了に発火

#### `'page:change'`
* どんな場合でもページを表示した後に発火
* `$(document).ready()` 相当
* `DOMContentLoaded` リスナーに追加されている
* 発火タイミングは `DOMContentLoaded` + `fetchReplacement` + `fetchHistory`
* **しかし、`DOMContentLoaded`はIE6,7など古いブラウザでは実装されていないので古いブラウザでも動作させたい処理を書くときは使用しない事。**


## triggerの発火タイミング

|             	| landing, reload | fetchReplacement 	| fetchHistory 	|
|--------------	|----------	  |------------------	|--------------	|
| ready        	| ○        	  | ×                	| ×            	|
| page:load    	| ×        	  | ○                	| ×            	|
| page:restore 	| ×        	  | ×                	| ○            	|
| page:change  	| ○               | ○                	| ○            	|  

* fetchReplacement失敗時は再度レンダリングされ, landingの状態になる。

## よく出てくるおまじない

#### `$(document).on 'ready page:load', ->`

これはjqueryが提供する`'ready'`とTurbolinksが提供する`'page:load'`の両方で定義すること。  

* 非Turbolinks遷移時は`'ready'`で発火。
* Turbolinks遷移時は`'page:load'`で発火の両方をカバーするという意味。
* Turbolinksをサポートしていないブラウザでも`'ready'`が発火するので安心。

#### jquery-turbolinks gem
* `'ready'`triggerに`page:load`を追加してくれる機能を提供。


## Trigger tips
* 通常、`page:change`でaddEventListenerしてはいけない。
  * fetchHistoryの際にもイベントが追加されるのでページを戻った時に多重にEventを追加してしまう事があるため。
  * 'ready page:load'を使用する。
* `page:change`はTurbolinksのサポート判定とは異なり、`document.addEventListener` と`document.createEvent`がブラウザでサポートされている事が判定基準。
  * なのでTurbolinksは動かないけど`page:change`は発火する事がありえる。
* `page:change`は動くブラウザとそうでないブラウザがあるので注意が必要。
  * 特にGoogleAnalyticsは`page:change`を使うと古いブラウザのデータが取れない。


## Turbolinksと仲良くするために注意すること

#### `<body>`タグにjsを書かない。  

* `<body>`タグ内にeventを設定する処理(clickなど)をjsで書くとTurbolinksで遷移する度に設定され、何度も呼ばれる。  
* どうしても書きたい時は`<script>` に `data-turbolinks-eval="false"`と書けば1度しか実行されない

#### js, css, `<meta>`タグなど`<head>`をページごとに更新する必要がある場合は`data-turbolinks-track`を記述する

* 読み込む必要があるjsやcssなどが読み込まれないことを防ぐため

#### Rails側でクロスドメインへリダイレクトするリンクには`data-no-turbolink`を指定する

* 無駄なリクエストを未然に防ぐ

#### できるだけapplication.js, application.cssにまとめる

* cssやjsが変わるとTurbolinksで遷移できず、再描画されるのでせっかくの旨味が減る。
* 無駄にリクエストが2度飛ぶのでTurbolinks使わない場合より遅くなることも。
* サブドメインが異なるとTurbolinksはそもそも動作しないため、サブドメインごとにファイルを分けるのはあり。


## Turbolinksに向いているサイト
* javascriptの記述量が少ない
* Railsが生成したビューを返す事がメインであるサイト
* 開発コスト上げてでもページレンダリングを高速化したい用途がある

## Turbolinksに向かないサイト
* すでにJavascriptを多く記述してる
* Javascriptのclientside frameworkをサイトで多く使用している(Backbone, Angular等)
* ページレンダリングを高速化する必要がない規模のサイト
* subdomainをサイトで多用している
* ページごとにcss, jsを分けて記述している

## Turbolinksの良い点・悪い点

### 良い点
* Turbolinksを使うとページのレンダリングが速くなる ([turbolinks_test](https://github.com/steveklabnik/turbolinks_test/tree/all_the_assets))
* 戻るボタンで画面遷移する場合、キャッシュから復元しサーバーにアクセスしないため超高速。
* うまく使いこなす事が出来ればリクエスト数を減らす事が出来る。
* Rails4.0がリリースされて約1年ちょっと。そろそろ情報を調べる事ができるようになってきた。
* ソースコード読むとそんなに難しくない

### 悪い点
* jsやブラウザの前提を覆す。
  * readyが呼ばれない
  * headerが変化しない等
* フロントエンドエンジニアとデザイナーに負担を書けてしまう場合が多い
* jsをTurbolinks依存したコードを書く必要がある
* Turboliksで遷移出来ない場合、不要なアクセスが増える
* 本来Turbolinksで遷移出来るはずのリンク(ignoreClickでない)がcss, jsが異なる理由で失敗する場合,2回リクエストが飛ぶので逆に遅くなる。

## まとめ
* Turbolinksでうまく遷移できるように構築しているとページのレンダリングが早くなる。
* 逆にTurbolinksでうまく遷移できるように構築していないとリクエストが多く発生し逆に遅くなる場合がある。
* 'page:change'は古いブラウザでは動かないので使用するのには注意する。
* jsを書く場合はTurbolinksの挙動を理解しておく必要がある。
* たぶんほとんどのwebアプリケーションでは使用する必要はないかもしれません。。

### 開発コストがあがり苦労をする面もありますが、適切に使うとそれなりにメリットがあるので、使用用途をしっかり考え運用することをおすすめします。


## 追記
[Turbolinksとプログレスバーをあわせると開発がかなり捗る nprogress-rails](http://qiita.com/saboyutaka/items/34d65201600ba00ea930)

[Turbolinks 3こそRailsの未来](http://qiita.com/saboyutaka/items/2cd40f7942f8463dcfde)

を書いたのでこちらもどうぞ。