Fetch API

この Codelab は、Google Developers トレーニング チームが開発した「プログレッシブ ウェブアプリの開発」トレーニング コースの一部です。Codelab を順番に進めると、このコースを最大限に活用できます。

コースの詳細については、プログレッシブ ウェブアプリの開発の概要をご覧ください。

はじめに

このラボでは、リソースを取得するためのシンプルなインターフェースである Fetch API の使用方法について説明します。これは XMLHttpRequest API の改良版です。

学習内容

  • Fetch API を使用してリソースをリクエストする方法
  • fetch を使用して GET、HEAD、POST リクエストを行う方法
  • カスタム ヘッダーの読み取りと設定方法
  • CORS の使用方法と制限事項

必要な予備知識

  • 基本的な JavaScript と HTML
  • ES2015 の Promise のコンセプトと基本的な構文を理解していること

必要なもの

  • ターミナル/シェルにアクセスできるパソコン
  • インターネット接続
  • Fetch に対応しているブラウザ
  • テキスト エディタ
  • Nodenpm

注: Fetch API は現在すべてのブラウザでサポートされているわけではありませんが、ポリフィルがあります。

GitHub から pwa-training-labs リポジトリをダウンロードするか、クローンを作成し、必要に応じて Node.js の LTS バージョンをインストールします。

パソコンのコマンドラインを開きます。fetch-api-lab/app/ ディレクトリに移動して、ローカル開発用サーバーを起動します。

cd fetch-api-lab/app
npm install
node server.js

サーバーは Ctrl-c でいつでも終了できます。

ブラウザを開き、localhost:8081/ に移動します。リクエストを行うためのボタンを含むページが表示されます(まだ機能しません)。

注: ラボに干渉しないように、Service Worker の登録を解除し、localhost の Service Worker キャッシュをすべてクリアします。Chrome DevTools でこれを行うには、[アプリケーション] タブの [ストレージを消去] セクションで [サイトデータを消去] をクリックします。

任意のテキスト エディタで fetch-api-lab/app/ フォルダを開きます。app/ フォルダは、ラボを構築する場所です。

このフォルダには次のものが含まれています。

  • echo-servers/ には、テストサーバーの実行に使用されるファイルが含まれています
  • examples/ には、フェッチのテストで使用するサンプル リソースが含まれています。
  • js/main.js はアプリのメイン JavaScript であり、すべてのコードを記述する場所です。
  • index.html は、サンプルサイト/アプリケーションのメインの HTML ページです。
  • package-lock.jsonpackage.json は、開発サーバーとエコーサーバーの依存関係の構成ファイルです。
  • server.js はノード開発サーバーです

Fetch API のインターフェースは比較的シンプルです。このセクションでは、fetch を使用して基本的な HTTP リクエストを記述する方法について説明します。

JSON ファイルを取得する

js/main.js では、アプリの [Fetch JSON] ボタンが fetchJSON 関数にアタッチされています。

examples/animals.json ファイルをリクエストしてレスポンスをログに記録するように fetchJSON 関数を更新します。

function fetchJSON() {
  fetch('examples/animals.json')
    .then(logResult)
    .catch(logError);
}

スクリプトを保存してページを更新します。[JSON を取得] をクリックします。コンソールにフェッチ レスポンスが記録されます。

説明

fetch メソッドは、取得するリソースのパス(この場合は examples/animals.json)をパラメータとして受け取ります。fetch は、Response オブジェクトに解決される Promise を返します。Promise が解決されると、レスポンスが logResult 関数に渡されます。Promise が拒否されると、catch が引き継ぎ、エラーが logError 関数に渡されます。

レスポンス オブジェクトは、リクエストに対するレスポンスを表します。これらには、レスポンス本文だけでなく、便利なプロパティとメソッドも含まれています。

無効なレスポンスをテストする

コンソールでログに記録されたレスポンスを確認します。statusurlok プロパティの値をメモします。

fetchJSONexamples/animals.json リソースを examples/non-existent.json に置き換えます。更新された fetchJSON 関数は次のようになります。

function fetchJSON() {
  fetch('examples/non-existent.json')
    .then(logResult)
    .catch(logError);
}

スクリプトを保存してページを更新します。[JSON を取得] をもう一度クリックして、存在しないリソースの取得を試します。

フェッチが正常に完了し、catch ブロックがトリガーされなかったことを確認します。新しいレスポンスの statusURLok プロパティを見つけます。

2 つのファイルの値は異なるはずです(理由を理解していますか?)。コンソール エラーが発生した場合は、値がエラーのコンテキストと一致していますか?

説明

エラー レスポンスで catch ブロックがアクティブにならなかったのはなぜですか?これは、fetch と Promise に関する重要な注意事項です。エラーレスポンス(404 など)も解決されます。フェッチ プロミスはリクエストを完了できなかった場合にのみ拒否されるため、レスポンスの有効性を常に確認する必要があります。次のセクションでは、レスポンスを検証します。

詳細情報

レスポンスの有効性を確認する

回答の有効性を確認するようにコードを更新する必要があります。

main.js で、回答を検証する関数を追加します。

function validateResponse(response) {
  if (!response.ok) {
    throw Error(response.statusText);
  }
  return response;
}

次に、fetchJSON を次のコードに置き換えます。

function fetchJSON() {
  fetch('examples/non-existent.json')
    .then(validateResponse)
    .then(logResult)
    .catch(logError);
}

スクリプトを保存してページを更新します。[JSON を取得] をクリックします。コンソールを確認します。これで、examples/non-existent.json のレスポンスによって catch ブロックがトリガーされるようになります。

fetchJSON 関数の examples/non-existent.json を元の examples/animals.json に置き換えます。更新された関数は次のようになります。

function fetchJSON() {
  fetch('examples/animals.json')
    .then(validateResponse)
    .then(logResult)
    .catch(logError);
}

スクリプトを保存してページを更新します。[JSON を取得] をクリックします。レスポンスが以前と同様に正常にロギングされていることを確認します。

説明

validateResponse チェックを追加したため、不正なレスポンス(404 など)はエラーをスローし、catch が引き継ぎます。これにより、失敗したレスポンスを処理し、予期しないレスポンスがフェッチ チェーンに伝播するのを防ぐことができます。

回答を読む

Fetch レスポンスは ReadableStreamsstreams 仕様)として表され、レスポンスの本文にアクセスするには読み取る必要があります。レスポンス オブジェクトには、これを行うためのメソッドがあります。

main.js で、次のコードを使用して readResponseAsJSON 関数を追加します。

function readResponseAsJSON(response) {
  return response.json();
}

次に、fetchJSON 関数を次のコードに置き換えます。

function fetchJSON() {
  fetch('examples/animals.json') // 1
  .then(validateResponse) // 2
  .then(readResponseAsJSON) // 3
  .then(logResult) // 4
  .catch(logError);
}

スクリプトを保存してページを更新します。[JSON を取得] をクリックします。コンソールで、examples/animals.json からの JSON が(Response オブジェクトではなく)ロギングされていることを確認します。

説明

それでは、現在の状況を振り返ってみましょう。

ステップ 1. リソース examples/animals.json で Fetch が呼び出されます。Fetch は、Response オブジェクトに解決される Promise を返します。Promise が解決されると、レスポンス オブジェクトが validateResponse に渡されます。

ステップ 2. validateResponse は、レスポンスが有効かどうか(200 かどうか)を確認します。そうでない場合は、エラーがスローされ、残りの then ブロックがスキップされて catch ブロックがトリガーされます。これは特に重要です。このチェックがないと、不正なレスポンスがチェーンに渡され、有効なレスポンスの受信に依存する後続のコードが破損する可能性があります。レスポンスが有効な場合、readResponseAsJSON に渡されます。

ステップ 3. readResponseAsJSON は、Response.json() メソッドを使用してレスポンスの本文を読み取ります。このメソッドは、JSON に解決される Promise を返します。この Promise が解決されると、JSON データが logResult に渡されます。(response.json() からの Promise が拒否されると、catch ブロックがトリガーされます)。

ステップ 4. 最後に、examples/animals.json への元のリクエストの JSON データが logResult によってロギングされます。

詳細情報

Fetch は JSON に限定されません。この例では、画像を取得してページに追加します。

main.js で、次のコードを使用して showImage 関数を記述します。

function showImage(responseAsBlob) {
  const container = document.getElementById('img-container');
  const imgElem = document.createElement('img');
  container.appendChild(imgElem);
  const imgUrl = URL.createObjectURL(responseAsBlob);
  imgElem.src = imgUrl;
}

次に、レスポンスを Blob として読み取る readResponseAsBlob 関数を追加します。

function readResponseAsBlob(response) {
  return response.blob();
}

fetchImage 関数を次のコードで更新します。

function fetchImage() {
  fetch('examples/fetching.jpg')
    .then(validateResponse)
    .then(readResponseAsBlob)
    .then(showImage)
    .catch(logError);
}

スクリプトを保存してページを更新します。[画像を取得] をクリックします。ページに、棒を取ってくるかわいい犬が表示されます(これは fetch のジョークです)。

説明

この例では、画像 examples/fetching.jpg を取得しています。前の演習と同様に、レスポンスは validateResponse で検証されます。レスポンスは、前述のセクションの JSON ではなく、Blob として読み取られます。画像要素が作成されてページに追加され、画像の src 属性が Blob を表すデータ URL に設定されます。

注: URL オブジェクトの createObjectURL() メソッドは、Blob を表すデータ URL を生成するために使用されます。この点に注意してください。画像のソースを Blob に直接設定することはできません。Blob はデータ URL に変換する必要があります。

詳細情報

このセクションはオプションの課題です。

fetchText 関数を次のように更新します。

  1. fetch /examples/words.txt
  2. validateResponse でレスポンスを検証する
  3. レスポンスをテキストとして読み取る(ヒント: Response.text() を参照)
  4. ページにテキストを表示します。

この showText 関数は、最終的なテキストを表示するためのヘルパーとして使用できます。

function showText(responseAsText) {
  const message = document.getElementById('message');
  message.textContent = responseAsText;
}

スクリプトを保存してページを更新します。[テキストを取得] をクリックします。fetchText を正しく実装すると、ページにテキストが追加されます。

注: HTML を取得して innerHTML 属性を使用して追加したくなるかもしれませんが、注意が必要です。このチェックボックスをオンにすると、サイトがクロスサイト スクリプティング攻撃を受けるおそれがあります。

詳細情報

デフォルトでは、fetch は特定のリソースを取得する GET メソッドを使用します。ただし、fetch は他の HTTP メソッドも使用できます。

HEAD リクエストを行う

headRequest 関数を次のコードに置き換えます。

function headRequest() {
  fetch('examples/words.txt', {
    method: 'HEAD'
  })
  .then(validateResponse)
  .then(readResponseAsText)
  .then(logResult)
  .catch(logError);
}

スクリプトを保存してページを更新します。[HEAD リクエスト] をクリックします。ログに記録されたテキスト コンテンツが空であることを確認します。

説明

fetch メソッドは、2 番目のオプション パラメータ init を受け取ることができます。このパラメータを使用すると、リクエスト メソッド、キャッシュ モード、認証情報など、フェッチ リクエストの構成を行えます

この例では、init パラメータを使用して、フェッチ リクエスト メソッドを HEAD に設定しています。HEAD リクエストは、レスポンスの本文が空である点を除き、GET リクエストと同じです。この種のリクエストは、ファイルに関するメタデータのみが必要で、ファイルのすべてのデータを転送する必要がない場合に使用できます。

省略可: リソースのサイズを確認する

examples/words.txt のフェッチ レスポンスのヘッダーを見て、ファイルのサイズを確認しましょう。

レスポンス headerscontent-length プロパティをログに記録するように headRequest 関数を更新します(ヒント: ヘッダーのドキュメントと get メソッドをご覧ください)。

コードを更新したら、ファイルを保存してページを更新します。[HEAD リクエスト] をクリックします。コンソールに examples/words.txt のサイズ(バイト単位)が記録されます。

説明

この例では、HEAD メソッドを使用して、リソース自体を実際に読み込むことなく、リソースのサイズ(バイト単位)(content-length ヘッダーで表される)をリクエストしています。実際には、これを使用して、完全なリソースをリクエストする必要があるかどうか(またはリクエスト方法)を判断できます。

省略可: 別の方法で examples/words.txt のサイズを調べ、レスポンス ヘッダーの値と一致することを確認します(具体的なオペレーティング システムでの確認方法を調べることもできます。コマンドラインを使用するとボーナスポイントが加算されます)。

詳細情報

Fetch は POST リクエストでデータを送信することもできます。

エコー サーバーを設定する

この例では、エコーサーバーを実行する必要があります。fetch-api-lab/app/ ディレクトリから次のコマンドを実行します(コマンドラインが localhost:8081 サーバーによってブロックされている場合は、新しいコマンドライン ウィンドウまたはタブを開きます)。

node echo-servers/cors-server.js

このコマンドは、送信されたリクエストをエコーバックするシンプルなサーバーを localhost:5000/ で起動します。

このサーバーは、ctrl+c を使用していつでも終了できます。

POST リクエストを行う

postRequest 関数を次のコードに置き換えます(セクション 4 を完了していない場合は、セクション 4 の showText 関数を定義していることを確認してください)。

function postRequest() {
  fetch('http://localhost:5000/', {
    method: 'POST',
    body: 'name=david&message=hello'
  })
    .then(validateResponse)
    .then(readResponseAsText)
    .then(showText)
    .catch(logError);
}

スクリプトを保存してページを更新します。[POST リクエスト] をクリックします。送信されたリクエストがページにエコーバックされることを確認します。名前とメッセージが含まれている必要があります(フォームからまだデータを取得していないことに注意してください)。

説明

fetch で POST リクエストを行うには、init パラメータを使用してメソッドを指定します(前のセクションで HEAD メソッドを設定した方法と同様です)。ここでは、リクエストの本文(この場合は単純な文字列)も設定します。本文は送信するデータです。

注: 本番環境では、機密性の高いユーザーデータを常に暗号化してください。

データが localhost:5000/ への POST リクエストとして送信されると、リクエストがレスポンスとしてエコーバックされます。レスポンスは validateResponse で検証され、テキストとして読み取られ、ページに表示されます。

実際には、このサーバーはサードパーティ API を表します。

省略可: FormData インターフェースを使用する

FormData インターフェースを使用すると、フォームからデータを簡単に取得できます。

postRequest 関数で、msg-form フォーム要素から新しい FormData オブジェクトをインスタンス化します。

const formData = new FormData(document.getElementById('msg-form'));

次に、body パラメータの値を formData 変数に置き換えます。

スクリプトを保存してページを更新します。ページのフォーム([Name] フィールドと [Message] フィールド)に記入し、[POST] リクエストをクリックします。ページに表示されたフォームの内容を確認します。

説明

FormData コンストラクタは HTML form を受け取り、FormData オブジェクトを作成できます。このオブジェクトには、フォームのキーと値が入力されます。

詳細情報

非 CORS エコーサーバーを起動する

前のエコーサーバーを停止し(コマンドラインから ctrl+c を押します)、次のコマンドを実行して fetch-lab-api/app/ ディレクトリから新しいエコーサーバーを起動します。

node echo-servers/no-cors-server.js

このコマンドは、別のシンプルなエコーサーバーを localhost:5001/ に設定します。ただし、このサーバーはクロスオリジン リクエストを受け入れるように構成されていません。

新しいサーバーから取得する

新しいサーバーが localhost:5001/ で実行されているので、フェッチ リクエストを送信できます。

localhost:5000/ ではなく localhost:5001/ から取得するように postRequest 関数を更新します。コードを更新したら、ファイルを保存してページを更新し、[POST Request] をクリックします。

コンソールに、CORS Access-Control-Allow-Origin ヘッダーがないためクロスオリジン リクエストがブロックされたことを示すエラーが表示されます。

postRequest 関数の fetch を次のコードで更新します。このコードは、no-cors モードを使用し(エラーログが示すように)、validateResponsereadResponseAsText の呼び出しを削除します(以下の説明を参照)。

function postRequest() {
  const formData = new FormData(document.getElementById('msg-form'));
  fetch('http://localhost:5001/', {
    method: 'POST',
    body: formData,
    mode: 'no-cors'
  })
    .then(logResult)
    .catch(logError);
}

スクリプトを保存してページを更新します。メッセージ フォームに記入して、[POST Request] をクリックします。

コンソールに記録されたレスポンス オブジェクトを確認します。

説明

Fetch(および XMLHttpRequest)は、同一オリジン ポリシーに準拠しています。つまり、ブラウザはスクリプト内からのクロスオリジン HTTP リクエストを制限します。クロスオリジン リクエストは、あるドメイン(http://foo.com/ など)が別のドメイン(http://bar.com/ など)からリソースをリクエストしたときに発生します。

注: クロスオリジン リクエストの制限は、混乱を招くことがよくあります。画像、スタイルシート、スクリプトなどの多くのリソースは、ドメインを越えて(つまり、クロスオリジンで)取得されます。ただし、これらは同一オリジン ポリシーの例外です。クロスオリジン リクエストは、スクリプト内から引き続き制限されます。

アプリのサーバーのポート番号は 2 つのエコーサーバーとは異なるため、どちらかのエコーサーバーへのリクエストはクロスオリジンと見なされます。ただし、localhost:5000/ で実行されている最初のエコー サーバーは、CORS をサポートするように構成されています(echo-servers/cors-server.js を開いて構成を確認できます)。localhost:5001/ で実行されている新しいエコー サーバーは、そうではありません(そのため、エラーが発生します)。

mode: no-cors を使用すると、不透明なレスポンスを取得できます。これにより、レスポンスを取得できますが、JavaScript でレスポンスにアクセスすることはできません(そのため、validateResponsereadResponseAsTextshowResponse を使用できません)。レスポンスは、他の API で使用したり、Service Worker でキャッシュに保存したりできます。

リクエスト ヘッダーを変更する

Fetch はリクエスト ヘッダーの変更もサポートしています。セクション 6 で、localhost:5001(CORS なし)エコーサーバーを停止し、localhost:5000(CORS)エコーサーバーを再起動します。

node echo-servers/cors-server.js

localhost:5000/ から取得する postRequest 関数の以前のバージョンを復元します。

function postRequest() {
  const formData = new FormData(document.getElementById('msg-form'));
  fetch('http://localhost:5000/', {
    method: 'POST',
    body: formData
  })
    .then(validateResponse)
    .then(readResponseAsText)
    .then(showText)
    .catch(logError);
}

次に、ヘッダー インターフェースを使用して、postRequest 関数内の Headers オブジェクトを作成します。この関数は messageHeaders と呼ばれ、Content-Type ヘッダーは application/json に等しくなります。

次に、init オブジェクトの headers プロパティを messageHeaders 変数に設定します。

body プロパティを、次のような文字列化された JSON オブジェクトに更新します。

JSON.stringify({ lab: 'fetch', status: 'fun' })

コードを更新したら、ファイルを保存してページを更新します。[POST リクエスト] をクリックします。

エコーされたリクエストの Content-Typeapplication/json になっていることを確認します(以前は multipart/form-data でした)。

次に、カスタム Content-Length ヘッダーを messageHeaders オブジェクトに追加し、リクエストに任意のサイズを指定します。

コードを更新したら、ファイルを保存してページを更新し、[POST Request] をクリックします。このヘッダーはエコーされたリクエストで変更されていないことに注意してください。

説明

Header インターフェースを使用すると、Headers オブジェクトの作成と変更が可能になります。Content-Type などの一部のヘッダーは、fetch で変更できます。Content-Length などの他のフィールドは保護されており、セキュリティ上の理由から変更できません。

カスタム リクエスト ヘッダーを設定する

Fetch はカスタム ヘッダーの設定をサポートしています。

postRequest 関数の messageHeaders オブジェクトから Content-Length ヘッダーを削除します。任意の値(「X-CUSTOM': 'hello world'」など)を持つカスタム ヘッダー X-Custom を追加します。

スクリプトを保存し、ページを更新して、[POST Request] をクリックします。

エコーされたリクエストに、追加した X-Custom プロパティが含まれていることを確認します。

次に、Headers オブジェクトに Y-Custom ヘッダーを追加します。スクリプトを保存し、ページを更新して、[POST Request] をクリックします。

コンソールに次のようなエラーが表示されます。

Fetch API cannot load http://localhost:5000/. Request header field y-custom is not allowed by Access-Control-Allow-Headers in preflight response.

説明

クロスオリジン リクエストと同様に、カスタム ヘッダーはリソースがリクエストされたサーバーでサポートされている必要があります。この例では、エコーサーバーは X-Custom ヘッダーを受け入れるように構成されていますが、Y-Custom ヘッダーは受け入れません(echo-servers/cors-server.js を開いて Access-Control-Allow-Headers を探すと、ご自身で確認できます)。カスタム ヘッダーが設定されるたびに、ブラウザはプリフライト チェックを実行します。つまり、ブラウザは最初に OPTIONS リクエストをサーバーに送信して、サーバーで許可されている HTTP メソッドとヘッダーを特定します。サーバーが元のリクエストのメソッドとヘッダーを受け入れるように構成されている場合は送信されます。それ以外の場合はエラーがスローされます。

詳細情報

解答コード

作業コードのコピーを取得するには、solution フォルダに移動します。

これで、Fetch API の使い方がわかりました。

リソース

PWA トレーニング コースのすべての Codelab を確認するには、コースのウェルカム Codelab をご覧ください。