この記事では、JavaScript における非同期ネットワーク通信の手段の一つである Fetch API について解説する。
同期通信と非同期通信
ブラウザに表示されている内容は、ブラウザから URL を指定してリクエスト(要求)を送信し、ネットワーク(インターネット)を介して、その URL で指定したデータを保有しているWebサーバーがレスポンス(応答)を返すことで取得されている。
そしてこのページ内容を取得する通信には、同期通信と非同期通信の2種類が存在する。
同期通信の場合、ブラウザはリクエストを送信すると受信待ちになり、処理を停止するため、ユーザーの操作を受け付けなくなる。その後、Webサーバーからの応答があると、取得した新しいページを画面に表示させ、ユーザーの操作も受け付けるようになる。
一方、非同期通信の場合は、JavaScript でリクエストを送信し、イベント処理としてWebサーバーからの応答を待ち受ける。そのため、ブラウザの処理は停止せず、ユーザーの操作も可能な状態が維持される。そして、Webサーバーからの応答があるとイベントハンドラー(コールバック関数)が発火し(呼び出され)、そのコールバック関数の中で受信したデータに対する処理を行ない、ページの一部にその結果を反映させる。
JavaScript で同期通信を行うなら、html の <form> 要素を取得し、その中のプロパティやメソッドを使用する方法が取られる。
一方で非同期通信の場合は、ブラウザの JavaScript 実行環境が用意する Web APIs に含まれている XMLHttpRequest あるいは Fetch API を使用したり、あるいはこれらを使って実装されているライブラリ(axios など)を使用する方法がある。
この記事では、Fetch API を利用した非同期通信について解説する。
fetch メソッド
fetch メソッドの第一引数には URL を指定する
Fetch API を使ってリクエストの送信・レスポンスの受信を行うには、fetch メソッドを使用する。下の画像に示すように、fetch メソッドは Window オブジェクトに含まれている。
そのため window.fetch でアクセスできるが、window は省略可能なため、単に fetch でもアクセスできる。試しに呼んでみよう。
「1 argument required, but only 0 present.」、つまり少なくとも1つの引数を与える必要があるという内容のエラーが表示された。それもそのはず、リクエストをするからにはその対象を指定する必要がある。
fetch メソッドでは、第一引数でリクエストの対象の URL を指定することになっている。ただし、ブラウザのセキュリティ上の観点から、JavaScript の非同期通信でリクエストが許可されるページは制限されている。(参考:MDN)
そこで今回はこのセキュリティの制限に引っ掛からずにリクエストが許可される、Google Books APIs を使って fetch メソッドを試してみたいと思う。
fetch メソッドの戻り値は Promise
Google Books API は、「Google ブックス」に登録されている書籍などのデータを、API経由で取得できるサービスであり、例えばキーワードを指定して検索を行いたいときは、「https://www.googleapis.com/books/v1/volumes?q=キーワード」を URL に指定してリクエストを送信すればよい。(参考:API Reference)
今回はキーワードとして “javascript” を指定してみることにする。そして実際に URL を指定して fetch メソッドによるリクエストを実行したものが下の画像だ。
この画像から次のことが分かる。
- fetch メソッドは実行すると、未解決(”pending”)の Promise のオブジェクトを戻り値として返却する。
- サーバーからの応答を受信すると、その Promise インスタンスは Response クラスのオブジェクトで解決される。
非同期通信について説明した際に、JavaScrip の非同期通信は、Webサーバーからの応答をイベントとして待ち受け、応答を受信するとコールバック関数が呼び出されると説明した。XMLHttpRequest であればその説明は正しいのだが、Fetch API の場合は少しその説明を修正する必要がある。
Fetch API の場合は、応答を受信するとコールバック関数が呼び出されるのではなく、fetch メソッドが返却した Promise のオブジェクトが解決される。そしてその Promise オブジェクトが内部に保有する結果データ(Response オブジェクト)は、then メソッドに登録したコールバック関数で受け取る、と言う方が正確だろう。
Response オブジェクト
それでは fetch メソッドの返却した Promise のオブジェクトから、Response オブジェクトを取り出して調べてみよう。
const promise = fetch('https://www.googleapis.com/books/v1/volumes?q=javascript');
promise.then(function(response) {
console.log(response);
});
Response オブジェクトには次のようなプロパティが含まれていることが分かる。(参考:MDN)
プロパティ | 意味 |
---|---|
body | レスポンスの本体(ReadableStream オブジェクト)を取得 |
bodyUsed | レスポンスの本体が既に読み取られたかどうか |
headers | レスポンスのヘッダー(Headers オブジェクト) |
ok | リクエストが成功したか (HTTPステータスコードが 200~299 の場合に true) |
redirected | レスポンスがリダイレクトの結果であるか |
status | HTTPステータスコード |
statusText | ステータスコードに対応したステータスメッセージ |
type | レスポンスのタイプ |
url | レスポンスの URL |
また、__proto__(つまり、Response クラスの prototype オブジェクト)を確認すると、Response オブジェクトでは下の画像のようなメソッドを使用できることが分かる。
これらのメソッドの意味は次のようになっている。
メソッド | 意味 |
---|---|
arrayBuffer | レスポンスの本体を ArrayBuffer オブジェクトに変換し、 そのオブジェクトで解決される Promise を返却する。 |
blob | レスポンスの本体を Blob オブジェクトに変換し、 そのオブジェクトで解決される Promise を返却する。 |
clone | Response オブジェクトのクローンを生成する。 |
formData | レスポンスの本体を FormData オブジェクトに変換し、 そのオブジェクトで解決される Promise を返却する。 |
json | レスポンスの本体を JSON オブジェクトに変換し、 そのオブジェクトで解決される Promise を返却する。 |
text | レスポンスの本体をテキスト(文字列)に変換し、 そのテキストで解決される Promise を返却する。 |
ほとんどのメソッドが、レスポンスの本体(body)をそれぞれの形式のオブジェクトに変換してから返却するものになっている。変換に時間のかかるレスポンスも想定しているためだと思うが、返却されるのは変換したオブジェクトそのものではなく、そのオブジェクトで解決される予定の Promise オブジェクトであることに注意が必要だ。
レスポンス(応答)の取得
それでは Response オブジェクトを使用して、実際にレスポンスの中身(本体)を取得してみよう。
まず、Google Books APIs がどのような形式のレスポンスを返しているのかを確認するために、text メソッドを使用してテキストとしてレスポンスの中身を取得してみる。
const promise = fetch('https://www.googleapis.com/books/v1/volumes?q=javascript');
promise
.then(function(response) { // fetch が返した Promise の解決を待つ
return response.text();
})
.then(function(text) { // response.text が返した Promise の解決を待つ
console.log(text);
});
最初の then メソッドで fetch メソッドが返却した Promise オブジェクトの解決、つまりWebサーバーからの応答(レスポンス)の受信を待っている。
レスポンスを受信すると最初の then メソッドに渡したコールバック関数が発火し、Response オブジェクトが渡ってくるので、それを引数 response で受け取っている。
そして今度はレスポンスの内容をテキストとして読み出すために、Response オブジェクトが持つ text メソッドを実行し、その戻り値である Promise オブジェクトを return している。
これにより、最初の then メソッドが返却した Promise オブジェクトは、text メソッドが返却した Promise オブジェクトの解決を待つようになる。2つ目の then メソッドではその解決、つまりテキストへの変換の完了を待っている。
そしてテキストへの変換が完了すると、2つ目の then メソッドに渡したコールバック関数が発火し、引数でテキストを受け取ってコンソールに表示させている。
以上がこのプログラムの流れとなっている。そして実際にこのプログラムを実行すると次のように表示される。
この結果から、Google Books APIs が JSON 形式でレスポンスを返却していることが分かる。そのため、Response オブジェクトの json メソッドを使って JSON オブジェクトに変換するのが適切だろう。それには先ほどのコードを以下のように変更する。
const promise = fetch('https://www.googleapis.com/books/v1/volumes?q=javascript');
promise
.then(function(response) { // fetch が返した Promise の解決を待つ
return response.json();
})
.then(function(jsonObj) { // response.json が返した Promise の解決を待つ
console.log(jsonObj);
});
あるいは、async / await 構文を使用するのなら、次のように書くことができる。
async function asyncfunc() {
const response = await fetch('https://www.googleapis.com/books/v1/volumes?q=javascript');
const jsonObj = await response.json();
console.log(jsonObj);
}
asyncfunc();
これを実行すると次のような結果を得る。
この画像から、Google Books APIs が返却したレスポンスの内容を、JavaScript のコードで処理可能なオブジェクトに変換し、読み込むことに成功したことが分かる。
後はこの例であれば、取得したオブジェクト jsonObj の items プロパティに配列として検索キーワード “javascript” にヒットした書籍のデータが格納されているので、ループ文を使って一つずつ取り出して処理を行えばよいだろう。
ただしこの例の場合、ヒットした件数が 955 件にも関わらず、10 件しか取得できていない。これは Google Books APIs がデフォルトの設定では、一度のリクエストで 10 件しかデータを返さないためだ。設定を変更しても最大で 40 件しか返さない。(参考:Pagination)
そのため、955 件のデータを全て取得するためにはキーワード以外にパラメータを指定して、複数のリクエストを繰り返す必要があるが、fetch メソッドの使い方の説明というこの記事の趣旨から外れるので、これ以上は踏み込まない。