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 での非同期処理は、複数のリクエストが同時に処理されることがあるため、このままではうまくいきません。下記の処理が必要です。
- リクエストごとにタイマーを管理する
- すべてのリクエストが完了したときにインジケータを非表示にする
リクエストごとにタイマーを管理する
通信を開始する時刻、通信が完了するまでの時間はリクエストごとに異なるので、インジケータを表示するタイマーはリクエストごとに管理する必要があります。
ajaxSend
と ajaxComplete
には、リクエストを一意に特定するような機能はありませんが、ハンドラの第 2、3 引数 (上例の jqxhr
と options
) は、リクエストごとに同じオブジェクトが渡されるので、これを利用しましょう。
ここでは、第 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
に指定する時間を調整して、極力表示されないようにしましょう。