turbolinksのハマリどころのメモ（作成中）

※この投稿はハマりどころが増えたら順次更新していきますので未完成です。

turbolinksのバージョンが5になったので、今まで使ってなかったけれど重い腰を上げてやってみることに。
turbolinks5初心者の自分がハマったことなどをここに追記していこうと思います。

## windowに設定されたイベントがページ遷移する度に増えていく

turbolinksは`window`,`document`はそのままの状態を保ちます。
そのため、これらのオブジェクトにイベントを設定する処理が何度も呼ばれると、
ページ遷移する回数だけイベントが設定されてしまうことに…。

### 悪い例

```coffeescript
document.addEventListener 'turbolinks:load', ->
  # ページ遷移する回数だけイベントが設定されてしまう…
  $(window).scroll ->
    console.log("foo")
```

### 改善する

`turbolinks.visit`のタイミングで、`window`に設定されているイベントを解除します。
`$(window).scroll`という記述は、`$(window).on 'scroll'`と同義なので、`off`を使って解除することができます。

```coffeescript
document.addEventListener 'turbolinks:visit', ->
  $(window).off 'scroll'

document.addEventListener 'turbolinks:load', ->
  $(window).scroll ->
    console.log("foo")
```

## ブラウザの戻るボタン対策

### キャッシュしなければいい

基本的に大丈夫かなと思えたのですが、戻るボタンを押した際に、要素の座標を計算するような処理を行っていた箇所で、不具合が出ました。

まだいい解決策が見つかってないので暫定措置として、キャッシュしない！というふうにしました。

```slim:views/layouts/application.html.slim
meta name="turbolinks-cache-control" content="no-cache"
```

### キャッシュしない＝遅い

かといって、このままだと全部のページで戻るボタンを押したときに遅くなるので、ヘルパーメソッドで管理するようにしました。
`no-cache`で、キャッシュを使わないように。`no-preview`でプレビューしないようにしています。
プレビューを使うと、扱うコンテンツによってはチラつきが起きたりするので、私はデフォルトを`no-preview`にしました。

```rb:application_helper.rb
def use_turbolinks_cache?
  if @use_turbolinks_cache === false
    "no-cache"
  else
    "no-preview"
  end
end
```

View側は、これを使うだけ。

```slim:views/layouts/application.html.slim
meta name="turbolinks-cache-control" content="#{use_turbolinks_cache?}"
```

application_helper.rbで使ってる`@use_turbolinks_cache`は、ApplicationControllerで値を設定するメソッドを定義します。

```rb:controllers/application_controller.rb
class ApplicationController < ActionController::Base
  # 略
  private
  def no_use_turbolinks_cache
    @use_turbolinks_cache = false
  end
end
```

あとは、キャッシュしたくないアクションのときのみ、`no_use_turbolinks_cache`を実行します。

```rb:controllers/foos_controller.rb
class FoosController < ApplicationController
  before_action :no_use_turbolinks_cache, only: [:show]
end
```

これで、`/foos/1`などの結果はキャッシュされなくなったので、戻るボタンで`/foos/1`に戻っても問題なくなりました。

## knockout.jsと戻るボタンの相性が悪い

turbolinksのキャッシュを有効な状態で(no-preview含む)、knockout.jsを使っているページに対して戻るボタンで戻ると不具合が出ます。

そもそも、`ko.applyBindings`を実行してすでにバインド済みだと、再度`ko.applyBindings`を行うとエラーになります。
なので、一旦バインドを解除してから再バインドをするようにしていました。

これで一見、問題なさそうだったのですが、ObservableArrayを使っているところで問題が出ました。

もともとのViewが以下のようなもので、groupsはnameプロパティを持つとします。

```html:knockout.jsのView
<ul data-bind="foreach: groups">
  <li data-bind="text: name"></li>
</ul>
```

groupsの中身は`[{name: 'foo'}, {name: 'bar'}]`というObservableArray
これをbindすると、以下のようになりますね。

```html:bind後のコード
<ul data-bind="foreach: groups">
  <li data-bind="text: name">foo</li>
  <li data-bind="text: name">bar</li>
</ul>
```

これを`ko.cleanNode(document.body)`でバインドを解除してから、次のページに移動し、戻るボタンで戻ってくると、前のページがキャッシュされているから、テンプレートの内容がbind後のコードのままなのです。

```html:キャッシュされているView
<ul data-bind="foreach: groups">
  <li data-bind="text: name">foo</li>
  <li data-bind="text: name">bar</li>
</ul>
```

これに対して`ko.applyBindings`が実行されてしまい、結果は以下のように…。

```html
<ul data-bind="foreach: groups">
  <li data-bind="text: name">foo</li>
  <li data-bind="text: name">foo</li>
  <li data-bind="text: name">bar</li>
  <li data-bind="text: name">bar</li>
</ul>
```

今のところ、解決策は完全にキャッシュしないこと以外思いつきません…。