Block Rockin’ Codes

back with another one of those block rockin' codes

Fetch API 解説、または Web において "Fetch する" とは何か?

Update

  • [14/11/11]: Chromium での実装が M40 からあるそうなので、末尾に引用追記させていただきました。
  • [14/11/12]: この記事を書くにあたって、色々なかたにレビューや助言を頂いたのですが、謝辞などが一切抜けてました、本当にすいません。追記しました、ご協力頂いた方々本当にありがとうございました。

WHATGW Fetch Spec

WHATWG のメンテナンスするドラフトに Fetch Spec が追加されました。 もうすでに日本語訳もあります、すばらしい。Fetch Standard 日本語訳

この仕様には二つのことが定義されています。

  • "Fetching": Fetch するとは何か? の定義
  • "Fetch API": fetch() の定義

後者の定義に基づく fetch() という DOM API の実装も始まっています。(詳細は後述)

しかし、実は前者が定義されたことも結構重要だと個人的には思っています。

とうことで、今回は以下の二つについて解説したいと思います。

  • なぜ今更 "Fetching" が定義されたのか?
  • "Fetch API" は何に使うのか?

Fetching の定義

Fetch とはどのような処理を意味するか?

実は、この仕様の中の Section 1~4 が "Fetching" ずばり

Web において、 Fetch する(Fetching)とは何を意味しているか?

という定義になります。 仕様としては "Fetching Algorithm" が明記され、ブラウザはその通り処理することで "Fetching" を標準化しています。

なぜ今更仕様が出たのか?

Fetch が何かを正確に答えられなくても、「なんとなくリクエストを発行してレスポンスとしてドキュメントを取ってくる。。」的なイメージはきっとあるでしょう。実際そんな感じです。

ということは、私たちは「Web が始まった瞬間から」Fetch し続けてきた筈です。 では、なぜ今更独立した仕様として Fetch の仕様が出来たのか?という疑問が出ます。

そもそも Fetching って定義されていなかったのでしょうか?そんなことはありません。

従来の定義

例えば WHATWG HTML の仕様の中には、"fetching-resources" に関する定義があります。

HTML Standard#fetching-resources

Living Standard なので最新になってしまっています。(WHATWG の diff って途中までしか見られない気がするのでどう追ったらいいのか。。)

ということでバージョン管理されている W3C の方で溯ると 2009 年あたりにこの節が追加されたことがわかります。

http://www.w3.org/TR/2009/WD-html5-20090212/infrastructure.html#fetching-resources

以下の内容で始まるこの仕様が、いわゆる "fetch algorithm" として育って行くことになります。

"When a user agent is to fetch a resource, the following steps must be run:"

(ちなみにこの時の仕様には、注で「なにか見落としている点があったら教えてくれ」と書かれています。)

最初は、非常に短い仕様で以下の二つだけでした。

  • Protocol concepts
  • Encrypted HTTP and related security concerns

ここに様々なケースが追加されて行くのですが、一つの大きな転換が 2012/3 の更新 で追加されています。 それが CORS です。

Cross Origin での Fetch

クロスオリジンなリクエストを発行する場合、 Simple でないリクエストの場合は Preflight による事前ネゴシエーションが必要になり、単純に GET を投げてレスポンスを取得するのと比べれば多くの処理が必要になります。

おそらく、 CORS により従来の単純だと思っていた Fetching が、以外とややこしい仕様になってしまったため、エディタ達は CORS の仕様を整理し始めたようです。

実はこの Fetch の仕様(github で管理されている) の最初のコミットを見てみると、もともとは CORS の仕様だったことがわかります。

https://github.com/whatwg/fetch/commit/58a721afdb3d202935439952b703f69b59395598#diff-8d4d847e6257b75f4bf8030496281de4R4

最初のうちは CORS での Preflight や Cookie を始めとする各ヘッダについての記述の厳密化などが行われていたようです。

CORS -> Fetch へ

すると、いつ CORS が Fetch という仕様に名前を変えたのかが気になります。

Github でたどると、以下のコミットで "new reality" という謎のコミットメッセージとともに HTML の仕様にある fetch の項 とマージされています。

https://github.com/whatwg/fetch/commit/d94f992865142088724866ad5b4e7bea0216897e

このコミットメッセージからは読み取れません(コミットメッセージはちゃんと書きましょう)が、なんとなくここから CORS の仕様が Fetch によって解消し、 W3C から WHATWG に移されていったことが読み取れると思います。

ちなみに、 W3C WebApps WG の ML をたどって、彼らがこのころ何を揉めていたのかを調べてみると、同じくらいの時期に以下のスレッドが関連しそうな話をしています。

[XHR] What referrer to use when making requests from Jonas Sicking on 2012-07-06 (public-webapps@w3.org from July to September 2012)

内容はざっくりこんな感じ。

「DOM とかを色々いじったり、 pushState したあとに発行した
リクエストの referrer ってどうすんの?」

で、この辺は、仕様に無いわけではないけど、 「もう少しちゃんと定義しよう」 という感じで終わったようです。

おそらくこのあたりになってから、新しい仕様が追加されるごとにアルゴリズムを更新するのではなく、共通した仕様としての Fetch を定義し、そこを全体から参照するように整理しようという流れであることは伺えます。

同様のことは、仕様策定者の Anne 自身もブログに書いています。

Fetching URLs — Anne’s Blog

実際に WHATWG の XHR の仕様ではすでに "fetching" が参照されています。

ServiceWorker のような仕様についても、この Fetch の仕様をアップデートすれば、全体がアップデートできるということだと思います。

なぜ今 Fetch か?

まとめると、以下のような理由だと推測できます。

  • 「Fetch する(Fetching)」 という処理は、思った以上に複雑になってしまった。
    • CORS, PushState, Service Worker etc...
  • それをまとめた仕様にすることで、他から参照可能にし、全体を整理する。
  • 新たな仕様により変更が入った場合、ここを更新する。

では、それをふまえて最新の定義をざっと眺めてみます。

最新の "Fetching" 仕様

執筆時点での Fetching の定義、つまり section 4 は、ざっと以下のような内容になってます。 メモレベルです。

4.1 Basic fetch

basic な fetch を実行する場合、 URL のスキーマに応じて処理が別れる。

  • about
    • 基本は "blank" のみ、他はネットワークエラー(about:config は navigation のレイヤで扱われ、 fetch のコンテキストではネットワークエラー扱い)
  • blob, data
    • 要するに blob や data を取得する
  • file, ftp
    • 仕様としは定義されて無く、現実世界での実装がそのあまま有効となる、らしい
  • filesystem
    • flag によるエラーは定義されているが、そうでない場合は未定。
  • http, https
    • 次セクションに定義
  • 他、エラー
    • "chrome:" はもちろん独自仕様ということ。

4.2 HTTP fetch

ざっと言うと http スキーマでのリクエストの処理アルゴリズムについて規定されています。 ただし、これは各フラグに応じた HTTP リクエストを投げる直前までの準備についてと、レスポンスのステータスコード毎の処理についてです。 注目すべきは、以下についてきちんとそのアルゴリズムに組み込まれている点でしょう。

  • CORS の場合
  • CORS かつ Preflight が必要な場合
  • 認証が必要な場合
  • Service Worker から実施された場合
  • キャッシュが有効な場合
  • 301-304, 307, 308, 401, 407 の場合の挙動

4.3 HTTP network or cache fetch

前段で準備されたリクエストを発行する際の処理アルゴリズムについて書かれています。

  • リクエスト
  • Referrer
  • Origin
  • Cookie
  • Authorization
  • キャッシュの有無による分岐
  • レスポンス
  • Content-Encoding/Type
  • Set-Cookie の処理

4.4 CORS preflight fetch

CORS の preflight での OPTIONS リクエストの発行処理。

4.5 CORS preflight cache

CORS の preflight のキャッシュの処理。

4.6 CORS check

CORS での Access-Control-Allow-Origin / Access-Control-Allow-Credentials の処理。

Fetch API

さて次は section 5 に定義された Fetch API についてです。

この API はここまでに定義してきた Fetching の定義、つまり Fetch Algorithm を実装した、 「ドキュメントを Fetch するための API」です。

chromiumfirefox で、すでに実装が始まっているようです。(ただし、現時点では Service Worker 内のみ)

pollyfil も作られています。

ServiceWorker での利用

ただし、chromium/firefox どちらの実装も、 ServiceWorker での利用にとどめられています。これはまだ fetch()API が Window に公開されても大丈夫かどうかといった検証が必要だからだそうです。

この辺は ServiceWorker で入った Cache API でも同じようにコンセンサスを取るために議論されてるようです。

https://github.com/slightlyoff/ServiceWorker/issues/535 https://github.com/slightlyoff/ServiceWorker/issues/297

基本的に Fetch 関連の isssue はまだ ServiceWorker のリポジトリで管理されているものが多いようです。

API

基本的な API は以下のような感じです。

fetch('/').then(res => res.text()).then(text => console.log(text));

お気づきの通り Promise ベースで設計されています。 よりオプションが必要な場合は、以下のようになります。

fetch('http://my.api.org/', {
  method: 'post',
  headers: {
    'content-type': 'application/json'
  },
  body: JSON.stringify({
    user: 'Jxck'
  }),
  credentials: 'cors',
  chache: 'force cache'
}).then(res => {
   console.log(res.url, res.type, res.status);

   if(res.headers.get('content-type') === 'application/json') {
     res.json().then(json => console.log(json));
   } else {
     // res.arrayBuffer();
     // res.blob();
     res.text().then(text => console.log(text));
   }
}).catch(err => console.error(err));

だいたいどんな感じかはわかるかと思います。

Fetch API と XHR

XHR の実装がある現在、一体なぜ Fetch API が必要なのでしょうか? Fetch API の仕様の最初には以下が書かれています。

The fetch() method is relatively low-level API for fetching resources.
It covers slightly more ground than XMLHttpRequest, 
although it is currently lacking when it comes to reporting progression.

現時点で Fetch API の Polyfill が XHR で実装されていますが、これはまあ他に方法が無いからですね。

実際は Fetch API は XHR よりもより低レベルな API となります。そして完全な Polyfill は XHR では書けません。

XHR はもともと IE が実装していたものが Ajax とか Web2.0 的なあれでびっくりするくらい広まってしまったものが、仕様にあとから落とされていった経緯があり、 XHR2 でもそれらと互換性を壊さないように発展してきたものです。

Fetch API をふまえて、後だしじゃんけん的に XHR を見て言えば「中途半端に高レベル」であることと、「ちょっと古い API」だと言えると思います。

API のレベル

API レベルで言うと Fetch では以下のようなこまかい部分まで定義ができます。 この辺は、 Fetching Algorithm 内で、処理の分岐に使われる "Flag" がそのまま API として外に出ているイメージです。

ここまで細かい明示的な制御は、従来の XHR ではできなかったはずです。(間違ってたら教えてください。)

enum RequestMode { "same-origin", "no-cors", "cors" };
enum RequestCredentials { "omit", "same-origin", "include" };
enum RequestCache { "default", "bypass", "reload", "revalidate", "force-cache", "offline" };

また、 API として外に出ているのは fetch() という関数一つですが、内部では Request/Response/Headers/Body などの interface が切り分けられて、整理されています。(この Request / Response は例えば ServiceWorker の Cache API が保存する単位 となる方向で進んでいます。)

また、例で見てもわかるように Fetch API は Promise を返します。 例えば jQuery の $.ajax は Deferred を返すことで、結果を Promise 化するようになりましたが、 Fetch API は最初から Promise を返します。

モダンと言えばモダンなインタフェースと言えるでしょう。今後 WHATWG で新たに定義される非同期 API はこうして Promise を使ったものになっていくと予想されるので、相性もよさそうです。

Fetch API の位置づけ

"Fetching" という基本的かつ重要な処理が、 API として提供されるということは、開発者にとって非常に重要となる可能性があります。

例えば、なにか新しい仕組みやライブラリを思いつき、その内部で "Fetch" が必要となった場合、 XHR よりもより低レベルな fetch() を用いてその外側に必要な処理をかぶせることで、実装することができます。

これまで、仕様化とブラウザへの実装を待たなくてはいけなかった Web 標準のエリアで、こうした低レベルな API が提供されることは、開発者が自分の速度で、 W3CWHATWG やブラウザベンダの動きとは別に、その仕組みを試して Web を良くしていくことができる可能性が生まれるということです。

これは、仕様策定者である Anne van Kesteren も支持している、 Extensible Web の考え方に通じるところがあります。

先日の High Performance Browser Networking で、 Ilya Grigorik も同様のことを言っていました。

mozaic.fm #11 hpbn 1:00:00 ~ (以下意訳)

ブラウザベンダの実装の足並みは、だいぶ揃うようになったけど、
互換性を保つために時間がかかるものもある。
Fetch や Stream のような Lowlevel API を開発者に提供することは、
何か新しいアイデアが合った場合、
こうしたものを用いてすぐに自分で試すことが出来るようにする目的もある」

「Fetch するとは何か」や「Fetch API」が提供されたことは、 XHR よりもモダンな API が提供されたということ以上に、これから Web の API を進化させて行く上で非常に重要な位置づけであると言えると思います。

そうしたこととは別のコンテキストで、単に AjaxJSON を取るだけと言った場合は、ノウハウも多くこなれた XHR を使っていても、しばらくは問題ないのではないかと個人的には思います。

Stream じゃないだと!!!

although it is currently lacking
when it comes to reporting progression.

なんということでしょう、 Fetch API は Promise "しか" 返しません。 結果のレスポンスは、 Response オブジェクトとして揃った段階で一度だけ resolve されます。 つまり、大きなデータを Fetch しても、その途中の状態をイベントで取ることができません。

XHR ですら、 onreadystatechange などでなんとなくローディングアイコンを出したりできたのに、これは、これではあまりにもあんまりです。

前回の記事でさんざん書いたように、こうした連続するデータを表すためには Stream というインタフェースが最適 であり、これも同じく WHATWG で定義されています。 Stream API を使えば、例えば Fetch している途中の状態を chunk 毎に取り、それを pipeTo() で他の Stream で繋いだりといったことができるはずです。

と思って質問してみたところ、既に別でこの API についてのアイデアはでているようです。 実際、仕様中では内部に Stream を用いるように表現されています。(なぜか Stream API を参照はされていないようですが) よって、 Stream のサポートは、実際にはどうやって Stream を外に出すかを決める作業のようです。

fetch-with-streams

ただし、まだ議論中といった感じのようです。 https://github.com/slightlyoff/ServiceWorker/issues/533

まとめ

  • Fetching (Fetch する) とは何かが定義された
  • Fetching の定義によって、内部で Fetch する全ての API が整理された
  • その Fetch Algorithm を実装した Fetch API が定義された
  • Fetch API の実装は始まっているが、まだ ServiceWorker 内にとどめられている。
  • Fetch API Polyfill は XHR で実装されているけど実際は XHR よりも低レベル
  • 新しいアイデアを思いついたら Fetch API の上に実装することができる
  • 単なる XHR の代わり以上の意味がある。
  • まだ Stream じゃないが、作業中。

謝辞

このブログを書くにあたって、以下の方々にレビューや助言を頂きました。 内容で一杯一杯で後出しになってしまいましたが、 遅ればせながら本当にありがとうございました。

追記