順番に読み進めながら学べます

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
応答が届く順番と 2 回の await
fetch(url) でサーバーに要求を送るPromise はpending のままstatus 200 とヘッダーが届くawait fetch(…) が終わりResponse が入る本文が最後まで届きJSON から変換するawait json() が終わり配列 3 件が入る
fetch の await は、ヘッダーが届いた時点で終わります。配列が入るのは、本文を読み終える json() の await の後です

json() は本文を最後まで読み取り、JSON.parse と同じ変換をしてから値を返すので、結果を await で待ちます。変換した後の orders は、filtermap がそのまま使えるふつうの配列です。

Response を出力すると {} になる

練習問題のコンソールでは、要素と同じく console.log(response){} になります。await の無い response.json() も Promise で {} です。Response は status を、本文は json()await した値を出力して確かめます。

会員管理の画面で、サーバーから会員の一覧を受け取り、名前を並べます。会員の一覧は fixtures/users.json から取得できます。

① 会員の一覧を取得し、応答の status を表示してください。

② 本文を配列にして、会員の人数を表示してください。

③ 名前だけを取り出して「、」でつなぎ、表示してください。

(正しく実行できれば解説が表示されます)

JavaScript / TypeScript エディタ

コードを実行してください

ページの場所から 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
runner.html と fixtures の位置
runner.desktechlearn.com — ドメインの直下(/)
  • 先頭に / を付けた URL は、ここからたどる
/tools/js-editor/ フォルダ
  • runner.html — 練習問題のコードが動くページ
  • 先頭に何も付けない URL は、ここからたどる
fixtures フォルダ
  • orders.jsonusers.jsonproducts.json がある
fixtures は、runner.html と同じ js-editor フォルダの中です。先頭の / の有無で、たどり始める場所が変わります

先頭に / を付けた URL はページがどのフォルダにあっても同じファイルを指し、付けない URL はページと fixtures を一緒に別のフォルダへ移しても書き換えずに済みます。下の表は、書き方ごとに、たどり始める場所と取りに行く URL のパスをまとめたものです。

書き方たどり始める場所取りに行く URL のパス
fixtures/orders.jsonrunner.html と同じ js-editor フォルダ/tools/js-editor/fixtures/orders.json
/tools/js-editor/fixtures/orders.jsonドメインの直下/tools/js-editor/fixtures/orders.json
orders.jsonjs-editor フォルダ/tools/js-editor/orders.json(ファイルは無い)

DevTools で試すと別の場所を探す

記事のページで DevTools の Console を開き、fetch("fixtures/orders.json") を実行すると、起点は記事のページの URL になり、fixtures の JSON には届きません。記事のページのサーバーは無いパスにもページの HTML を status 200 で返すので、取得できたように見えても中身は JSON ではありません。

雑貨店の管理画面で、商品の価格を合計します。brokenPath には、fixtures フォルダを書き忘れた商品一覧の URL が入っています。

brokenPath で取得し、status が 200 か(true / false)を表示してください。

② ページと同じフォルダからたどる URL に直して取得し、商品の件数を表示してください。

③ ドメインの直下からたどる URL で取得し、価格の合計を表示してください。

JavaScript / TypeScript エディタ

コードを実行してください

取得の失敗を見分ける — response.ok と status

URL のつづりを間違えたり、ファイルが削除されていたりすると、サーバーは「見つからない」という応答を返します。応答自体は届くので fetch は Response で fulfilled になり、try / catch で囲んでも catch には進まず、本文を読む次の行へ進みます。

status が 200〜299 のとき、response.oktrue です。成功の番号には 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("注文を読み込めませんでした");   // 注文を読み込めませんでした
}
ok の判定で分かれる 3 つの結末
await fetch(url)はどれも fulfilledorders.jsonstatus 200order.jsonstatus 403 などorder.jsonstatus 403 などok は truejson() を呼ぶok は falsejson() を呼ばないok を読まずにjson() を呼ぶ配列 3 件を受け取る読み込めない案内を表示するSyntaxError本文が JSON でない
ファイルが無くても fetch は fulfilled になります。JSON でない本文に json() を呼ぶと SyntaxError です

右の列では、本文が JSON ではなくエラーを説明する文章(練習問題のコンソールでは XML という形式)なので、json()await した行で止まります。中央の列のように、okfalse なら本文を読まずに案内を出すと、取得の失敗を画面の表示に変えられます。

商品ページに出す、商品とレビューの一覧を読み込みます。レビューはまだ公開されていません。

fixtures/products.json を取得し、成功したか(true / false)を表示してください。

fixtures/reviews.json を取得し、成功したかを表示してください。

③ ② が失敗なら「レビューはまだありません」、成功なら「レビュー 〇 件」の形で表示してください。

④ ① が成功なら本文を配列にして、在庫が 0 の商品名を表示してください。

JavaScript / TypeScript エディタ

コードを実行してください

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 件
fetch の結果では本文がそろわない
fetch() を2 回呼んで渡す2 本の Response が届いたら終わるResponse が2 つ並ぶresponses[0].lengthは undefinedloadJson() を2 回呼んで渡すそれぞれの中でjson() まで待つ配列が2 つ並ぶproducts.lengthは 4
上の段は、Response がそろった時点で終わります。本文を並べるには、json() まで待つ関数を呼んだ結果を渡します

Promise.all は、渡した Promise が 1 つでも rejected になると、ほかの結果も渡さずに失敗します。loadJsonokfalse でも null を返して fulfilled になるので、片方が見つからなくても、もう片方の配列は受け取れます。

売上の集計画面で、会員・注文・レビューを同時に読みます。パスの配列 paths は宣言済みで、レビューは未公開です。

① 成功すれば本文の値を、失敗すれば空の配列を返す async 関数 loadList を作ってください。

pathsloadList に通してまとめて待ち、「会員 4 人 / 注文 3 件 / レビュー 0 件」の形で表示してください。

③ 1 件目の注文の会員を userId で探し、名前を表示してください。

JavaScript / TypeScript エディタ

コードを実行してください
QUIZ

理解度チェック

まずは1問ずつ答えてみましょう。

Q1const response = await fetch(url); を実行した後、response に入るのは?

Q2存在しないパスで const res = await fetch(url); を実行した直後の状態は?

Q3/tools/js-editor/runner.html のコードで、fetch("users.json") が探す場所は?