つくる 読み込み中の表示を出す
このレッスンで分かること
- 遅れて届くデータは、届く前と届いたあとで画面を描き分ける
- 読み込み中の表示は、
hiddenを付け外しするだけで切り替えられる- 描き分けを 1 か所の関数に集めると、読み込み直しが 1 行で書ける
つくる 読み込み中の表示を出す とは
300 ミリ秒遅れて作品を返す Promise を自分で作り、待っているあいだはスピナーを、届いたあとはカードを出します。第9章で本物の通信につなぐ前の下ごしらえです。
ここまでで何ができるようになったか
第37回から第41回までで、非同期の道具がひととおりそろいました。振り返ると次のとおりです。
- 第38回で
new Promiseを書き、resolveで中身を確定させました - 第39回で
awaitを縦に並べ、前の結果を次に使いました - 第40回で
Promise.allを使い、関係の無い処理を同時に待ちました - 第41回で
rejectをtryとcatchで受け止めました
ここまではすべて関数の戻り値の話で、画面には何も出していません。今回はそれを画面につなぎます。入門の第8章で覚えた createElement と classList を、非同期と組み合わせるだけです。新しい DOM の書き方は出てきません。
遅れて届くと、画面に空白の時間ができる
これまでの作品一覧は、works 配列が最初からそこにあったので、読み込んだ瞬間にカードが並びました。ところが第9章で通信を使い始めると、返事が来るまでの何百ミリ秒かは配列が空です。
そのあいだ何も出さないと、利用者には「作品が 0 件のサイト」に見えます。壊れているのか、読み込み中なのか、区別が付きません。だから届く前と届いたあとで、画面を意識して描き分けます。
| 状態 | 画面に出すもの | この回での作り方 |
|---|---|---|
| 読み込み中 | 回るスピナーと説明文 | #works-loading を出したままにする |
| 届いたあと | 作品カードと件数 | #works-loading に hidden を付け、カードを描く |
状態を表すのに、要素を消したり作り直したりする必要はありません。
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 のコメントを上から順に埋めていくと迷いにくいです。おおまかには次のとおりです。
- HTML に
#works-reloadと#works-statusと#works-loadingを足す - JS で 3 つの要素を
querySelectorで取る fetchWorksを書き、setTimeoutの中でresolveするshowLoadingとshowLoadedを書くloadWorksをasync functionとして書き、最後に 1 回呼ぶ#works-reloadのクリックでloadWorksをもう一度呼ぶ
3 まで書いた時点でスピナーが回りっぱなしになります。それが正しい途中経過です。4 と 5 を書くと、300 ミリ秒後にカードへ切り替わります。
うまく切り替わらないときは、次の順で見ていくと原因にたどり着きやすいです。まず、スピナーが一度も出ないなら showLoading が await より後ろにあります。次に、スピナーが回ったまま止まらないなら resolve が呼ばれていないか、showLoaded を呼び忘れています。最後に、カードは出るのに件数が出ないなら #works-status の取得に失敗しています。querySelector の結果が null になっていないか確かめてください。
画面を触る処理を
showLoadingとshowLoadedの 2 本に分けておくと、この切り分けがそのまま関数の名前で言えます。どちらの関数まで動いたかを見れば、直す場所が 1 か所に決まります。関数に名前を付ける利点は、読みやすさだけでなく、壊れたときの探しやすさにも出ます。
よくある間違い
1 つめは、await の前に読み込み中の表示を出し忘れることです。
async function loadWorks() {
const list = await fetchWorks();
showLoading(); // もう届いてから読み込み中にしている
showLoaded(list);
}showLoading が動くのは待ち終わったあとなので、スピナーは一瞬も見えません。読み込み中にする処理は、必ず await より前の行に置いてください。
2 つめは、スピナーの要素ごと消してしまうことです。
worksLoading.remove(); // 二度と出せなくなるremove すると要素が無くなるので、読み込み直したときにスピナーを出せません。classList.add("hidden") なら要素は残ったまま隠れるだけです。
課題
- 作品セクションに、読み込み中の表示 #works-loading (中に span.spinner) と、状態を出す #works-status と、読み込み直す #works-reload を足す
- fetchWorks() を作る。300 ミリ秒後に published が true の作品だけの配列で resolve する Promise を返す
- showLoading() で #works-loading から hidden を外し、カードと状態表示を空にする
- showLoaded(list) で #works-loading に hidden を付け、カードを描き、#works-status に「N 回目の読み込み / M 件」を出す
- loadWorks() を async function として作り、読み込んだ回数を数える。読み込んだ直後に 1 回呼び、#works-reload を押したらもう一度呼ぶ