« Rails3 to Rails4 変更点まとめ:3 / 4 »

カテゴリー:
Rails
タグ:
 Rails pjax ajax turbolinks

このエントリーをはてなブックマークに追加
更新日時:
2015年01月07日(水)
作成日時:
2013年03月05日(火)

前の記事 / 次の記事

Rails4ではデフォルトでturbolinksが導入されるようなので、
どんなものかいじってみたら、なかなかエキサイティングだったけど、
他のJavaScriptと共存させようとすると結構デリケートだったので、それをまとめたメモ。

※ v2.1.0未満のturbolinksでfirefox26以降で正常に動作しなくなる不具合あり。
※ v2.2.1未満のturbolinksでfirefox27以降で正常に動作しなくなる不具合あり。

bundle update turbolinks

目次

  1. この記事で言及する各ライブラリの場所
  2. turbolinksとは何なのか?
  3. 何が問題になるのか?
  4. window.onload()が発火しない問題の解決
  5. scriptタグのsrcがキックされない問題の解決
  6. セキュリティ上の注意
  7. リロード問題
  8. noscriptタグ消滅問題
  9. IE11での背景画像反映されない問題
  10. ページ位置の座標が記憶される問題

1.この記事で言及する各ライブラリの場所

2.turbolinksとは何なのか?

HTML5のpushStateとajaxを組み合わせてページを高速でレンダリングするための仕組み。
turbolinksを導入するとリンクをクリックした時にGETで取得する全てのページがajaxで処理されるようになる。
(POST、PUT、DELETEの場合はturbolinksを導入していない場合と同じように処理される。)

具体的にはページ遷移が発生する時に(これはaタグがクリックされたタイミング)、
htmlのtitleタグの中身とbodyタグの中身をajaxで入れ替え、
ブラウザの現在のURLを遷移先のURLに、戻り先のURLを遷移前のURLに更新する。

これによってページ遷移の際にheadタグの中に書かれたJavaScriptとStyleSheetの
パースが行われなくなるので、特にJavaScriptを多用しているサイトではかなりの
高速化になるんじゃないかと。

ただこれは、JavaScriptとStyleSheetがアセットコンパイルされて
どのページでも常に同じファイルを読み込むようになっているという前提なので、
ページごとに異なるJavaScriptやStyleSheetを利用するようになっている場合は、
そもそもturbolinksの導入には向いていない。

ajax単体で同様のことを実現しようとした場合、
ajaxではURLは変えられないので、同じURLが複数のコンテンツを指し示したり、
複数のURLが同一のコンテンツを指し示すっていうダサい感じになるが、

turbolinksを使うと、HTML5のpushStateを使ってブラウザの履歴を更新出来るので、
ajaxでページを更新したタイミングでURL(ブラウザの履歴)も更新されて、イケてる感じになる。

また、JavaScriptやHTML5が使えない場合は従来通りのページ遷移を行う。

pjaxのようなもの。
pjaxについては、Rails上でのpjaxについて調べたのでメモ を参照。

turbolinksを導入した時にどのくらい速くなるのかは、
このサイトもturbolinksでページ遷移をしているので、
このサイト内で何度かページを遷移させてみて下さい。

3.何が問題になるのか?

今のところturbolinksを導入しても、
そのままの状態だとほとんどの場合不都合が多いと思う。

問題になったのはいずれも他のJavaScriptとの共存に関する問題で次の通り。

  • window.onload が発火しない
  • scriptタグのsrcがキックされない問題

これで何が問題になるのかというと、

window.onload()が読み込まれない
ajaxでページ遷移を行った場合、 window.onload が発火しないのはもちろん、
jQueryを使っている場合、$(document).ready() に定義した項目も発火しない。
scriptタグのsrcがキックされない問題
ツイートボタンやいいね!ボタンなど外部のサイトから提供されたコードを
scriptタグで貼り付けたりしている場合、それが動かない。

この記事はあくまでturbolinksを謳歌するためのまとめなので、
それでも頑張ってturbolinksを使っていく方向で話を進めるが、
もしどうしてもバグっちゃう場合はそのページだけturbolinksを適用しないことも出来る。

方法は アンカータグのdata属性 アンカータグを内包するコンテナのdata属性として
data-no-turbolink を指定する。

-# haml
%div{ data: {"no-turbolink" => true} }
  =link_to "バグちゃうページへ移動します", bagutteru_path

4.window.onload が発火しない問題の解決

jQueryの $(document).ready() を使っている場合は
turbolinksのページに解決策が示されていて、別途jquery-turbolinksを導入すれば解決する。

これを導入するとどうなるのかというと、turbolinksでページ遷移した時も$(document).ready() が発火する。
具体的には、turbolinksの "page:load" イベントが発生した時に $(document)ready() が発火するようになる。
("page:load"以外のイベントが発生した時に発火するように設定することも可能。)

なので、$(document).ready() 内に $(document).on("page:load", function(){ alert("fire"); })
などと書くとページを移動する度に $(document).ready() が入れ子状に増えていくので、
$(document).ready() 内で "page:load" をトリガーとする処理を書いてはいけない。
っていうか最初分かってなくて嵌った。

window.onload を直接使っている場合については謎、というかjQueryで満足したので調べてない。

5.scriptタグのsrcがキックされない問題

まず初めに、キックされないのはsrcに書かれたスクリプトで、
HTMLに直書きされたスクリプトは実行される。

次のコードは 実行されない

<script src="http://o.inchiki.jp/fire.js"></script>

次のコードは 実行される

<script>
  alert("fire");
</script>

解決方は大きく分けて次の二つのいずれかになると思う。

  • スクリプトの提供元から解決法が 示されている場合
  • スクリプトの提供元から解決方が 示されていない場合
スクリプトの提供元から解決法が示されている場合
仕様に従ってスクリプトを追加すれば動くようになるはず。
具体的な方法は ajaxで持って来たソーシャルボタンをボタンを更新する を参照
スクリプトの提供元から解決方が示されていない場合
iframe内に無理矢理押し込めることで実行することが出来る。
例えば対象のスクリプトタグだけをレンダリングするページを作成し、
ajaxで取得するページのiframeとしてそのページを呼び出すようにすると、
そのiframe内にあるスクリプトタグのsrcはキックされる。
参考ページ:ニコニコ動画外部プレイヤーを後から追加で読み込む方法

どうしてもどうにもならないもう無理な場合は仕方ないのでそのページだけturbolinksの対象外とする。

-# haml
%div{ data: {"no-turbolink" => true} }
  =link_to "例えば外部スクリプトの呼び出しがあるページ", tekitou_path

6. セキュリティ上の注意

参考ページ

うひょー!turbolinks何も考えなくても勝手にサクサク-!!

と手放しで喜んでいられるのかというとどうやらそうでもなかったようで、
いくつかのセキュリティの注意事項がある様子。

XHR Level2の仕様上リダイレクトかどうかを判別出来ないところに問題があるようで、

自サイト内にオープンリダイレクタがある場合
そのリダイレクタを経由した場合に それがリダイレクトであるのか否かが分からない
というかリダイレクトではないと判定されて リダイレクト先のページを
自サイト内のページとして読み込ませることが出来る
模様。

つまり

JumpsController
  def go
    redirect_to params[:to]
  end
end

みたいなのがあって、

http://my.fucking.site/jumps/go?to=http://destroy.your.computer/

みたいなアクセスが飛んで来ると、
"to=" に記述されたページを自サイト内のページとして読み込んでしまう。

これを防ぐためにはXHR2の仕様上リダイレクトかどうかの判定が出来ないので、
オープンリダイレクタを廃止するか、turbolinksを使うのをやめるかのどちらかしかない。
(全廃しなくても危険なページへ遷移する時だけ "data-no-turbolink" を指定すればいい。)

という脆弱性があるっぽい。検証はしてない。

7. リロード問題

自分で勝手にそう呼んでいるだけなのだけど、
リロードした時に変更前のページが復活する現象がある。

詳しく調べてはないから発生の条件や発生の原理は間違ってるかも知れない。
あとfirefoxでしか試してない。

以下、現象再現手順

まず、ページ内に自身のURIへのリンクがあるフォーム要素を持つページを作成する。

  1. フォームを適当に操作してフォームの入力値を変更する。
  2. 自身へのURIをクリックして自身から自身へ移動する。
  3. 1.で変更した状態はクリアされる。
  4. F5でリロードする(F5じゃなくてもいいけどスーパーリロードしない)。
  5. 1.で変更した状態が復活する。

推測

2.で自身から自身へ移動した時はTurbolinksが機能してAjaxで自身を書き換える。
4.でリロードした時はTurbolinksは介さずに2.で書き換えられる前のページを取得する。

対策

自身へ移動するリンクがクリックされた時にTurbolinksが発動しないようにする。

-# haml
%div{ data: {"no-turbolink" => true} }
  =link_to "move to self", self_path

あくまでTurbolinksを生かす場合は、
ページが読み込まれたタイミングでフォームごとにデフォルト値を設定するとか?
でもだるいよね。

8. noscriptタグ消滅問題

Turbolinks特有の問題なのか特定の使い方でAjaxするとなっちゃうのか検証してないけど、
Turbolinksで遷移した時に、遷移先のページにあるnoscriptタグが消滅する。

Turbolinksが使えてる時点でnoscriptタグに用はないはずなので、
基本的には問題にならないと思うんだけど、
JavaScriptで作成した要素をnoscriptタグの次に挿入するとかやってるとバグる。
バグった。ないから挿入されない。

9. IE11での背景画像反映されない問題

かなり限定的な状況でしか起きない気がするけど、
背景画像を指定するCSSがTurbolinksで差し替えられるコンテナの中に直書きされていて、
かつIE11(IE11未満は調べてないってだけでIE11未満でも起きるかも知れない)を使っていて、
かつTurbolinksで差し替えられるコンテナの中にbuttonタグが存在する場合、
背景が反映されない。

差し替えが上手くいかないとかじゃなくてHTMLは差し替わってるけどIE上に反映されない。
これはChromeやFirefoxでは起こらなくてIEだけで起こる。Safariは確かめてない。

解決法は、背景画像を直書きで指定している時にbuttonタグを使わないようにするか、
buttonタグの記述以降に後に背景画像を指定するようにする。

10. ページ位置の座標が記憶される問題

ずっと気付かなかったのだけど一部のブラウザで座標が記憶されてしまう現象が発生する。

自分が確認したのはAndroid版のFirefoxだけだけど、
マイナーなブラウザも含めたら他にもありそうな気がする。

ていうか端末再起動したら直ったからAndroid特有のなんか適当な感じかも知れない。
でも折角調べたのでメモ。

この現象が発生すると例えば、

ページの一番下までスクロールした状態で、
リンクをクリックして遷移先のページを呼び出すと、
遷移先のページの初期表示位置が、
遷移前のページを一番下までスクロールした場所の座標になる。

解決策、になるか分からないけどどうしても解決しない場合の応急処置。

参考:Railsでページ遷移するとスクロール位置がおかしい場合の対処法
一応検索した参考ページを載せておくが、自分はこの方法では解決しなかった。

どうしてこういう現象が発生するのが原因は不明というか
window.scrollTop() とか windows.pageyoffsett() とか座標を直接指定する系の
命令がうまく機能してないんだろうけど、この現象が発生している時、
これらの命令はturbolinksのトリガーでキックできない。

でも jQueryの $(element).animate { scrollTop: 0 } は使えたので頑張ってこれを使う。

agent = navigator.userAgent
if agent.search(/Android.*?Firefox//) != -1
  $(document).on "page:change", -> $("html, body").animate { scrollTop: 0 }, 1

とか。
ちなみに duration は1以上を指定しないとうまく動かなかった。
(通常時は0を指定しても問題ない。)

あと頑張って座標を移動させる方法をとった場合、
遷移前の座標が遷移後のページに存在しないと空白のページが表示されるという
致命的な状況になった。クリックしたり何か操作すると出てくるんだけど何もしないと
何も表示されない。

仕方ないので該当のブラウザではturbolinksを殺す。

agent = navigator.userAgent
if agent.search(/Android.*?Firefox/) != -1
  $("a").attr("data-no-turbolink", true)

turbolinksのトリガーイベントは
https://github.com/rails/turbolinks#events