turbolinks と jQuery での非同期通信時、時間がかかる場合にインジケータを表示する

XMLHttpRequest による非同期通信は便利ですが、デフォルトではなんのフィードバックもないため、時間がかかるとユーザは不安になります。

ここでは、turbolinks および jQuery による非同期通信で、一定の時間がかかった場合に、通信中であることを示すインジケータを表示する方法を説明します。

前提

インジケータはすべてのページに loading-indicator という id の要素で用意されているものとします。この要素の表示・非表示を切り替えることで、通信中かどうかを示します。

コードは CoffeeScript で示します。

検証環境は下記の通りです。

  • turbolinks 2.2.0
  • jQuery 1.10.2
  • Chrome 33
  • OSX 10.9.2

turbolinks の場合

turbolinks は、リンクのクリックからページのロードまでに、いくつかのイベントを発火させます。

今回のケースでは、下記の 2 つのイベントを利用します。

  • page:fetch リクエストが送られる前に発火する
  • page:change レスポンスが返ってきて DOM 構築後に発火する
# インジケータを表示する処理
showIndicator = ->
  $("#loading-indicator").fadeIn()

timer = null

# リクエストが送られる前
document.addEventListener "page:fetch", (e)->
  timer = window.setTimeout(showIndicator, 300)

# レスポンスが返ってきたとき
document.addEventListener "page:change", (e)->
  if timer
    window.clearTimeout(timer)

これで、リクエストの 300ms 後にインジケータが表示されます。また、300ms 以内に通信が終わったときは、インジケータが表示されないようタイマーを止めています。

インジケータを非表示にする処理がありませんが、turbolinks は body 内の要素をすべて置き換えるので、自動的に表示されなくなります。

jQuery の場合

jQuery では、すべての非同期通信にフックを仕掛けることができます。

下記の 2 つが使えそうです。

  • ajaxSend リクエストが送られる前に呼ばれる
  • ajaxComplete レスポンスが返ってきたときに呼ばれる
# インジケータを表示する処理
showIndicator = ->
  $("#loading-indicator").fadeIn()

# インジケータを非表示にする処理
hideIndicator = ->
  $("#loading-indicator").hide()

timer = null

# リクエストが送られる前
$(document).ajaxSend (event, jqxhr, options)->
  timer = window.setTimeout(showIndicator, 300)

# レスポンスが返ってきたとき
$(document).ajaxComplete (event, jqxhr, options)->
  if timer
    window.clearTimeout(timer)
  hideIndicator()

インジケータを非表示にする処理を追加した以外は turbolinks とほとんど変わりませんね。

ただし、jQuery での非同期処理は、複数のリクエストが同時に処理されることがあるため、このままではうまくいきません。下記の処理が必要です。

  1. リクエストごとにタイマーを管理する
  2. すべてのリクエストが完了したときにインジケータを非表示にする

リクエストごとにタイマーを管理する

通信を開始する時刻、通信が完了するまでの時間はリクエストごとに異なるので、インジケータを表示するタイマーはリクエストごとに管理する必要があります。

ajaxSendajaxComplete には、リクエストを一意に特定するような機能はありませんが、ハンドラの第 2、3 引数 (上例の jqxhroptions) は、リクエストごとに同じオブジェクトが渡されるので、これを利用しましょう。

ここでは、第 3 引数 (options) にタイマーの ID を持たせることにします。

# インジケータを表示する処理
showIndicator = ->
  $("#loading-indicator").fadeIn()

# インジケータを非表示にする処理
hideIndicator = ->
  $("#loading-indicator").hide()

# リクエストが送られる前
$(document).ajaxSend (event, jqxhr, options)->
  # タイマーを options の適当なプロパティに持たせる
  options["loading-indicator:timer"] = window.setTimeout(showIndicator, 300)

# レスポンスが返ってきたとき
$(document).ajaxComplete (event, jqxhr, options)->
  if options["loading-indicator:timer"]
    window.clearTimeout(options["loading-indicator:timer"]) # タイマーを止める
  hideIndicator()

これでリクエストごとにタイマーを管理できるようになりました。

すべてのリクエストが完了したときにインジケータを非表示にする

ある通信が完了していても、ほかの通信が継続しているときには、インジケータは表示したままのほうがよいでしょう。

発行しているリクエストの数を数えて、0 になったらインジケータを非表示にします。ajaxSend で数を増やし、ajaxComplete で数を減らして、必要ならインジケータを非表示にします。

# インジケータを表示する処理
showIndicator = ->
  $("#loading-indicator").fadeIn()

# インジケータを非表示にする処理
hideIndicator = ->
  $("#loading-indicator").hide()

requestCount = 0 # 発行しているリクエストの数

# リクエストが送られる前
$(document).ajaxSend (event, jqxhr, options)->
  requestCount += 1 # 発行しているリクエストの数を増やす
  options["loading-indicator:timer"] = window.setTimeout(showIndicator, 300)

# レスポンスが返ってきたとき
$(document).ajaxComplete (event, jqxhr, options)->
  if options["loading-indicator:timer"]
    window.clearTimeout(options["loading-indicator:timer"])
  requestCount -= 1 # 発行しているリクエストの数を減らす
  if requestCount == 0 # すべてのリクエストが完了していたらインジケータを非表示に
    hideIndicator()

いい感じになりました。

注意事項

インジケータの表示の仕方にもよりますが、非同期通信時に毎回表示されるとうっとうしく感じます。setTimeout に指定する時間を調整して、極力表示されないようにしましょう。