まだXMLHttpRequestを使ってるの？　fetchのすすめ

JavaScriptでWeb的なプログラムを書いたことがある人は、**XMLHttpRequest**を使った経験もあるのではないかと思います。XMLHttpRequest，略してXHRは、JavaScript（+DOM）でサーバーとHTTP通信をするための唯一の方法としての地位を長らく保ってきましたが[^xhr_unique]、ここ3〜4年でより新しいAPIである**fetch API**が登場しました。fetch APIが出たばかりの頃は何だこのおもちゃはと正直思いましたが、いつの間にか仕様が充実していい感じになっていました。

[^xhr_unique]: 唯一とはいいつつも、JSONPとかServer-Sent Eventsとか、あるいはFlash等を用いてサーバーと通信する方法も一応ありました。ただ、前2つは汎用性に乏しく、FlashはJavaScriptではありませんから、JavaScriptから使えるAPIとしてはXHRがやはり実質唯一といえます。

皆さんは、この新しいAPIであるfetchをちゃんと使っているでしょうか。それとも、古いXHRを未だに使っているのでしょうか。この記事では、未だにfetchを使っていない人を主な対象としてfetchの使い方を解説します。（XHRと比較しながら解説するので`$.ajax`とか`axios`とかしか使ったことがないよという人はすみませんがサポート対象外です。しかし、この機会にfetchを理解するのもいいかもしれません。）

# fetchの基本：リクエストの発行と結果の取得
HTTP通信の一番の基本であるGETリクエストを発行してみましょう。XHRではこんな感じでしたね。

```js:XHRでのGETリクエスト
var xhr = new XMLHttpRequest();
xhr.open("GET", "/");
xhr.send();
xhr.onload = ()=> {
  console.log(xhr.response);
};
```

まず`XMLHttpRequest`オブジェクトを作り、`open`メソッドでメソッドとURLを指定、そして`send`メソッドでリクエストを送信します。結果は`load`イベントで受け取ります。`load`イベントはリクエストが成功して結果が返ってきたときに発生するイベントです。エラーの処理は簡単のため省きました。

同じことをfetchでやるとこうなります。

```js:fetchでのGETリクエスト
fetch("/", {
  method: "GET",
}).then(response => response.text())
.then(text => {
  console.log(text);
});
```

まず、リクエストは`fetch`関数を呼び出すことで発行します。第1引数がURLです。メソッドとか、あるいはPOSTの場合はリクエスト本文などは第2引数のオプションで指定します。メソッドのデフォルトはGETなので、今回は第2引数を全部省略することも可能です。

`fetch`の返り値はいまどきの非同期処理らしくPromiseです。Promiseの結果は[Responseオブジェクト](https://developer.mozilla.org/en-US/docs/Web/API/Response)であり、後で詳しいことは説明しますが、このオブジェクトからリクエストの結果を文字列で得るためには`text`メソッドを用います。`text`メソッドは再びPromiseを返すので、再びthenメソッドをチェーンして文字列を得ることになります。

このように、XHRではメソッドを何回も呼び出したりイベントを監視したりする必要がありましたが、fetchはPromiseをベースとしたAPIになっており明快です。

# Responseオブジェクト
fetchを使いこなす鍵は上でちらっと出てきた**Response**オブジェクトにあります。そもそもResponseオブジェクトというのは何を表しているのでしょうか。

それを知るためには、まずResponseオブジェクトが得られるのはいつなのか、すなわちfetchの返すPromiseが解決されるのはいつなのかを知る必要があります。実は、**fetchが返すPromiseが解決されるのはレスポンスヘッダが全部返ってきたとき**です[^fetch_spec_all_headers]。

[^fetch_spec_all_headers]: 仕様を完全に読み込んだわけではありませんが、[fetchの仕様のHTTP-network fetchの項目](https://fetch.spec.whatwg.org/#concept-http-network-fetch)の4-3に“Wait until all the headers are transmitted.”とあります。

HTTPのレスポンスというのはまずステータスコード、次にヘッダ、そして最後に本文（レスポンスボディ）という順番ですから、この時点ではまだ本文が到着していないことになります。これが、本文を文字列で取得する`response.text()`メソッドがPromiseを返す理由です。このPromiseは、さらにレスポンスの本文が全部到着するのを待ってから解決されることになります。

逆に言えば、ステータスコードやヘッダの情報は、Responseオブジェクトから直に（さらに待つことなく）取得できるということです。ステータスコードやヘッダはこのように取得します。

```js:fetchでステータスコードとヘッダを取得
fetch("/")
.then(response => {
  console.log(response.status); // 多分200
  console.log(response.headers.get('Content-Encoding')); // "gzip"とか
});
```

ちなみに、Responseオブジェクトはちょっと便利なプロパティとして`ok`プロパティを持っています。これは真偽値で、通信が成功したかどうかを表します。ここでの成功というのは、ステータスコードが200番台だったかどうかです。

XHRでも、一応ヘッダーだけが返ってきて本文はこれからというタイミングに処理を挟むことはできました。readystatechangeイベントを監視してreadyStateが2になったタイミングで処理をすればよいのです。しかし、readystatechangeイベントを使うよりはPromiseベースのAPIのほうが直感的でよいのではないかと思います。

これはXHRにも言えることですが、ここで注意すべきは、何を以って通信が成功したかと言うのかということです。そもそも、Promiseというのは成功する場合と失敗する場合があります。例えば`fetch`の返り値のPromiseが成功するのはいつで失敗するのはいつでしょうか。

答えは、大雑把に言えば「サーバーからレスポンスが（少なくともヘッダーまで）返ってきたら成功」です。これは、たとえステータスコードが404とかだったとしても成功であるということを意味します。逆に失敗するのは、サーバーに接続できなかったとか、CORSに引っかかったとか、そういう場合です。もし404を失敗にしたいのであれば、下のように自分でいい感じに処理する必要があります。

```js:正常でないステータスコードを失敗にする例
fetch("/non-existent-page")
.then(async response => {
  if (response.ok) {
    return response.text();
  } else {
    throw new Error(`Request failed: ${response.status}`);
  }
})
.catch(err => console.error(err)); // Error: Request failed: 404
```

ここまで見たように、Responseオブジェクトというのは、ヘッダーまでのレスポンスで得られた情報を表すオブジェクトであるというのが1つの側面です。`status`や`headers`などを通してこれらの情報を得ることができます。

そして、もうひとつの側面が、これから取得されるであろうレスポンス本文を表すオブジェクトという側面です（仕様書的にはこれを[Body mixin](https://developer.mozilla.org/en-US/docs/Web/API/Body)と読んでいます）。最初の例で使用した`response.text()`メソッドはこれに属するメソッドです。

# レスポンス本文の取得方法
ここまではResponseオブジェクトの`text()`メソッドを使ってレスポンスを文字列として取得してきました。XHRで言うところの`responseType = "text"`に相当する動作です。しかしこれでは不便な場合がありますから、文字列以外にもいくつかのデータ形式を選べるようになっています。XHRで選べたのは以下の形式でした。

| responseType | 意味 |
|--------------|------|
| `"text"`     | 文字列 |
| `"arraybuffer"` | [ArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer) |
| `"blob"` | [Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob) |
| `"json"` | レスポンス文字列をJSONとしてパースした結果のオブジェクト |
| `"document"` | レスポンス文字列をHTMLまたはXMLとしてパースした結果のDocumentオブジェクト |

バイナリデータを扱いたいときは用途に応じて`"arraybuffer"`か`"blob"`を選びます。`"json"`は普通に少し便利なやつですね。そして、`"document"`が特徴的です。XHRはXMLを名に冠しているくらいですからXMLをパースできたのです。

一方、fetchではResponseオブジェクトに次のようなメソッドが用意されています。もちろん、返り値は全てPromiseです。

| メソッド | 結果 |
|--------------|------|
| `text()`     | 文字列 |
| `arraybuffer()` | [ArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer) |
| `blob()` | [Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob) |
| `json()` | JSON |
| `formData()` | [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData) |

見て分かる通り、`"document"`が消えた代わりにFormDataが増えました。FormDataというのは、`multipart/form-data` MIMEタイプで送信されたデータをいい感じに表すやつです。[^response_formdata]

[^response_formdata]: サーバが`mutlipart/form-data`でデータを送ってくることはあまり無いでしょう。どちらかと言うとこれはServiceWorker内でユーザーからのリクエストボディを読むときに使うやつです。

`"document"`が無いとXMLとかをパースできなくて困るという方がいるかもしれませんが、ご安心ください。文字列で取得したあとに[DOMParser](https://developer.mozilla.org/en-US/docs/Web/API/DOMParser)でパースすればいいのです。

というわけで、おおよそXHRと同じことができることが分かりましたね。

# ストリーム
次に、XHRには無いfetchならではの点を紹介します。これはいくつかあるでしょうが、一つ代表的なのはレスポンス本文を**ストリーム**として扱えるという点です。ストリームの話をするとfetch APIではなく[Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API)という別のAPIの話に片足を突っ込むことにはなるのですが、折角なので少し紹介します。

ストリームというのは、データが徐々にやってくるやつです。HTTP通信というのはインターネットを介してデータを送ってもらうわけですから、データは一瞬で全部がやってくるのではなくパケットに分割されたりして（あまり詳しくないので詳細な話は避けますが）順々にやってくるわけです。

となると、データを前から順に見ていくような処理を行いたい場合には、データが全部やってくるのを待つ必要はありません。既に到着したデータから順に見ていくことが可能なはずです。さらに、既に見たデータはもう不要という場合は、レスポンスの全部を律儀に覚えておくのではなく見たそばからデータを捨てることができます。このような処理は、上で紹介したメソッドでは実現できません。これらは全てデータの全体が届くまで待ってまとめて取得するものだからです。

XHRではレスポンスをストリームとして扱うことはできませんでした[^xhr_stream]。progressイベントを用いて現在何バイト受信したかを知ることだけはできましたが。

[^xhr_stream]: 一応、responseTypeが`"text"`のときは受信の途中でもデータにアクセスすることはできたようです（[MDN](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/response)）。

## ストリームの例1

あまり意味のない例ですが、レスポンス本文をストリームとして扱い、レスポンス中の`'<'`の文字を数える例を出しておきます。余談ですが、[ReadableStreamのreadメソッド](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamDefaultReader/read)はAsyncIteratorとAPIが似ていますね。ECMAScriptとDOMで微妙に食い違ったり歩調が揃わなかったりするのはそこそこよくあるという印象ですが。

```js:fetchとストリームの例
fetch("/")
.then(async response => {
  // 結果を数えるための変数
  let count = 0;
  // response.body にレスポンス本文のストリーム（ReadableStream）が入っている
  // ストリームのReaderを作成
  const reader = response.body.getReader();
  while (true) {
    // ストリームからデータを読む
    const {done, value} = await reader.read();
    if (done) {
      // doneがtrueならストリームのデータを全部読み終わった
      break;
    }
    // 読んだデータはバイナリデータ（Uint8Array）で与えられる
    for (const char of value) {
      // 文字'<' (0x3c) を見つけたらカウント
      if (char === 0x3c) {
        count++;
      }
    }
  }
  return count;
}).then(count => console.log(count)); // Qiitaのトップページで試してみたら262でした
```

## ストリームの例2
XHRのprogressイベントで現在受信したバイト数を知ることができると先ほど述べました。せっかくなので、同じことをストリームを使ってやってみましょう。まずはXHR版を作ります。

```js:XHRで受信したバイト数を得る例
var xhr = new XMLHttpRequest();
xhr.open("GET", "/");
xhr.send();
xhr.onprogress = e => {
  console.log(`${e.loaded} / ${e.total}`);
};
```

実際にこれを使ってUIとかを作ろうとするともう少し複雑になりそうですが、これだけなら簡単ですね。fetchでは以下のように行なえます。

```js:fetchで受信したバイト数を得る例
fetch("/")
.then(async response => {
  // 全バイト数を先に取得
  const total = Number.parseInt(response.headers.get("Content-Length"));

  // 受信したバイト数
  let loaded = 0;

  const reader = response.body.getReader();
  while (true) {
    const {done, value} = await reader.read();
    if (done) {
      break;
    }
    // 読んだデータはバイナリデータ（Uint8Array）で与えられる
    loaded += value.length;
    console.log(`${loaded} / ${total}`);
  }
});
```

これらの例は`Content-Length`ヘッダに依存しているのでContent-Lengthが送られてこない場合などは正しい表示になりませんが、まあ仕方ありませんね。

fetch API（とStreams API）を使った例のほうがなんだか長い気がしますが、まあ生のAPIを叩いていればこんなものでしょう。いい感じのライブラリなどもあるのかもしれません（ちゃんと調べていませんが）。

# リクエストのキャンセル
XHRでは、発行したリクエストをキャンセルすることができました。リクエストを発行した直後にキャンセルする意味不明な例は以下のとおりです。

```js:XHRでのリクエストのキャンセル
var xhr = new XMLHttpRequest();
xhr.open("GET", "/");
xhr.send();
// 一瞬待ったあとにキャンセル
setTimeout(()=> xhr.abort(), 0);
// キャンセルされたイベントを検知
xhr.onabort = e => console.error(e);
```

では、fetchの場合はどうするのでしょうか。fetchの返り値はPromiseですが、Promiseは成功と失敗があるのみで、処理をキャンセルする機能は備わっていません。非同期処理を全部Promiseで扱うとなればそのような「キャンセル可能なPromise」にも当然需要が発生するはずですが、現在のところ[議論はある](https://github.com/tc39/proposal-cancellation)ようですが標準化は遠い状況です。

そこで、fetch（というかDOM）では独自のアプローチをとっています。それが[AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController)です。AbortControllerオブジェクトを生成すると対応する`signal`が得られるので、それをfetchに渡します。その後、AbortControllerオブジェクトの`abort()`を呼び出すことでリクエストをキャンセルできます。

以上をコードにすると以下のようになります。実行すると、リクエストが失敗することが分かります。

```js:fetchのキャンセル
// 新しいAbortControllerを生成
var controller = new AbortController();
fetch("/", {
  // controllerが持つsignalをfetchに渡す
  signal: controller.signal,
})
.then(response => console.log('成功', response.status))
.catch(err => console.log('失敗', err));

// 一瞬待った後にキャンセル
setTimeout(()=> controller.abort(), 0);
```

# fetchでしかできないこと
XHRはできないがfetchではできることとして先ほどストリームの扱いを紹介しましたが、以前は他にもありました。というのも、一時期[Credential Management API](https://developer.mozilla.org/en-US/docs/Web/API/Credential_Management_API)ではパスワード情報を保持したPasswordCredentialオブジェクトを`fetch`のオプションの一つ`credential`に渡すことでサーバーにパスワード情報を送るようになっていました。このPasswordCredentialオブジェクトはfetchでサーバーに送信することでパスワードがサーバーに送信できましたが、クライアントのスクリプトはこのオブジェクトからパスワード情報を取り出すことができませんでした。こうすることで、パスワード情報に触れるスクリプトを減らしてセキュリティリスクを下げる意味があったのでしょう。

これはfetchのみの機能でXHRには対応していませんでした。もっとも、この機能は[のちに廃止され](https://developers.google.com/web/updates/2017/06/credential-management-updates?hl=ja)、今は普通にPasswordCredentialオブジェクトからパスワードを取り出せるようになったのでfetchに特化した仕様ではなくなりました。

とはいえ、このようにfetchのみの仕様が今度も追加されるかもしれません。それに備えてXHRからfetchに乗りかえておくのがよいでしょう。

# ServiceWorkerとの関係
**ServiceWorker**は今をときめくDOMのすてき機能で、いわゆるPWAにおいて重要な役割を果たす機能です。ServiceWorkerの目玉機能のひとつが、ページからの通信に割り込んでキャッシュを働かせたりすることができる機能です。実は、そのような通信を扱うのにあたってこの記事で紹介したResponseオブジェクト、そしてその対となるRequestオブジェクトなどが使われるのです。fetchの使い方を知っておくことで将来的にServiceWorkerを扱いたくなったときに役に立つかもしれません。

# まとめ
このQiitaにもfetchを紹介する記事はいくつかあるわけですが、何かどれも中途半端な気がしたので自分の勉強も兼ねてまとめ直してみました。

この記事では**fetch**の使い方を**XMLHttpRequest**と比較しながら紹介し、XHRと同様なHTTP通信がよりモダンなやり方でできることを示しました。CORS周りの機能など、この記事では紹介していないfetchの機能も色々ありますので、よかったら調べてみてください。

ES2015でPromiseといういい感じの非同期処理のやり方が追加されて以降（キャンセルをどうするのという問題は残っていますが）、非同期処理はPromiseベースのAPIでやるのが普通になりました。その流れに乗ったfetchですから、XHRを使うという面倒なことをせずにどんどんfetchを使っていきたいものです。

# Q&A
- **Q.** どうせIEじゃ使えないんでしょ？
- **A.**  [ Polyfill とは ] [ 検索 ][^fetch_polyfill]
- **Q.** Polyfillとかそんな変なライブラリ勝手に使えないんですけど！
- **A.** 転職したら？
- **Q.** [axios](https://github.com/axios/axios)で良くない？ / jQueryで良くない？ / （ここに好きなライブラリを入れよう）で良くない？
- **A.** そうだね。でもWeb標準もちゃんと勉強しといたほうが良くない？

[^fetch_polyfill]: ストリーム周りは実装されていないPolyfillが多いみたいですが。