Extensible Web を支える低レベル API 群
Intro
最近 Extensible Web の話がたまに出るようになりましたが、なんというかレイヤの高い概念(ポエム)的な話が多い気がしてます。
もう少し具体的な API とか、「それコード書く上で何が変わるの?」って話があまりないので、今日はそこにフォーカスして、 Extensible Web 的な流れの中で整理された API の話をします。
しかし、実際には API が 「Extensible Web という理念で生まれたかどうか」は自明ではないので、 今標準化されている低レベルな API を拾い、それを整理するというエントリだと思ってもらと良いかもしれません。
あまり知られてない API もあると思うので、これを期に「これがあれば、今までできなかったアレが、標準化や実装を待たなくても、できるようになるな」と思ったら是非書いてみると良いと思います。 実際はそれこそが Extensible Web の目指すところなので。
Extensible Web とは
年末にエントリを書きました。
Extensible Web の夜明けと開発者が得た可能性の話
あと mozaic.fm | #15 Extensible Web でも全部話しています。
簡単に言うと、こんな感じの方針です。
足の遅い標準化や、時間のかかるブラウザ実装を待たずに、 開発者が自分のアイデアをコードで実現して Web を進化させられるように、 必要な機能をモデル化し、そのモデルにアクセスする 低レベルな API を提供するようにしよう。
出典は The Extensible Web Manifesto という 2013 年のマニフェストで、 そこから徐々にこの方針に則ったと思われる API が整備され、だんだん実装まで降りて来始めたというフェーズです。
(先日 マニフェストの翻訳 を本家に取り込んでもらったので合わせてどうぞ。)
TOC
- HTML 拡張系 API (WebComponents)
- Custom Elements
- バイナリ系 API
- ArrayBuffer / ArrayBufferView / DataView
- Blob
- Encoding
- ネットワーク系 API
- オフライン系 API
- Service Workers
- Caches
- URL 系 API
- URLSearchParams
- FormData
- 非同期処理系 API
- Promise
- Streams
- 低レベル API との向き合い方
リンク貼れたらなぁ。
HTML 拡張系 API (WebComponents)
例えば HTML 自体に新しいタグを追加するとなれば、 W3C/WHATWG で標準化する必要がありました。 しかし開発者がコードで新しく独自のタグを定義し、その挙動を JS で実装できるようになりました。
これが WebComponents と呼ばれる API 郡です。
Custom Elements
タグ名に - をつける必要がありますが、新しいエレメント(タグ)を定義することができます。
また、新しいタグを定義しなかったとしても、既存のタグを拡張して、そのタグに対して機能を追加することができます。
今までは、独自のタグを定義する標準的な方法がなかったため、 div / span などの汎用タグに class 属性などを付けたうえで拡張する挙動などを定義してきました。
しかし、 CustomElement を用いると、任意の挙動を実装したオリジナルのエレメントを定義することができます。 これを用いると、完全に独自なエレメントを、標準化やブラウザの実装を待たずに定義することができるのです。
var xFoo = document.registerElement('x-foo'); // xFoo に挙動を定義 // オリジナルのエレメントを作成 // <x-foo></x-foo>
しかし、HTML の既存のタグは既にかなりのノウハウと実績を持っているため、それらを無視した完全にオリジナルなエレメントを作成しても、それが従来の Web と自然な形で馴染むところまで作り込む事は実際には難しいです。
そこで、ブラウザが標準で実装しているものをベースとし、拡張機能を定義することもできます。 この方法は、もし CustomElements にブラウザが対応していないとしても、通常のタグの動作にフォールバックできるというメリットもあります。
document.registerElement('x-form', { extends: 'form', prototype: Object.create(HTMLFormElement.prototype) }); // 既存のエレメントを拡張 // <form is="x-form"></div>
タグを定義可能な API であることを利用し、「HTML の標準エレメントを Custom Element で再実装する」という実験プロジェクトもあります。
https://github.com/domenic/html-as-custom-elements
これにより、以下を調べる取り組みのようです。
つまり、 Custom Elements は、 Web が持つ HTML エレメントという低レベルな要素を、 API を用いて再構築可能なまでに低レベルな API を目指しています。そうして開発者から出てきた新しいエレメントが有用であれば、標準化のプロセスを経てブラウザに実装されるかもしれません。
ただし、エレメントを実装するには、既存の DOM ツリーに依存しないスタイルの適用や、その定義を読み込む機能が欲しくなります。 こうした足りてない部分を補うために、以下の 3 つの API が新たに提供され、一般的にはそれをまとめて WebComponents と呼んでいます。
http://w3c.github.io/webcomponents/spec/custom/
バイナリ系 API
JS は長らく、バイナリデータを扱う標準 API がありませんでした。 特に WebGL の導入に伴い、こうした API の需要が強くあったという経緯もあるようなので、 Extensible Web よりも少し前な気もしますが、重要な低レベル API であり、他の低レベル API もここに依存するものが多いので、あらためて整理しておきます。
ArrayBuffer / ArrayBufferView / DataView
ArrayBuffer はバイナリデータが詰め込まれた、読み取り専用のバイト配列です。 バイナリデータを返す API などは、この ArrayBuffer の形式で返すものがあります。
実際に ArrayBuffer からバイナリデータを取り出すのが ArrayBufferView です。 もし JavaScript に uint8 や int32 型などの、サイズ固定な数値型が定義できれば、その型で宣言した変数にデータを取り出すといったことができますが、 JavaScript には number 型しかないためその方針はとれません。
そこで、 ArrayBuffer に詰め込まれたバイナリデータを uint8 や int32 といった Chunk ごとに区切られた配列とみなし、 その配列からデータを取り出した結果が求めるサイズの number になっているという方針をとります。 この任意のサイズに ArrayBuffer を区切るためにかぶさる View が、 Uint8Array や Int32Array といった ArrayBufferView になります。
Uint32Array: [ 4035938432] Uint16Array: [ 34944, 61583] Uint8Array: [ 128, 136, 143, 240] ArrayBuffer: [ 1000000, 10001000, 10001111, 11110000]
しかし、 ArrayBufferView は固定長に区切られたデータを扱うには向いていますが、たとえばネットワークやファイルのような、ヘッダにある様々なのサイズの情報を読み込むような場合には、そのたびに View の型も変える必要がでてしまいます。
そこで、こうしたタイプのデータは DataView を用います。 DataView は getUint8(), getInt32() といった具合にデータを指定サイズで都度取り出せるため、パケットの解析などに適しています。もちろん setter を使えば、データを送る/書く側(サーバなど)に使うことができます。
ArrayBuffer: [ 1000000, 10001000, 10001111, 11110000] getUint8(0): 128 getUint16(0): 32940 getUint32(0): 2156433392
この ArrayBuffer, ArrayBufferView, DataView を合わせて、一般に TypedArrays と呼ばれています。 後述する多くの API がバイナリを何らかの形で扱っており、その場合この TypedArrays を用いることになるため、非常に重要な低レベル API と言うことができます。
https://www.khronos.org/registry/typedarray/specs/latest/
Blob
ブラウザ上のバイナリデータの固まりを扱うオブジェクトです。 文字列や ArrayBuffer, ArrayBufferView などをもとに生成します。
バイナリデータを「ブラウザという世界」に持ってくる際にに経由する、地味だけど非常に重要な API です。
特徴は URL.createObjectURL() に渡すと URL が生成できることです。 これは img, video などの src の値や、 xhr など URL を扱う API にそのまま渡せるということです。 データの基本であるバイナリオブジェクトをブラウザが基本要素として扱う URL に変換できる事によって、全てのデータがブラウザと繋がることになります。
例えば File オブジェクトも Blob の拡張として成り立っており、 URL に変換してして <a> の href に繋ぐことでダウンロードできます。これは <a> の href の扱いが既にブラウザ上で規定されており、そこに任意のバイナリデータを URL を介して接続できている事を意味します。
var blob = new Blob(arrayBuffer, { 'type' : 'audio/mp3' }); // バイナリ var url = URL.createObjectURL(blob); // URL document.querySelector('a').href = url; // ファイルがダウンロードされる
自分で何かバイナリを扱う API を考える場合に、基礎として使うことができます。
https://developer.mozilla.org/en-US/docs/Web/API/Blob.Blob
Encoding
今まで String.prototype.charCodeAt() や String.fromCharCode()、 ES6 では String.fromCodePoint() などで、文字を Unicode の code point と相互変換することができましたが、任意のエンコードとの変換はバイナリ操作によって自分で行う必要がありました。
Encoding API を用いると、文字列を「文字コードに応じた」バイナリデータと相互変換できます。 インタフェースは、内部のエンコーディング形式をそれぞれに持つ事ができるように TextEncoder/TextDecoder に分かれています。
TextEncoder は 'utf-8', 'utf-16be', 'utf-16le' のみに対応しており、 encode() で String をその文字コードの Uint8Array に変換します。
var utf8 = new TextEncoder('utf-8'); utf8.encode('あ'); // [227, 129, 130]
エンコードする文字コードが限定されているのは、 Web において基本的にこれらの文字コードしか使わないようにさせるためです。 Web の世界ではもう utf-8 以外を使うことは、極力避けて行くべきという旨が仕様にも書かれています(https://encoding.spec.whatwg.org/#preface)。
TextDecoder はかなり多くの文字コードを指定することができ、 decode() で Uint8Array を String に変換します。 (どのくらい多いかは、 https://encoding.spec.whatwg.org/encodings.json を見るとわかります。レガシーな文字コードもきちんとカバーされてて本当にすごい。)
例えば shift-jis だと以下のような感じです。
var shiftjis = new TextDecoder('shift-jis') shiftjis.decode(new Uint8Array([130, 160])) // 'あ'
デコードでは多くの形式が対応しているのは、例えば任意のテキストファイルを File API で読み込んで、その内容を表示するといった場合に、多くのエンコーディングに対応する必要があったからかと思います。 utf-8/16 以外の文字コードの文字は Blob として取得し、 utf-8 の文字に直してから別の処理を行うといった形になっていくと思います。
また、 utf-8 値が取得できるため、非推奨な escape() の変わりに UTF8 パーセントエンコーディングなども可能になります(エンコードして toString(16).toUpperCase() して '%' をつけるだけ)。
https://encoding.spec.whatwg.org
ネットワーク系 API
Fetch
ブラウザから発生するネットワークアクセスは基本的には HTTP です。 しかし、単純な HTTP/1.1 GET リクエストは、 telnet などで発行すれば 3 行程度で済みますが、 ブラウザはブラウザ特有の様々な挙動が追加されるため、一見単純な GET でも複雑なリクエストになります。
例えば以下のようなものです。
- UA が自動で付与される
- リファラが付与される
- Cookie が付与される
- ブラウザがキャッシュを管理し、付随するヘッダ処理をする
- CORS の制限が適用される
- 必要に応じて自動で preflight が発生する
- etc
こうしたブラウザ特有の挙動を加味して発行された HTTP リクエストで、ブラウザはサーバからリソースを取得します。この行為を Fetching と呼んでいましたが、その行為は XHR や CORS によってどんどん複雑になってきました。
Fetch の仕様は 「ブラウザが Fetch するとはどういうことか?」という概念をきちんと整理して定義しており、その最小限の API として実装されたのが fetch() API です。
fetch() は XHR と違い Promise を返す API であることから、単にモダンな感じにしただけかと思われがちですが、 fetch() 導入に伴って以下が変わっています。
- Request, Response, Header などのクラスが定義された
fetch()自体はオブジェクトではなく単なる関数- chache や origin や credential など細かな制御が引数で可能になった
- Promise を返す
したがって、ブラウザがデフォルトで発行するリクエストと同等なものを用いたネットワークアクセスが必要なライブラリなどを書く場合は、継ぎ足しで作られた XHR の古めかしいインタフェースにとらわれる事無く実装ができます。
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));
注意点としては、今はまだ abort() が無く、 Promise を返すので onprogress 相当がありません。
これは今後 CancelablePromise や Stream などの議論とともに進んでいく予定のようです。それが実装できると、 XHR は fetch を使って完全に再現できる筈です。
TCP and UDP Socket API
WebSocket は「Web 上での双方向通信」を目的としており純粋な TCP ではないし、 WebRTC は「Web 上での P2P」を目的としており純粋な UDP ではありません。 fetch() は「ブラウザからの」 HTTP リクエストとして一番低レベルでした。
そこで、もういっそ TCP/UDP のソケット API がそのままブラウザから使えるようにしてしまえ、という感じのコンセプトです。 Socket API は TCP/UDP への write() はもちろん、 listen() もできます。つまりサーバが立ててしまう可能性があるのです。
// write var client = new TCPSocket('http://example.com', 80); client.writeable.write('ping').then(() => { client.readable.wait().then(() => { console.log(client.readable.read()); }); }); // listen var server = new TCPServerSocket({'localPort': 300}); server.listen().then((connection) => { connection.readable.wait().then(() => { console.log (connection.readable.read()); }); });
例えば MQTT などを WebSocket 介さず実装したり、現在議論中の ORTC のようなものを PoC 実装したりする際に使えるだけでなく、 ブラウザを積んだデバイスが、 ServiceWorker 内で TCP サーバ立てたり、 DNS の名前解決するなんてことができてしまうかもしれません。
ブラウザのレベルまでこの API を持ってくる事は、セキュリティのサンドボックス化が非常に気になると思います。確かに従来 OS が扱っていたレベルまで下がっている Socket API の実装ではそこが非常に重要になります。 そこで Extensible Web の中には、「それこそが標準化がリソースを裂くべきところだ」と明記されています。高レベル API の策定ではなくセキュリティ的に安全な低レベル API の提供に注力するべきという方針です。
一方で、例えば raw socket を扱うコードが JS で完結すると、今まで拡張やプラグインで無茶したコードのセキュリティホールによって発生していた、ネイティブ層まで突き抜けるような脆弱性が、減少する可能性も考えられます。
http://www.w3.org/TR/raw-sockets/
オフライン系 API
オフライン化するために必要な機能をモデル化すると以下の三つに大別することができました。
- 通常の JS のコンテキストとは、別のコンテキスト(スレッドと言っても概ね良い)で JS を動かす環境 (service worker)
- キャッシュを保持する機能 (cache)
- ブラウザのリクエストを再現する機能 (fetch)
これらを合わせて 「オフライン Web を実現する API」だとされている感がありますが、それは使い方の凡例の一つです。 各々は別にチュートリアルに書かれてる通り、オフライン化するために「しか」使えないわけではありません。
Service Workers
ブラウザの JS とは別のコンテキストで動作する(別のスレッドが立っているイメージ)環境を提供します。 それだけなら WebWorker と同じですが、 SW はブラウザとネットワークの間に挟まる Proxy のように動作します。
例えば、ブラウザ上で発生した HTTP リクエスト(つまり Fetch) をイベントで検知することができたり、 外から Push API で飛んで来たメッセージをイベントで受け取ることができます。
ブラウザとは独立したコンテキストで、ブラウザの裏でメタな処理が可能な環境を提供するのが、 SW の本質です。 この事をふまえて、よく言われている「オフラインアプリ」や「プロキシ」として使う事もできるというだけであって、そう使わないといけないというものではありません。
// 登録した SW 内で this.addEventListener('fetch', (e) => { // ブラウザで発生した fetch (リンククリックや XHR) を取得 var req = e.request; // req に細工 req.header.set('x-foo', 'bar'); // 細工したリクエストを fetch して response 返す e.respondWith(fetch(req).then((res) => res)); });
このように fetch() が介入できるのは、 fetch の仕様で紹介した Request/Response/Header クラスによって、ブラウザで発生した Request オブジェクトをいじったり、それを用いて fetch() して得た Response をブラウザに返したりというインタフェースが整ったからです。
https://slightlyoff.github.io/ServiceWorker/spec/service_worker/
Caches
外部から取得したリソースのキャッシュを管理するためのオブジェクトです。 この API は純粋に、 Request にひもづいた Response を保存できます。
したがって、ブラウザが発行した Request に、変わりに fetch した Response を保存するといったことが可能になるのです。 Request をキーにしたオブジェクトのように扱えるため、 API の粒度が細かくプログラマブルであり、 Application Cache API とは違い、一部のリソースだけキャッシュを更新すると言った事が柔軟に可能です。
SW + Cache が Application Cache API に変わるオフラインアプリの作成で注目されているのはこの部分です。
// request に紐づいたキャッシュを返す self.addEventListener('fetch', (e) => { event.respondWith(caches.match(e.request) .then((response) => response)); });
しかし、オフライン化のためにしか使ってはいけない訳ではなく、例えばオンライン状態でも純粋なキャッシュとして利用して、パフォーマンスの向上などを行う事もできます。(その場合は本来は cache-control ヘッダを使うべきですが)
https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-objects
URL 系API
Web の基本かつ重要要素である URL を、 JS から手軽に扱うことができます。なんで無かったのかというレベルです。 実は難しい URL のパース/シリアライズが可能で、パーセントエンコーディング、 punycode、 IPv6 や Base Path などなどを、 new するだけでまるっとやってくれる API です。(今までは <a>タグを使って無理矢理 やるか、自分でパースするしかありませんでした。)
var url = new URL('http://user:name@www.ドメイン.com:8080/login?foo=bar#hash') url.hash; // "#hash" url.host; // "www.xn--eckwd4c7c.com:8080" url.hostname; // "www.xn--eckwd4c7c.com" url.href; // "http://user:name@www.xn--eckwd4c7c.com:8080/login?foo=bar#hash" url.origin; // "http://www.xn--eckwd4c7c.com:8080" url.password; // "name" url.pathname; // "/login" url.port; // "8080" url.protocol; // "http:" url.search; // "?foo=bar" url.username; // "user" URL.domainToAscii('ドメイン名.com'); // 'xn--n8jwd4c7c.com' URL.domainToUnicode('xn--n8jwd4c7c.com'); // 'ドメイン名.com'
domainToAscii() と domainToUnicode() については、執筆時点で実装しているブラウザはないようなので、サンプルです。
パーセントエンコードなどがきちんと考慮されるのは、 EncodingAPI によって utf-8 がきちんと扱えるようになったためと言えます。
注意点として、標準の DOM API はこの URL オブジェクトを内部で使えど、外に公開するためには用いないということです。
外に公開する場合は、従来通り URL を文字列として公開すべきという旨が書かれています。(https://url.spec.whatwg.org/#url-apis-elsewhere)
実際 fetch() などは内部で URL オブジェクトを使っていますが、 Request クラスが公開しているのは文字列型の url プロパティです。(https://fetch.spec.whatwg.org/#request-class)
片手間な正規表現で処理していたスクリプトは、すぐにでもこのクラスで置き換える方がいいでしょう。 また、新しく書くライブラリも、 URL 文字列を受け取るなら、まず最初にこれでパースし、公開時に toString() するくらいが良いと思います。
URLSearchParams
URL のパラメータ部分を扱うためのオブジェクトです。
また、 Form を POST するときなどに使う form-urlencoded 形式の表現を得ることができます。
非常にシンプルな API ですが、シリアライズ時には適切にエンコードされた文字列が得られます。
var params = new URLSearchParams('a=b&c=d&a=x'); params.has('a') // true; params.get('a') // 'b'; params.getAll('a'); // ['a', 'x'] params.delete('a'); params.append('あ', 'い'); params.toString(); // 'c=d&%E3%81%82=%E3%81%84'
もし適当に & や = で split(), join(), encodeURIComponent() していた処理があったとすれば、この実装で置き換える事でブラウザと完全に互換な実装にできます。
自分で実装するライブラリがパラメータを扱う場合は統一した API としてこれを使う事が考えられます。
現時点では XHR でこのオブジェクトを直接 form-urlencoded として送るなどのことはできません。
それだとちょっと微妙なので、提案しました。今 議論中 です。
https://url.spec.whatwg.org/#interface-urlsearchparams
FormData
その名の通り Form の Data を扱うオブジェクトです。 URLSearchParams と似ていますが、こちらは DOM 上の Form から直接生成する事もでき、そのまま XHR で送信も可能です。
また DOM の Form が <input type="file"> を許容するように、
FormData にも File オブジェクトを含む事ができます。
なので、そのまま XHR で send() すれば手軽に File アップロードが可能です。
var form = document.getELemeneById('#login_form'); var formData = new FormData(form); xhr.send(formData);
気をつけないといけないのは、 FormData 経由で送ると必ず multipart/form-data 形式になるため、たとえ文字列しかなくても x-www-form-urlencoded で送りたければ URLSearchParams に詰め替える必要があります。(content-type を指定しても無視される)
先ほどの提案した内容は、これと同じように Form から URLSearchParams を生成できるようにする方向で動いているので、それが実装されたら以下のようになるでしょう。
- URLSearchParams: テキストのみ
x-www-formurlencoded用 - FormData: Blob 込み
multipart/form-data用
https://xhr.spec.whatwg.org/#interface-formdata
非同期処理系 API
WHATWG で策定されている新しい API で、非同期を扱うものは基本的には Promise/Stream ベースで設計されています。
Promise
Promise 自体は DOM から始まった仕様ですが、 ECMA に移され ES6 の API となりました。 これまでは onxxxxxx という関数にコールバックを登録したり、 addEventListener を用いる API が主流でしたが、単発の非同期処理は Promise を返す API に統一されつつあります。
例えば前述の fetch や service worker などはすでにそうなっています。
Promise の仕様が ES のものであるメリットはでかく、 node/io と browser で完全に互換な API で使えます。
http://people.mozilla.org/~jorendorff/es6-draft.html#sec-promise-objects
Streams
node/io でおなじみの Stream と同じようなイメージです。
特にイベントが連続的に発生する API では、途中の chunk データを emit するために、 Promise の代わりに stream を返す API に統一されていくようです。
node.js とは、例えば pipe() ではなく pipeTo() であったり、引数の API が違ったりしますが、基本的な考え方は近いので、どちらも Stream ベースで書くことができるでしょう。
https://streams.spec.whatwg.org/
低レベル API との向き合い方
HTML5 の一連の新規 API 系の話は、割と使い方とセットで提供されていたかもしれませんが、 ここに上げたような API は、どのような使い方をしても構いません。よくあるチュートリアルにある使い方に限定する必要はありません。
SeviceWorker はオフラインだけのためじゃないし、 WebComponents は 4 つの API セットで使わないと行けない訳ではないです。
そして、何かをするためには低レベルすぎて使いにくい・煩雑だと思うかもしれませんが、それは当然です。 あえて低レベルとして提供されているものなので、その上で高レベル API としてのフレームワークやライブラリ(例えば Polymer のような) を作り、使うのが良いでしょう。今はまだ揃ってはいないので、逆に自分が考える最強のライブラリを作るべき段階と言えます。
そして、そうやって低レベル API の上に作られた高レベル API が良いものであれば、 jQuery が querySelector() に繋がったように標準化がそれを取り込んで、やがてブラウザに実装されて、ライブラリ無しで使えるようになる可能性があります。
要するに、標準化やブラウザベンダのことなど何も気にせずコードを書けば良く、それが Extensible Web の目指すところです。
一番の問題は、こうした低レベル API 自体は、標準化してブラウザが実装しないといけません。 だから、理想のサイクルが回るにはもう少し時間がかかります。
その辺が最近自分が取り組んでいる事なんですが、長くなったのでその話はまたいずれ。
ServiceWorker を使った XHR のモックテスト
Intro
似た話は既出なのでご提案にはならないけど、PoC として一応ライブラリっぽくしてみた。
タイトルの通り XHR のリクエストに対して、任意のレスポンスを返すことによって、 再現の面倒なエラー処理などのテストを、 Local Proxy 無しで実現する方法のメモ。
というか、これ自体がブラウザ上の JS だけで完結する Local Proxy と言えるかもしれません。
ざざっと書いただけなので、API というか使い方もまだ適当というかどうしようか考えてるところです。 今これが使えるブラウザ自体がそんなにないので、 SW の実装が普及するまでにもう少しまともにしておきます。
Jxck/response-injection · GitHub
色々踏んだので、 Service Worker 使う際の参考になればと。
モチベーション
Service Worker(SW) には色々な機能があり、それ故に色々な使い方が考えられます。
代表的なのはオフラインキャッシュですが、それは一部であり、本質的には表示されているページとは別のスレッドに常駐する JS を登録し、発生するイベントをフックしてメタ的な処理を挟み込みための API だととらえています。
SW がフックできるイベントに 'fetch' というものがあり、これはブラウザ上でリクエストが発生したことを通知し、そのリクエストを盗むことができます。 オフラインアプリの場合は、このイベントのフックで、 Cache API に保存してあるレスポンスを返すなどの使い方をしますが、 今回はテストに都合のよい任意のレスポンスを生成して、変わりに返すことで、実際のネットワークアクセスをせずに、 XHR (もしくは fetch API) を用いたライブラリなどのテストをするものです。
任意のレスポンスが返せるということは、エラー処理のテストなどに便利で、 LocalProxy を使わずブラウザで完結できます。
仕組み
例えばこんな雑な API のコードを書いたとする。
function get(url) { return new Promise(function(done, fail) { var xhr = new XMLHttpRequest(); xhr.open('GET', url); xhr.responseType = 'json'; xhr.addEventListener('load', function() { if(xhr.status >= 400) { return fail(xhr.response); } done(xhr.response); }); xhr.addEventListener('error', fail); xhr.send(); }); }
このテストをこんな感じで書きたい。
Injector( // URL とレスポンスの対応を書く { '/success': { head: { status: 200, statusText: 'OK', headers: { 'Content-Type': 'application/json' } }, body: { id: 200, name: 'jxck', mail: 'jxck@example.com' } }, '/fail': { head: { status: 404, statusText: 'Not Found', headers: { 'Content-Type': 'application/json' } }, body: { message: 'user not found' } } }, // option // 除外したい URL と、 SW のスコープを書く { ignore: ['/', '/test.js', '/main.js'], // ignore injection (default []) scope: '.' // scope for register worker (default '/') } ).then( // ここまでにレスポンスの登録は終わってる // 普通にテストを書く function test() { console.log('start test'); get('/success').then(function(res) { console.assert(res.id, 200); // 差し込んだ結果が返ってくる }).catch(function(err) { console.assert(false); }); get('/fail').then(function(res) { console.assert(false); }).catch(function(err) { console.assert(err.message, 'user not found'); }); // etc } ).catch(console.error.bind(console));
SW が有効になったブラウザで動かすと、最初のリクエストで SW が登録されるので、 一度リロードすればレスポンスが差し込まれます。
また、 SW の登録は最初の一回だけでよく、既に登録されていれば、上部のレスポンスのモックデータを書き換えてリロードするだけで、動的にモックデータを更新できます。 毎回、登録しないのでテストの更新がすぐ反映されてストリレスが少なく、テスト自体は邪魔してないので、 Injector の Promise が解決されたあとにいつも通りのテストが走らせられれば、 連携するフレームワークなどは選ばないので、取り入れやすいはず。
デモはこんな感じ。
$ git clone ttps://github.com/Jxck/response-injection $ cd response-injection/sample $ # python とかでサーバを立てる
http://localhost:3000/ とかを開いてリロードするとテストが走る。
本当はもっとキーを柔軟にしないと実用に耐えないと思うので、 差し込むデータの形式は最終的に Request/Response オブジェクトの対応まで拡張してしまえばいいかと思ってる。 全く同種のリクエストの分岐は、カスタムヘッダの付与とかでいいかと。
何をしているか
まだ書き始めたばかりなので、コードは短いです。かいつまんで解説します。
全体
1 ファイルで登録する側、される側を両方兼ねています。 分岐は以下のような感じになります。
if ('ServiceWorkerGlobalScope' in self && self instanceof ServiceWorkerGlobalScope) { // service worker のコード } else { // Injector のコード(window 側) }
二つにわけて説明します。
Injector 側
データを渡すと SW に登録してくれて、登録が終わったら Resolve する Promise を返します。
function Injector(mockdata, option) { console.log(mockdata, option); option = option || {}; option.scope = option.scope || '.'; option.ignore = option.ignore || []; // 登録されていれば再利用、無ければ自身を登録。 return Promise.race([ navigator.serviceWorker.register('injector.js', { scope: option.scope }), navigator.serviceWorker.getRegistration() ]).then(function() { // これでメッセージをやりとりする。 var msgchan = new MessageChannel(); // メッセージは文字列のみ var message = JSON.stringify({ mockdata: mockdata, option: option }); // activate 前は controller が null になるけど reject すべきか。。 navigator.serviceWorker.controller.postMessage(message, [msgchan.port2]); return new Promise(function(done, fail) { // 登録が終わった通知を受け取る。 // addEventListener だと動かない msgchan.port1.onmessage = function(event) { if(event.data === 'done') { // 終わったら解決 done(); } else { fail(new Error('event.data')); } }; }); }) }
SW のコードは更新しなくはないので、一度だけ登録し、登録されていたら再利用します。 SW へデターを渡すには DOM を経由できないので、 MessageChannel API を使います。 SW は登録が終わったら応答させるようにして、そこで Promise を resolve させます。
なので、これが終わったらテストを実行すれば、レスポンスが挟めます。
navigator.serviceWorker.controller は activate されるまで null なんですが、どうせ二回リロードしないと動かない前提と放置してるけど、実際は resolve なり reject すべきか。
ちなみに、 port の message へのリスナが addEventListener で登録できなかったぽいのは報告中。
Worker 側
// window 側からのメッセージを受け取る self.addEventListener('message', function(message) { var data = JSON.parse(message.data); var mockdata = data.mockdata; var option = data.option; // レスポンスを生成 function createResponse(path) { var head = mockdata[path].head; var body = JSON.stringify(mockdata[path].body); console.log(head, body); return new Response(body, head); } // fetch イベントをフック // リスナは参照もってなくて良いように毎回上書き self.onfetch = function(event) { // とりあえずパスだけでマッチさせてる var path = (new URL(event.request.url)).pathname; // test.js やらへのリクエストを省くため if(option.ignore.indexOf(path) >= 0) { // ignore return; } // 作ったレスポンスを返す var response = createResponse(path); event.respondWith(response); }; // fetch リスナの登録が終わった事を伝える。 message.ports[0].postMessage('done'); });
登録した Worker 内では message イベントにリスナを貼っておきます。 これで window 側でリロードしたとき毎回イベントで必要なデータを受け取れます。
レスポンスは、 Response コンストラクタで生成できます。 実際の定義はこんな感じ。
typedef (Blob or BufferSource or FormData or URLSearchParams or USVString) BodyInit;
typedef (Headers or sequence<sequence<ByteString>> or OpenEndedDictionary<ByteString>) HeadersInit;
dictionary ResponseInit {
unsigned short status = 200;
ByteString statusText = "OK";
HeadersInit headers;
};
Response(optional BodyInit body, optional ResponseInit init),
今回は JSON にしてますが、 FormData や URLSearchParams を渡せるようにしたり、 そもそも Response の配列を渡せるようにしたい。
statusText は定義通り、省略すると 'OK' が返ってきますが、 Reason Phrase に依存してなければ無視して良いです。
HeadersInit は、 key-value で渡しておけば大丈夫です。
onfetch のリスナは、リロードするたびに上書きしてしまいたいので、こちらはあえて addEventListener を使わないで書いてます。
登録が終わったら、テストを走らせて良い事を window 側にまたメッセージを投げて伝えます。
memo
- 登録
- worker のコードを 1 byte でも変更すると再登録される
- 最初のリロードで取得され、この段階では古いコードが動く
- 二回目のリロードで、新しいコードが動く
- worker が登録できなかった時のエラーは慣れないと分かりづらい。
- 自分自身を register して、単一ファイル内に window 側と SW 側の処理を分岐で突っ込む事もできる。
- スクリプト内では self instanceof ServiceWorkerGlobalScope で分岐できる。
- inspector
- chrome://serviceworker-internals/ を見る
- 登録して動いている状態で inspect ボタンを押すと inspector が開く
- Open the DevTools にチェックしておくと勝手に inspector が開く
- SW 内の console.log はこっちのログに出て、 window 側には出ない
- SW 内での console.log は毎回 messaging で window に送って表示した方が見やすいかもしれない。
- inspector が残ってる状態で登録を繰り返すと、なんか inspector の画面が大量に発生してる場合がある?
- ライフサイクル
- SW のライフサイクルと、その時点での内部のデータやリスナの状態に注意しないと、更新した後??になる。
- 生きてるリスナは何か、クロージャに登録されてるのは何か
- 分からなくなったら inspector を全部閉じて、 unregister すれば消える。
- さらにブラウザ再起動すれば worker の履歴も消える。
- worker の履歴、 UI で消せるようにして欲しい。
- XHR 以外にも使える筈
- a タグとかもできるはずだけど試してない。
- 将来 fetch が window に来ても同様にできるはず。
- プラクティス
- とにかく Promise 返しておけば間違いない
- scope とディレクトリ構造のベストプラクティスが欲しい
とにかくコードの更新がうまく反映されてない時に一番ハマるので、慣れるまでは正確にコードが更新された事を確認するのが、当たり前だけど大事でした。
thanks
@hiroki_ さん、@kinu さん、@hirano_y_aa さんがた中の方々に色々ご助言頂きました、ありがとうございました!
Extensible Web の夜明けと開発者が得た可能性の話
Web based on Standards
Web は誰のものでもありません。
だれかプロダクトオーナーがいてその人が意思決定するとか、そういうのとは真逆の成り立ちをしています。
標準的な仕様を決めて、その仕様に則って Web の世界は成り立っている。
政府が作るサイトも、 Twitter も、学生が作ったブログも、全部同じルールで作られている。だから繋がる。
これって結構凄いことだと、自分は思っています。
Standarization
このルールの決め方にもルールがあって、ちょっと敷居は高いかもしれないけど、誰でも自由に参加して、自由に意見を述べることができる場があります。
標準化団体ってやつですね。
なんか一部の人たちが勝手にやっているように思えるかもしれないけど、それは選挙に行かない人の理論と同じです。
あなたが仕様について意見を持ってて、それが妥当であるならば、その発言は仕様を根本から変えることもできます。
その点で、選挙より結果との距離が近いかもしれないですね。
自分が標準化に参加していなくても、そのプロセスで行われた議論はオープンなので、追うことができます。
正直わかりにくい所もあるし、賛否があるのも分かる。時に政治色もある。 色々問題もあるかもしれないけど、これは今の Web を支える重要な仕組みなのは確かです。
Web の進化の速度
昔ブラウザベンダは、製品の差別化のために我先にと独自の仕様をブラウザに取り入れたました、 marquee タグとかああいうのですね。
でもそれだと他と繋がらなかった。
Web の利用者がどんどん増えて、その間で繋がることの価値が上回ったことで、どんどん消えて行った。
一方で、多くの人にとって価値のある仕様(XHR とか)は標準化のプロセスに乗せられた。
HTML5 関連の盛り上がりは、それが加速した結果だったと思います。
問題は、ステークホルダが増えたときの合議制にはやっぱり限界があったということ。 これが原因で標準化を嫌う人は結構いるんじゃないかな。
そして、仕様ができても、ブラウザの実装はすぐに行き渡る訳ではない現実がある。
ブラウザに実装されないと、俺らにとっては無いのも同じで。
考慮すべきブラウザがせいぜい両手で数えられるくらいなのが本当に不幸中の幸いだと思います。 もっと多かったらどうなっていたか。
本来、仕様を研ぎすますためには、実際にそれを使った開発者からのフィードバックが不可欠なはずなのに、 開発者の手元に API がやってくる頃には、時代のニーズは様変わりしているという事態も、現実に起こり始めていて。
その、わかりやすい例が Application Cache だったりするわけです。 標準化と、ステークホルダと、ブラウザの実装と、開発者のフィードバックと、時代の速度の微妙なずれの賜物。
Extensible Web Manifesto
結局、仕様の策定やブラウザの実装を待ってられないというか、そこに時間がかかりすぎることのフラストレーションって絶対あるんですよね。
仕様を策定してる横で、リアルワールドのユースケースはどんどん変化するし、それらに後追いで対応すると仕様は複雑になり、 いずれは誰の目にも「壊れたもの」にしか映らなくなる。 Application Cache の失敗 はつまりそういうことだったんだと思います。
オフライン対応を誰よりも待ち望んでいた 大手 SNS サービスあたりは「これはマズい」って気づいてい なんとかしようとした んだけど、根本的にはやっぱり難しく。
開発者が見向きしなくなった結果、どのブラウザもその仕様を実装しなくなって、きっとこのまま Web の歴史の闇に消えて行くだろうと思います。その方がいい。
最初の意図からどんどん外れて、継ぎ足し継ぎ足しで膨らんで来た XHR も、結構ギリギリな感じに見えますね。
これって、何がいけなかったのか、もしくは、本当に必要なものって何だったのか?
オフライン対応は絶対必要だけど、そのユースケースは仕様策定時に想定できる範囲を超えてどんどん広がっていってしまう。
「こういうことしたい」と現実の問題を解決して、その時に必要な API のどこまでをブラウザがやるべきか。
そのフィードバックこそが Web の健全な発展を後押しして、標準化のオーバーヘッドを最小限に減らすことができるはずなのに、そうなってるとは言い切れなかった。仕様策定側が「こういうことやりたいんだろ?こう使えよ」ってなってた。
実際は、細かく低レベルな機能を提供して、開発者がそれらを組み合わせながら自分たちのアイデアを素早く形にし、現実の問題を解決できるようにすることが必要なんだよね。
だから、そういうふうに
「開発者には、ユースケースじゃなくて、可能性とその道具を提供しよう」
そして
「開発者が仕様やブラウザの実装を待たずに Web を拡張していけるようにしよう」
と考えた人たちが出てきた。
それが、 Extebsible Web Manifesto の言いたかったことだと思ってます。
Extend the Web Forward
Extensible Web Manifest 自体は 2013 年の中盤くらいに出た話ですね。
書いたのは、Ember.js その他沢山の開発者である Yehuda Katz や、 Service Workers その他沢山のの仕様を策定している Alex Russell、Fetch, Stream, Cache その他たくさんの仕様を策定している Anne van Kesteren など WHATWG で活動する人たちです。
イメージでは、仕様も書いてるけどコードも書けるし、「W3C になんか任せてられるか!」って思ってそうな人たち。
サイトに行くと賛同する主立った人たちの名前が載ってます。もし賛同するならリンクから自分の名前を登録することもできます(意思表明だけで、別に何が起こる訳じゃないけど)。
で、具体的に何が変わるのか?
その辺の話は、特に中心になって呼びかけてる Yehuda Katz のブログ あたりにも書かれています。
I talk more about the reasons behind the Extensible Web Manifesto at https://t.co/mwKpMadPAy. #extendthewebforward
— Yehuda Katz (@wycats) 2013, 6月 10
低レベルな API セットと可能性
実は、この考え方は早速 Application Cache の後継として話題沸騰中の Service Worker で実践されています。
Service Worker は本来 Offline 対応に必要な API を考えた結果、それらを細かい個々の仕様に分けて、組み合わせて使うようになっています。
まず、 Shared Worker のように別スレッドで処理を実行できる環境に加え、それを適切にアップデートする仕組みをきちんと整備しました(AppCache の反省が見えますね)。
また、キャッシュの管理もブラウザ任せにしないため、開発者側が完全にコントロール可能にできるよう、 Cache API を作りました。 JS の API です。
Cache したコンテンツをユーザに提供する仕組みですが、ここが面白くて、別のスレッドから「HTTP リクエストに介入する」という方式をとっています。これが Service Worker の本懐。
この Network Interapt の仕様は、オフラインの実現には使えるけど、オフラインの実現にしか使えない訳じゃないですよね。
そこで Cahce を差し込んで返すのは One of Them です。 たとえば、ブラウザが作るのに任せきりにしていたリクエストを奪って、独自にヘッダを足したリクエストを代わりに投げたりすれば、サーバとより柔軟なネゴシエーションができるはずです。
すると、ブラウザが発行するリクエストが再現できて、かつプログラムから必要十分なコントロールを可能にしたリクエスト API が欲しくなる訳です。 それが、前回解説した Fetch API なんですね。だからそもそも 「Fetch するってなにか?」もちゃんと整理されたと。
github の fetch polyfill が XHR で実装されてて勘違いしている人がいるようですが、実際は Fetch API の方ができること多いです(逆に XHR は Fetch API で作れます)。ただのモダンなインタフェースラッパーじゃないです。ただ polyfill するには XHR しか今無いってだけで。
もう一つ、派手さが無いので紹介する場があまり無いけれど、 HTTP 投げるなら URL の扱いは必須な訳です。でも実は長いこと まともな API って無かった んですよね。 これも合わせて定義され、 URL API ができました。 Fetch 同様「URL とはどういうものか?」みたいなレベルから整理されたんです(文字コードの話からも逃げてません)。 派生の URLSearchParams とか使うと、今まで文字列結合でやってたこともいらなくなったり、地味に便利です。なぜ無かったのかと。
さらに、バックエンドとの同期や、サーバからのプッシュの受信、位置情報をベースとしたイベントの取得など、低レベルな API を個々に提供してます。やりたい放題ですね。だから Service Worker は HTTPS オンリーなんですよ。
今はまだ、モダンブラウザのみの閉じた環境であるのをいいことに、ServiceWorker 内でしか使えない API が多いですが、いずれ外に出てくればオフラインの世界以外でも使い道ありそうですよね。
というか、ネットワーク的な意味では、最終的に TCP/UDP Socket 自体を扱えるようになれば、もっとやりたい放題な可能性も待ってます。
すっかりおなじみの WebComponents もそうです。
WebComponents を使えば、標準化を待たずに独自のタグを定義可能になるけれど、それだけのために仕様が多くて複雑で低レベルで扱いが難しいと思ってる人って多いんじゃないでしょうか?
逆です、「それだけのため」じゃないからです。
WebRTC も色々覚えること多いですよね。の割にシグナリングの方法は仕様に無かったりには、ちゃんと意味があるんです。
WHATWG に限らず、 ECMA Script の property descriptor や dynamic proxy 、Symbol などなどのメタ API 郡も同じように解釈できそう。
低レベルは高レベルをかねる
ユーザは別に全ての API を HTML5Rocks に書かれた通りに組み合わせて使わないといけない訳じゃないんです。 それは一部のユースケースでしかない。
低レベルな API は、教科書の想像の範囲にとらわれず、自由に API を組み合わせて、自分が思う独自の世界をそこに気づき上げられる人たちの為に提供されています。どう使うかは、あなた次第。
ライブラリを作るもよし、フレームワークを作るもよし、誰も思いつかなかったような使い方で世界を変えたって全然構わない。
そうやってできたものが、 Polymer (polyfill 色が強いけど) や Peer.js みたいな高レベルな API を提供するライブラリになったり、それをコンセプトレベルで取り込んだ将来の Angular.js や Flux の先にある何かになったりすれば良くて。
そのままじゃ使いにくい という人はそれらを使えば良い。
開発者に可能性が増えたから、そのエコシステムの形成も進めやすい、これが重要。
そして生まれたユースケースから、欠けている API は提案すれば良いし、逆に頻出パターンにブラウザネイティブ実装の恩恵が欲しければ、より高レベルな API として提案するのもアリかもしれません。
少なくとも、ドラフトの上で生まれ ML で揉まれただけの API よりも、より洗練した実装に早くたどり着けそうな気はしますね。
Extensible Web の夜明け
マニフェストの公開は 2013 年なんで結構前ですが、そこで紹介されたような世界ってやっぱりすぐには実現しません。 仕様の策定とブラウザの実装に引っ張られない世界、それを実現するために必要な API だって、最初は仕様を策定しブラウザが実装する必要があるからです。
ServiceWorker や WebComponents は Polyfill じゃ完全には再現できないですし。
ところが、その実装も徐々に手元に届きはじめています。つまり、理想だった世界は徐々に現実味を帯びて来た感じがする。
リアルワールドな世界にシップするにはまだ早いかもしれませんが、それらの API を使ってアイデアを考え始めるには早すぎると言うことはありません。
自分の設計センスで高レベル API を提供するライブラリを作っても良い。 もっと全然別の、誰も思いつかなかったような使い方をしても良いはず。 そのアイデアが実現して広まるころには、ブラウザの実装ももっと進んで、リアルワールドに繋いでいけるようになっているはず。
自分は、こうして開発者が得た低レベル API という可能性に興味がある。そういうことを来年はやりたい。
草の根準備がちょっとづつ整って、仕様を書く側に偏ってしまった Web の進化の主導権が、コード書く側手に寄り戻ってくるというか、コード書く側が何かを形作って、リアルワールドからのフィードバックが健全に回る。
Web 開発者が、待ちのフェーズから、攻めのフェーズに移れる。
双方が適切な干渉をしあいながら、同じ過ちを繰り返さないようにちょっとづつ Web が進歩してゆく。
2015 年はそういう理想が、もっとリアルになっていくんじゃないかなと期待しています。というかなって欲しい。じゃないとつまらないので。
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 の仕様だったことがわかります。
最初のうちは 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 をたどって、彼らがこのころ何を揉めていたのかを調べてみると、同じくらいの時期に以下のスレッドが関連しそうな話をしています。
内容はざっくりこんな感じ。
「DOM とかを色々いじったり、 pushState したあとに発行した リクエストの referrer ってどうすんの?」
で、この辺は、仕様に無いわけではないけど、 「もう少しちゃんと定義しよう」 という感じで終わったようです。
おそらくこのあたりになってから、新しい仕様が追加されるごとにアルゴリズムを更新するのではなく、共通した仕様としての Fetch を定義し、そこを全体から参照するように整理しようという流れであることは伺えます。
同様のことは、仕様策定者の Anne 自身もブログに書いています。
実際に 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
前段で準備されたリクエストを発行する際の処理アルゴリズムについて書かれています。
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」です。
chromium と firefox で、すでに実装が始まっているようです。(ただし、現時点では 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 が提供されることは、開発者が自分の速度で、 W3C や WHATWG やブラウザベンダの動きとは別に、その仕組みを試して 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 を進化させて行く上で非常に重要な位置づけであると言えると思います。
そうしたこととは別のコンテキストで、単に Ajax で JSON を取るだけと言った場合は、ノウハウも多くこなれた 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 を外に出すかを決める作業のようです。
ただし、まだ議論中といった感じのようです。 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 じゃないが、作業中。
謝辞
このブログを書くにあたって、以下の方々にレビューや助言を頂きました。 内容で一杯一杯で後出しになってしまいましたが、 遅ればせながら本当にありがとうございました。
- @hirano_y_aa さん
- @myakura さん
- @omo2009 さん
- @saneyuki_s さん
- @ysnysnysn さん
追記
Blink/Chromium では M40 から Service Worker 上で Fetch API が使えます : https://t.co/x9jem7OG61
— nhiroki (@nhiroki_) November 11, 2014
