つくる 読み込み中の表示を出す

このレッスンで分かること

  • 遅れて届くデータは、届く前と届いたあとで画面を描き分ける
  • 読み込み中の表示は、hidden を付け外しするだけで切り替えられる
  • 描き分けを 1 か所の関数に集めると、読み込み直しが 1 行で書ける

つくる 読み込み中の表示を出す とは

300 ミリ秒遅れて作品を返す Promise を自分で作り、待っているあいだはスピナーを、届いたあとはカードを出します。第9章で本物の通信につなぐ前の下ごしらえです。

ここまでで何ができるようになったか

第37回から第41回までで、非同期の道具がひととおりそろいました。振り返ると次のとおりです。

  • 第38回で new Promise を書き、resolve で中身を確定させました
  • 第39回で await を縦に並べ、前の結果を次に使いました
  • 第40回で Promise.all を使い、関係の無い処理を同時に待ちました
  • 第41回で rejecttrycatch で受け止めました

ここまではすべて関数の戻り値の話で、画面には何も出していません。今回はそれを画面につなぎます。入門の第8章で覚えた createElementclassList を、非同期と組み合わせるだけです。新しい DOM の書き方は出てきません。

遅れて届くと、画面に空白の時間ができる

これまでの作品一覧は、works 配列が最初からそこにあったので、読み込んだ瞬間にカードが並びました。ところが第9章で通信を使い始めると、返事が来るまでの何百ミリ秒かは配列が空です。

そのあいだ何も出さないと、利用者には「作品が 0 件のサイト」に見えます。壊れているのか、読み込み中なのか、区別が付きません。だから届く前と届いたあとで、画面を意識して描き分けます。

状態画面に出すものこの回での作り方
読み込み中回るスピナーと説明文#works-loading を出したままにする
届いたあと作品カードと件数#works-loadinghidden を付け、カードを描く

状態を表すのに、要素を消したり作り直したりする必要はありません。hidden というクラスを付け外しするだけで足ります。CSS 側にはすでに .hidden { display: none; } が書いてあります。要素そのものは残るので、読み込み直すときにまた出せます。

遅れて届く Promise を自分で作る

今回はまだ fetch を使いません。第38回と同じ形で、300 ミリ秒後に公開中の作品を返す関数を作ります。

function fetchWorks() { return new Promise(function (resolve) { setTimeout(function () { resolve(works.filter(function (work) { return work.published; })); }, 300); }); }

第9章になったら、この関数の中身だけを fetch に取り替えます。呼ぶ側は 1 文字も直さずに済みます。非同期の処理を関数の内側に閉じ込めておく利点がここに出ます。

描き分けを 1 か所に集める

画面を触る処理は、読み込み中に切り替える関数と、届いたあとに切り替える関数の 2 本に分けます。そのうえで、順番を決める async function を 1 本置きます。

function showLoading() { worksLoading.classList.remove("hidden"); workList.textContent = ""; worksStatus.textContent = ""; } function showLoaded(list) { worksLoading.classList.add("hidden"); renderWorks(list); worksStatus.textContent = loadCount + " 回目の読み込み / " + list.length + " 件"; } async function loadWorks() { loadCount = loadCount + 1; showLoading(); const list = await fetchWorks(); showLoaded(list); }

loadWorks を読むと、何が起きるかが 4 行で分かります。回数を数え、読み込み中にし、待ち、届いた形にする、です。第3章で書いた純粋関数の考え方と同じで、画面を触る仕事を関数の名前で言い表しておくと、順番を決める側が短くなります。

読み込み直すボタンは、この関数をもう一度呼ぶだけで済みます。

reloadButton.addEventListener("click", function () { loadWorks(); });

showLoading の中でカードを消しているので、押した瞬間にまたスピナーへ戻ります。状態の切り替えを 1 か所に集めておくと、こういう追加が 3 行で終わります。

何回目の読み込みかを数える

読み込み直すボタンが本当に効いているかは、カードを見ているだけでは分かりません。同じ 4 件が並び直すだけなので、画面はほとんど変わらないためです。そこで、何回目の読み込みかを数えて出しておきます。

let loadCount = 0;

let で持つのは、押すたびに数を入れ替えるためです。第7章で「中身を書き換えるだけなら const でよい」と書きましたが、それは配列やオブジェクトの話です。数値そのものを別の数値にするときは、入れ物ごと差し替えることになるので const は使えません。

数えるのは loadWorks の先頭です。await より前なので、読み込み中の表示にもすでに新しい回数が反映されます。

数を数える変数を関数の外に置くのは、第3章で扱ったクロージャの考え方の入口です。いまは素直に外側の変数として持ちますが、値をひとつの入れ物にまとめたくなったら、状態を閉じ込めた関数を作るやり方が使えます。

手を動かす順番

TODO のコメントを上から順に埋めていくと迷いにくいです。おおまかには次のとおりです。

  1. HTML に #works-reload#works-status#works-loading を足す
  2. JS で 3 つの要素を querySelector で取る
  3. fetchWorks を書き、setTimeout の中で resolve する
  4. showLoadingshowLoaded を書く
  5. loadWorksasync function として書き、最後に 1 回呼ぶ
  6. #works-reload のクリックで loadWorks をもう一度呼ぶ

3 まで書いた時点でスピナーが回りっぱなしになります。それが正しい途中経過です。4 と 5 を書くと、300 ミリ秒後にカードへ切り替わります。

うまく切り替わらないときは、次の順で見ていくと原因にたどり着きやすいです。まず、スピナーが一度も出ないなら showLoadingawait より後ろにあります。次に、スピナーが回ったまま止まらないなら resolve が呼ばれていないか、showLoaded を呼び忘れています。最後に、カードは出るのに件数が出ないなら #works-status の取得に失敗しています。querySelector の結果が null になっていないか確かめてください。

画面を触る処理を showLoadingshowLoaded の 2 本に分けておくと、この切り分けがそのまま関数の名前で言えます。どちらの関数まで動いたかを見れば、直す場所が 1 か所に決まります。関数に名前を付ける利点は、読みやすさだけでなく、壊れたときの探しやすさにも出ます。

よくある間違い

1 つめは、await の前に読み込み中の表示を出し忘れることです。

async function loadWorks() { const list = await fetchWorks(); showLoading(); // もう届いてから読み込み中にしている showLoaded(list); }

showLoading が動くのは待ち終わったあとなので、スピナーは一瞬も見えません。読み込み中にする処理は、必ず await より前の行に置いてください。

2 つめは、スピナーの要素ごと消してしまうことです。

worksLoading.remove(); // 二度と出せなくなる

remove すると要素が無くなるので、読み込み直したときにスピナーを出せません。classList.add("hidden") なら要素は残ったまま隠れるだけです。

課題

  1. 作品セクションに、読み込み中の表示 #works-loading (中に span.spinner) と、状態を出す #works-status と、読み込み直す #works-reload を足す
  2. fetchWorks() を作る。300 ミリ秒後に published が true の作品だけの配列で resolve する Promise を返す
  3. showLoading() で #works-loading から hidden を外し、カードと状態表示を空にする
  4. showLoaded(list) で #works-loading に hidden を付け、カードを描き、#works-status に「N 回目の読み込み / M 件」を出す
  5. loadWorks() を async function として作り、読み込んだ回数を数える。読み込んだ直後に 1 回呼び、#works-reload を押したらもう一度呼ぶ

ヒント

index.html
style.css
script.js
プレビュー

できているか

  • 読み込み中の表示 #works-loading がある
  • 遅れて届いた公開中の作品が 4 件並ぶ
  • 届いたあとは #works-loading に hidden が付いて隠れる
  • #works-status に 1 回目の読み込みと件数が出る
  • #works-reload を押すと 2 回目の読み込みが走って作品が並び直す