Q1const response = await fetch(url); を実行した後、response に入るのは?
fetch — API からデータを取る
URL を渡してデータを取りに行く fetch と、本文を値に変える json()、取得の成否を表す response.ok、相対 URL の書き方、Promise.all で 2 つの JSON を同時に読む書き方を扱います。
これまでの演習では、データをコードに直接書いてきました。実際のサイトでは価格や在庫が変わるので、開くたびにサーバーの API(プログラムがデータを受け取るための URL)へ問い合わせます。
この記事では、URL からデータを取得する fetch と、成否を表す response.ok を扱います。
API の JSON を受け取る — fetch と json()
注文履歴のページで、サーバーに保存されている注文の一覧を表示したいとします。注文は JSON の文字列で届くので、届くまで待つ書き方と、文字列を配列に戻す手順が要ります。練習問題では、本サイトの fixtures フォルダに置いた JSON ファイルを、API の代わりに取得します。
fetch(URL のデータをサーバーに要求する関数)は、応答を表す Response で fulfilled になる Promise を返します。応答は status(成功なら 200 になる番号)とヘッダー(本文より先に届く、応答についての情報)が先に届き、本文は後から続くので、response.json() をもう一度 await して受け取ります。
// 本サイトに置いた注文の JSON を取得する(URL の書き方は次の章)
const response = await fetch("fixtures/orders.json");
console.log(response.status); // 200
// 本文を最後まで受け取り、JSON から配列に変換する
const orders = await response.json();
console.log(orders.length); // 3
console.log(orders[1].total); // 12600
// 受け取った後は、ふつうの配列として扱える
const preparing = orders.filter((order) => order.status === "準備中");
console.log(preparing.map((order) => order.id).join(", ")); // A-3002, A-3003
json() は本文を最後まで読み取り、JSON.parse と同じ変換をしてから値を返すので、結果を await で待ちます。変換した後の orders は、filter や map がそのまま使えるふつうの配列です。
Response を出力すると {} になる
練習問題のコンソールでは、要素と同じく console.log(response) は {} になります。await の無い response.json() も Promise で {} です。Response は status を、本文は json() を await した値を出力して確かめます。
ページの場所から URL を書く — 相対 URL
前の章では "fixtures/orders.json" とだけ書いて取得できました。https:// から書く URL は ドメイン(https:// の後に続く、runner.desktechlearn.com のようなサイトの場所を表す部分)まで含むので、サイトを置く場所が変わるたびに書き換えが要ります。
相対 URL(ページの URL を起点に、そこからの位置だけを書く URL)は、先頭に何も付けなければページと同じフォルダから、/ を付ければドメインの直下からたどります。練習問題のコードは、記事のページとは別の runner.desktechlearn.com にある、画面を持たない runner.html で動きます。
// このコードが動くページ(練習問題のコンソール)の URL
// https://runner.desktechlearn.com/tools/js-editor/runner.html
// 先頭に何も付けない: runner.html と同じ js-editor フォルダからたどる
const fromPage = await fetch("fixtures/orders.json");
console.log(fromPage.url); // url は実際に取りに行った URL
// https://runner.desktechlearn.com/tools/js-editor/fixtures/orders.json
// 先頭に / を付ける: ドメインの直下からたどる
const fromRoot = await fetch("/tools/js-editor/fixtures/orders.json");
console.log(fromRoot.url === fromPage.url); // true(同じファイルを指す)
// fixtures/ を書き忘れると、js-editor の直下を探す
const noFolder = await fetch("orders.json");
console.log(noFolder.url);
// https://runner.desktechlearn.com/tools/js-editor/orders.json
- 先頭に
/を付けた URL は、ここからたどる
runner.html— 練習問題のコードが動くページ- 先頭に何も付けない URL は、ここからたどる
orders.json・users.json・products.jsonがある
先頭に / を付けた URL はページがどのフォルダにあっても同じファイルを指し、付けない URL はページと fixtures を一緒に別のフォルダへ移しても書き換えずに済みます。下の表は、書き方ごとに、たどり始める場所と取りに行く URL のパスをまとめたものです。
| 書き方 | たどり始める場所 | 取りに行く URL のパス |
|---|---|---|
| fixtures/orders.json | runner.html と同じ js-editor フォルダ | /tools/js-editor/fixtures/orders.json |
| /tools/js-editor/fixtures/orders.json | ドメインの直下 | /tools/js-editor/fixtures/orders.json |
| orders.json | js-editor フォルダ | /tools/js-editor/orders.json(ファイルは無い) |
DevTools で試すと別の場所を探す
記事のページで DevTools の Console を開き、fetch("fixtures/orders.json") を実行すると、起点は記事のページの URL になり、fixtures の JSON には届きません。記事のページのサーバーは無いパスにもページの HTML を status 200 で返すので、取得できたように見えても中身は JSON ではありません。
取得の失敗を見分ける — response.ok と status
URL のつづりを間違えたり、ファイルが削除されていたりすると、サーバーは「見つからない」という応答を返します。応答自体は届くので fetch は Response で fulfilled になり、try / catch で囲んでも catch には進まず、本文を読む次の行へ進みます。
status が 200〜299 のとき、response.ok は true です。成功の番号には 200 のほか 201 なども含まれるので、成否は ok で判断します。無いファイルには 404(見つからない)を返すのが一般的ですが、練習問題のコンソールのサーバーは 403(アクセスを許可しない)を返します。
// orders を order と書き間違えた URL
const response = await fetch("fixtures/order.json");
// 見つからなくても fetch は fulfilled になり、Response が返る
console.log(response.ok); // false
console.log(response.status); // 404 や 403(サーバーで違う。練習問題のコンソールは 403)
// ok が true のときだけ、本文を配列に変換する
if (response.ok) {
const orders = await response.json();
console.log(`注文 ${orders.length} 件`);
} else {
console.log("注文を読み込めませんでした"); // 注文を読み込めませんでした
}
右の列では、本文が JSON ではなくエラーを説明する文章(練習問題のコンソールでは XML という形式)なので、json() を await した行で止まります。中央の列のように、ok が false なら本文を読まずに案内を出すと、取得の失敗を画面の表示に変えられます。
2 本を同時に取得する — async 関数と Promise.all
会員数と注文数を並べる管理画面では、互いの結果を使わない 2 つの JSON を、async / await の記事で扱った 並列(複数を同時に始めてから待つ進め方)で読み込みます。ただ、fetch を 2 回呼んで Promise.all に渡してもそろうのは、本文ではなく Response です。
fetch から json() までを 1 つの async 関数にまとめると、呼んで返る Promise は本文の値で fulfilled になります。この関数を 2 回呼んでから Promise.all で待つと、渡した順に本文の値が並んだ配列を受け取れます。
// fetch から json() までをまとめ、本文の値を返す(見つからなければ null)
async function loadJson(path) {
const response = await fetch(path);
if (!response.ok) {
return null;
}
return await response.json();
}
// fetch を 2 回呼んで渡すと、そろうのは Response
const responses = await Promise.all([
fetch("fixtures/products.json"),
fetch("fixtures/orders.json"),
]);
console.log(responses[0].length); // undefined(配列ではない)
// loadJson を 2 回呼ぶと 2 本が同時に始まり、Promise.all で本文の配列がそろう
const [products, orders] = await Promise.all([
loadJson("fixtures/products.json"),
loadJson("fixtures/orders.json"),
]);
console.log(`商品 ${products.length} 件 / 注文 ${orders.length} 件`); // 商品 4 件 / 注文 3 件
Promise.all は、渡した Promise が 1 つでも rejected になると、ほかの結果も渡さずに失敗します。loadJson は ok が false でも null を返して fulfilled になるので、片方が見つからなくても、もう片方の配列は受け取れます。
理解度チェック
まずは1問ずつ答えてみましょう。
Q2存在しないパスで const res = await fetch(url); を実行した直後の状態は?
Q3/tools/js-editor/runner.html のコードで、fetch("users.json") が探す場所は?