この Codelab は、Google Developers トレーニング チームが開発した「プログレッシブ ウェブアプリの開発」トレーニング コースの一部です。Codelab を順番に進めると、このコースを最大限に活用できます。
コースの詳細については、プログレッシブ ウェブアプリの開発の概要をご覧ください。
はじめに
このラボでは、リソースを取得するためのシンプルなインターフェースである Fetch API の使用方法について説明します。これは XMLHttpRequest API の改良版です。
学習内容
- Fetch API を使用してリソースをリクエストする方法
- fetch を使用して GET、HEAD、POST リクエストを行う方法
- カスタム ヘッダーの読み取りと設定方法
- CORS の使用方法と制限事項
必要な予備知識
- 基本的な JavaScript と HTML
- ES2015 の Promise のコンセプトと基本的な構文を理解していること
必要なもの
注: 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.json
とpackage.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
関数に渡されます。
レスポンス オブジェクトは、リクエストに対するレスポンスを表します。これらには、レスポンス本文だけでなく、便利なプロパティとメソッドも含まれています。
無効なレスポンスをテストする
コンソールでログに記録されたレスポンスを確認します。status
、url
、ok
プロパティの値をメモします。
fetchJSON
の examples/animals.json
リソースを examples/non-existent.json
に置き換えます。更新された fetchJSON
関数は次のようになります。
function fetchJSON() {
fetch('examples/non-existent.json')
.then(logResult)
.catch(logError);
}
スクリプトを保存してページを更新します。[JSON を取得] をもう一度クリックして、存在しないリソースの取得を試します。
フェッチが正常に完了し、catch
ブロックがトリガーされなかったことを確認します。新しいレスポンスの status
、URL
、ok
プロパティを見つけます。
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 レスポンスは ReadableStreams(streams 仕様)として表され、レスポンスの本文にアクセスするには読み取る必要があります。レスポンス オブジェクトには、これを行うためのメソッドがあります。
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
関数を次のように更新します。
- fetch
/examples/words.txt
validateResponse
でレスポンスを検証する- レスポンスをテキストとして読み取る(ヒント: Response.text() を参照)
- ページにテキストを表示します。
この 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
のフェッチ レスポンスのヘッダーを見て、ファイルのサイズを確認しましょう。
レスポンス headers
の content-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 モードを使用し(エラーログが示すように)、validateResponse
と readResponseAsText
の呼び出しを削除します(以下の説明を参照)。
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 でレスポンスにアクセスすることはできません(そのため、validateResponse
、readResponseAsText
、showResponse
を使用できません)。レスポンスは、他の 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-Type
が application/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 をご覧ください。