Block Rockin’ Codes

back with another one of those block rockin' codes

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 さんがた中の方々に色々ご助言頂きました、ありがとうございました!

2014 年をふりかえる

intro

恒例の振り返りです。

今年もこのブログを毎月更新するっていう目標は達成できました。

HTTP2

去年からはじめた HTTP2 まわりの活動はウェイトがかなり多かったですね。 HTTP2Study というコミュニティを小さく小さくやってきた感じです。

勉強会、ハッカソン、 issue-thon、conference とやって、アドベントカレンダーも無事完走しました。 自分でも HTTP2 の実装をするという目標があったので、それを粛々とやりました。

今まで HTTP レイヤやネットワークレイヤはきちんと勉強してこなかったので、この活動はかなり勉強になりました。

あと、今年は初めて IETF に参加しました。初参加でしかもちょっと話す時間をもらうっていうのは結構珍しいらしいです。

IETF89 で #http2study の話をしてきました。

この辺の活動は先日まとめました。

#HTTP2Study の軌跡

本当に凄い人たちが集まっています。この場を無駄にはしないようにしたいものです。 RFC が出るのが楽しみですね。

Go

Go研というこれまた小さくもガチな研究会をやってきました。 これも、イベントや発表会じゃない、本当の意味での「勉強会」ができた気がします。

github.com/goken/goken/

あと、Go には Tour of Go というチュートリアルがあるのですが、書籍など含めて「Tour of Go の次」っていうのがちょっと欠けていると思ったので、 Web+DB Press に Go の記事を書かせて頂きました。

Web+DB Press vol.82 で Go の特集を書かせて頂きました #wdpress

あとは、京都に行ってハンズオン してきたり、温泉行ってから GoCart やってきた。楽しかった。

Go のカンファレンス も関わらせてもらいました。 Rob 先生に直々に指導してもらえたのは貴重な体験でした。

Golang Error Handling lesson by Rob Pike

もともと HTTP2 のサーバを書くために始めたんですが、特に exe で動かしたいツールを書く時に便利で、地味に使ってます。

Node.js

やっと、やーーーーーーーっとのことで Socket.IO 1.0 がリリースされました。 その数日前に、 Guille から DM があったので、とりあえずまた来いということで呼んでミートアップやりました。

Socket.IO Meetup

1.0 が出たことで、仕事とかでもちょっと導入しやすくなりましたね。 ただ Socket.IO 自体は WebSocket の枠を超えてもっと広くネットワークプロトコルを抽象化していく存在になりそうなので、その辺はかなり期待しています。

個人的に Node.js でネットワークさわる何かを書く時は、 Socket.IO のインタフェースをまねるようにしてて、それがまあしっくり来ています。

High Performance Browser Networking

HTTP2 のカンファレンスに来てもらった Ilya が出版した HPBN の、ちょっとだけ?遅れた出版記念イベントをやりました。 彼の功績はホント凄いし、本当に良い話が聞けたなぁ。

HPBN meetup

Web のエンジニアが知っておくべきネットワークの知識としては、一つのマイルストーンとしてこの本は価値があります。

CROSS

次世代 Web セッションをやりました。去年に続き二回目ですね。 普段あまり表に出てこない方々も含めてかなり深い議論ができたと思います。

これからの Web の話をしよう。 (次世代 Web セッション @ CROSS2014)

ちなみに来年は出ませんが、このセッションはなんらかの形でやりたいなとは思ってます。

Podcast

「次世代 Web Podcast」ということで 4 月から始めました。

  • 今何がおこっていて
  • これからどうなっていくのか

これをテーマ毎に議論する感じです。

今年やったのは 13 テーマ、 intro と sideshow 含めて 18 エピソードでした。 合計で 20 人の最先端な方々に出ていただいて、お話を聞く事ができました。 出ていただいた方々には本当に感謝しかありません。ありがとうございました!

ぶっちゃけ「やるだけで精一杯」ですが、来年も地道にやっていければと思います。

2015 年に向けて

今年は、ずっと敬遠していた「標準化」というものに触れる機会が多かったです。 Web が成り立つ仕組みと、それが実際に行われている場面を自分の目で見る事ができたのは、自分の中で凄く良い経験だったと思います。

結果、俺自身はやっぱり標準化にはあまり向かないかなぁとは思います。求められるスキルも全然違うなと。

一方で、この標準化自体も見直されているという動きを感じます。


HTML5 も勧告されたし、 HTTP/1.1 もアップデートされました。今まで動いていたものを仕様にする作業は終わって、これから必要になる仕様を策定して行く作業が続いている中で、 これからの道の部分には、仕様のエディタや標準化団体だけじゃなくて、よりアグレッシブな Web の開発者の貢献が必要です。


標準化は、低レベルな API を提供するという形でそれの土台を整えてきました。 あとは、 Web の開発者がその上で新しい世界を築いて行くターンです。というかそういうことができるようになってきた。


それが、先日書いた話です。

Extensible Web の夜明けと開発者が得た可能性の話

ということで、来年は今年までに築いてきた HTTP やら HTML やらの土台の上で


自分なりに Web を進化させるためにできること(Extend the Web Forward) をやってみたいと思っています。

まずは土台から。

2015 年も、そんな感じに Web が盛り上がったら面白いなと思います。

来年もどうぞよろしくお願いいたします。

Jxck

#HTTP2Study の軌跡

Intro

この記事は HTTP2 アドベントカレンダー 24 日目 の記事です。

HTTP2Study

HTTP2 Study は、2013年8月くらいから小さく小さく活動しているコミュニティです。

HTTP2 もまだ HTTP2.0 と呼ばれていた頃で、 Draft でいうと 04 くらいですね(今は 16)。

(ちなみに、現在の仕様では "HTTP2.0" ではなく "HTTP/2" もしくは "HTTP2" が正しい名称です。)

仕様を策定してる HTTPbis としての議論は概ね片付いて、 HTTP2 の仕様は RFC にするための次のステップに移り、もし仕様を覆すような大きな指摘が無ければ、来年の頭にはこのまま RFC として公開されるかもしれないというところまで来ました。

そして今日はこの仕様策定をずっと追いかけてきた HTTP2Study がやってきたことを、簡単にまとめてみます。

きっかけ

この業界であれば HTTP/1.1 を普通にみんな使ってるわけですね。 そして keep-alive や pipelining といった仕様に対して、意見というか文句がある人は結構いて、思うに任せて呟いたりしていた人もよく見ていました。

まあ、 RFC 2000 番台で公開された仕様で、リアルワールドでがっつりデプロイされている仕様なので、そう簡単には変えられません。仕方ないのはわかります。

そして最近になって、その隣で WebSocket のような仕様が策定される様を自分は見ていました。仕様は結構ドラスティックに変わって、結果 RFC として公開されました。 でもやっぱり、仕様に文句言う人が結構沢山いるんですよね。まあわかります。フレームが複雑な気は確かにします。でもそれは意味があって議論した結果できたことで。。

というか、ついこの間まで仕様策定してたんだから、意見があるなら策定中に言えば反映されたかもなぁ、と薄々思っていました。


SPDY が出た時も文句がある人が結構いました。 HTTPS 前提とかバイナリフレームとか色々気に食わなかったんでしょう。でもこれは一企業の独自プロトコルなのでまあしょうがない。


でも、これが HTTP の次の仕様として策定されるドラフトの土台になって、標準化が始まりました。

この大きな変化の中で、仕様の策定段階から仕様を読んだり、実装を進めたり、集まって自分たちなりに議論したりすれば、この仕様策定に少しは「関与」することがもっとできるのではないか?

RFC 出てから文句を言うのではなく、議論の段階でそこに参加することで、今まで結果に対して受け身でしか無かった「標準仕様」というものに対して、もう少し積極的に関われるんじゃないかと思い、そういう場所を作ってみようという思いがありました。

単に参加者の前でスライドを読んで終わる発表会や、 LT 大会よりも、実装とか議論を意識的に重視してきたのはその意味もあります。





嘘です、 Web が変わって行く様が見たかっただけです。

実装

HTTP2 を実装するのが一つの目的なので、主要な人はだいたい実装を持っています。

主な実装は以下の Wiki にまとめられているんですが、この中で 5 つ、ここに載ってないものでも 2,3 個が日本から出ています。

http2 implementation

言語で言うと、 C, Go, Node.js, Ruby, Haskell etc と言う感じで多岐に渡っています。

中でも @tatsuhiro_t さんが実装する nghttp2 は、世界で最も素早く仕様に追随し、ほぼ全ての機能を実装して、あらゆるところで使われている神実装です。功績は書ききれない。

httpbis のチェアである Mark Nottingham にも 「nghttp2 は参照実装(reference implementation) だ」とお墨付きをもらっています。

こんな凄い開発者が日本人だったから HTTP2Study はできたという面は、正直かなりでかいです。

勉強会

小さく小さくやってきました。ただかなり濃かったと思います。

日々仕様は変わって行くので、まずは仕様策定が進む大事な会議である IETF の会議に参加したメンバーに、その参加報告をしてもらっています。主に @mad_p さんや @jovi0608 さんにやって頂きました。

海外渡航とか長期滞在はそう簡単にはできませんので、この報告は非常に助かってます。 (今年は、自分も IETF89 に参加する機会 があったのでやらせていただきました。)

各自が実装してみた知見の共有も結構やりました、ただ発表会じゃなくてそこから議論が始まるくらいには近い距離感を意識したので、時間内に終わったことはあまりないし、それができるように発表は少なくしてきたのも良かったと思います。

他にも、 HTTP2 の性質上色々なレイヤに話が及ぶので、そうした話に詳しい方もお呼びして話して頂きました。

たとえば Canon の藤沢さんに HPACK の原型になった EXI圧縮 の話をお願いしたり、 @hirano_y_aa さんに、議論中の WebSocket over HTTP2 の話をお願いしたり、 @kaname さんに、ネットワークインフラ(プロバイダ寄り)からみた HTTP2 のお話をお願いしたり。

ここでしか聞けな話は多かったと思います。

hack-a-thon

実装者も多いのでハッカソンも何回かやりました。

このタイミングで、後述するテストケースが生まれたりもしましたね。 これらは、 HTTP2 を実装する上であるとうれしいものだし、 HTTPbis の ML では同じようなものをやるやる詐欺してる人も多かったけど、実際にちゃんと作りました。後者は @kazu_yamamoto さんが作ってくれたものをもとに今作業中です。

成果物も多くなったので、 http://http2.info/ を作ってまとめる作業もやりました。(ロゴも含めて @summerwind さんがデザインしてくれました)

また、初心者も実装に参加できるように、膨らんだ HTTP2 の仕様を削りに削って最小の Hello World を実装できるようにするハンズオン。 「HTTP2 最速実装」シリーズも作りました。これはやるたびに仕様が変わるので、持ち回りで最新の仕様にアップデートしたもんをやっています。 (v0 は wiki に俺が書いたのがはじまりなんだけど、さすがに古いのでこれはアップデートしないとだ。。)

そして、毎回会場を貸していただいてる @y_iwanaga_ さんには本当に感謝です。いつも色々手配して頂いて本当にありがとうございます!

issue-thon

HTTP2 の仕様は github で管理されているので、そこにあがっている issue を読んできて、みんなで議論するという回をためしにやってみました。

目標としては、 issue を眺めるだけじゃなくて、それを理解して議論してコメントを書くことで、議論に参加しようというもの。

といっても理解するだけでも結構大変なので、俺はコメントするようなところまで行けませんでしたが、かなり高度な議論になり、会自体はすごく勉強になりました。

これも少人数だったのがよかったのかも。

またやりたいと思うのですが、 HTTP2 の issue は基本片付いた(だから RFC に向けて進んでる)ので、まあ直近は無いかもれませんが、似たような構成を別の形でやりたいですね。

(ちなみに、 @mad_p さんが今年の IETF でコミュニティの話を発表したときは、海外勢には issue-thon が一番食いつきよかったようです。)

HTTP2 Conference

色々なタイミングが重なったので、今年は HTTP2 のカンファレンス を開催しました。 まあ世界初のカンファレンスというと聞こえがいいですが、単純に RFC も出てない仕様のカンファレンスという意味では気が早かっただけですかねw

ゲストとして、 High Performance Browser Networking の著者でもある Ilya Grigorik に来日してもらって、キーノートをお願いしました。

そして、この少し前に h2o を実装した @kazuho さんも交えて、トークセッションをやらせてもらいました。

全体的に濃くて良いカンファレンスになったと思います。

ドラフトに ACK が載った

そんなこんなで色々やっている間に、ドラフトにこのコミュニティがやってきた活動に対して謝辞が載りました。

http://tools.ietf.org/html/draft-ietf-httpbis-http2-16

The Japanese HTTP/2 community provided an invaluable contribution,
including a number of implementations, plus numerous technical and
editorial contributions.

日本の HTTTP/2 コミュニティは、多くの実装に加え、
様々な技術的および編集上の修正など、多大な貢献をしてくれました

http://http2.github.io/ のページのトップにも、 HTTP/2 JP のページへのリンクがいつの間にか載っていました。

多分世界的に見ても、ここまで盛り上がっている地域コミュニティは多分まだないので、「どこぞの島国にクレイジーな奴らがいる」くらいの印象なんだろうとは思いますが、まあこうして認知してもらえる程度には貢献できたのかなと思います。

今後

直近では、ハッカソン を年明けにやります。 これは Interop といって、それぞれが実装を持ち寄って相互接続試験をやるという内容です。

今回は最速実装のような初心者向けコンテンツも用意しない(interop に集中するため)ので、初心者向けではありません。

あとは、とりあえず RFC が出るのを楽しみにするフェーズですね。出たらちょっとしたお祝いでもやれたらなと思っています。 HTTP2 も RFC になる前に大どんでん返しがあって、振り出しに戻る可能性だってありますが。

他は、正直まだ考えていません。来年の今頃は #quicstudy になってるかもw

ふりかえって

コミュニティとしての成果だけでなく、そこにいる人々の個別の成果も含めれば、ここに書いたのはほんの一部です。 そして実際、自分個人としてはまあ大したことはしてません。日程を決めて場所を取ってコンパスに登録したくらいです。

ただ、幸運にも凄い人たちが集まって、そこから得られるものは凄く多かったし、なによりもこうやって、議論や実装を重視する場ができたのが自分としては良かったなと思います。

そうした人たちに色々教えてもらったおかげで、自分の実装もかろうじて動くくらいまではきました。

本当にこのコミュニティは勉強になります。凄い場所だなといつも思います。

今後どうなるかはわかりませんが、とりあえず次の interop に向けて各自冬休みがんばりましょう!!

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 年の中盤くらいに出た話ですね。

f:id:Jxck:20141221134603p:plain

書いたのは、Ember.js その他沢山の開発者である Yehuda Katz や、 Service Workers その他沢山のの仕様を策定している Alex Russell、Fetch, Stream, Cache その他たくさんの仕様を策定している Anne van Kesteren など WHATWG で活動する人たちです。

イメージでは、仕様も書いてるけどコードも書けるし、「W3C になんか任せてられるか!」って思ってそうな人たち。

サイトに行くと賛同する主立った人たちの名前が載ってます。もし賛同するならリンクから自分の名前を登録することもできます(意思表明だけで、別に何が起こる訳じゃないけど)。


で、具体的に何が変わるのか?

その辺の話は、特に中心になって呼びかけてる Yehuda Katz のブログ あたりにも書かれています。

低レベルな 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 するってなにか?」もちゃんと整理されたと。

githubfetch 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 descriptordynamic proxySymbol などなどのメタ API 郡も同じように解釈できそう。

低レベルは高レベルをかねる

ユーザは別に全ての API を HTML5Rocks に書かれた通りに組み合わせて使わないといけない訳じゃないんです。 それは一部のユースケースでしかない。

低レベルな API は、教科書の想像の範囲にとらわれず、自由に API を組み合わせて、自分が思う独自の世界をそこに気づき上げられる人たちの為に提供されています。どう使うかは、あなた次第。

ライブラリを作るもよし、フレームワークを作るもよし、誰も思いつかなかったような使い方で世界を変えたって全然構わない。

そうやってできたものが、 Polymer (polyfill 色が強いけど) や Peer.js みたいな高レベルな API を提供するライブラリになったり、それをコンセプトレベルで取り込んだ将来の Angular.jsFlux の先にある何かになったりすれば良くて。


そのままじゃ使いにくい という人はそれらを使えば良い。

開発者に可能性が増えたから、そのエコシステムの形成も進めやすい、これが重要。


そして生まれたユースケースから、欠けている API は提案すれば良いし、逆に頻出パターンにブラウザネイティブ実装の恩恵が欲しければ、より高レベルな API として提案するのもアリかもしれません。

少なくとも、ドラフトの上で生まれ ML で揉まれただけの API よりも、より洗練した実装に早くたどり着けそうな気はしますね。

Extensible Web の夜明け

マニフェストの公開は 2013 年なんで結構前ですが、そこで紹介されたような世界ってやっぱりすぐには実現しません。 仕様の策定とブラウザの実装に引っ張られない世界、それを実現するために必要な API だって、最初は仕様を策定しブラウザが実装する必要があるからです。

ServiceWorker や WebComponents は Polyfill じゃ完全には再現できないですし。


ところが、その実装も徐々に手元に届きはじめています。つまり、理想だった世界は徐々に現実味を帯びて来た感じがする。

リアルワールドな世界にシップするにはまだ早いかもしれませんが、それらの API を使ってアイデアを考え始めるには早すぎると言うことはありません。

自分の設計センスで高レベル API を提供するライブラリを作っても良い。 もっと全然別の、誰も思いつかなかったような使い方をしても良いはず。 そのアイデアが実現して広まるころには、ブラウザの実装ももっと進んで、リアルワールドに繋いでいけるようになっているはず。


自分は、こうして開発者が得た低レベル API という可能性に興味がある。そういうことを来年はやりたい。

草の根準備がちょっとづつ整って、仕様を書く側に偏ってしまった Web の進化の主導権が、コード書く側手に寄り戻ってくるというか、コード書く側が何かを形作って、リアルワールドからのフィードバックが健全に回る。

Web 開発者が、待ちのフェーズから、攻めのフェーズに移れる。

双方が適切な干渉をしあいながら、同じ過ちを繰り返さないようにちょっとづつ Web が進歩してゆく。


2015 年はそういう理想が、もっとリアルになっていくんじゃないかなと期待しています。というかなって欲しい。じゃないとつまらないので。

#extendthewebforward

Golang Error Handling lesson by Rob Pike

Intro

この記事は Go Advent Calendar 2014 の 15 日目の記事です。

例えばネットワークのフレーム処理的なものを書いている場合、以下のようなコードがよくでてきます。

There are many codes like this, while writing a Network Frame Parser program.

var type uint8
err = binary.Read(r, binary.BigEndian, &type)
if err != nil {
    return err
}

var length uint32
err = binary.Read(r, binary.BigEndian, &length)
if err != nil {
    return err
}

...

関数の中では、各要素の長さ毎に読み込んで、読み込みに失敗したらエラーを呼び出し元に返す。

each function reads length of value from socket and return error if failed.


書き込む時は、ほぼ逆のコードをおぼ同じように書きます。

same things happens in writing to socket code.


さすがにこれが何個も続くと DRY ではないし、エラーの処理を忘れても気づきにくいという問題がある。

same code appears again and again, it's not DRY. and also you probably forget handle error which you can't notice easily.

Go のエラーの問題点

Go では多値を返して、その最後がエラーになるという形式が、一般的でありかつ型として定義されている。

It's a basic way in go, returning multiple values from function, and error is last value of then.


ただし、問題は戻り値は「処理しなかったとしてもコンパイルが通る」という仕様になっている。

but even if you don't care about return values, it's not compile error.


つまり上の例は、以下のように書いてもコンパイルが通る。

it means that compiler allows code like this.

// ignore return values
binary.Read(r, binary.BigEndian, &type)
binary.Read(r, binary.BigEndian, &length)

これが結構問題で、取り忘れたことがコンパイラだけでは分からない。

there is no notification when you forgot to process returned value even if it inclueds error.


知りたければ、別途ツールを使って検査するしかない。

if you want to fined uncaught error, you need another linter tool.


しかし、例えば戻り値の取得を必須にするとそれもまた問題で、例えば fmt.Println が多値を返すのでプリントデバッグが大変なことになる。

should compiler force programer to handle returned value ? I don't think so, for example fmt.Println returns error too. so its make difficult doing PRINT DEBUG.

_, _ := fmt.Println("print debug")

_, _ := fmt.Println("here")

_, _ := fmt.Println("")

How to solve?

特に Read や Write での処理が増えて行った場合、コードをすっきりさせつつ、 Error を確実に処理する方法について、自分の中では色々と試行錯誤していた。

I tried some practice for make lots of Read/Write proces simple, and make sure process all Errors.


で、先日 http://gocon.connpass.com/event/9748/ のために来日して下さった、 Rob Pike 先生に、この件を聞いてみた。

And I had a chance to ask Mr.Rob Pike about this problem at after party of http://gocon.connpass.com/event/9748/ in Japan last month.


そして Rob 先生は、俺のキーボード(悲しいことに vim しか入ってなかった。。)で、実際に書きながら説明してくれました!!なんという幸運。

and then, Mr.Rob taught me with writting a code on my Mac(unfortunately, I installed only vim...), what's a presious happenig !!

先生が説明しつつ書いてくれたコードがこちら。

Mr.Rob show me the code like this.

type errWriter struct {
    w io.Writer
    err error
}

func (e *errWriter) Write(p []byte) {
    if e.err != nil {
        return
    }
    _, e.err = e.w.Write(p)
}

func (e *errWriter) Err() error {
    return e.err
}

func do() {
    ew := &errWriter{
        w: ...
    }
    ew.Write(buf)
    ew.Write(buf)
    ...
    ...
    if ew.Err() != nil {
        return ew.Err()
    }
    return nil
}

Writer の Wrapper になっていて、エラーが発生した時は内部でそれを保持しつつ、以降の処理は全てパスされる。

Write a wrapper struct of Writer. while processing, hold a error if happened and pass all Write() below.


最後にエラーの有無を確認することで、エラー処理を一カ所にまとめる。

end of the function, check and process the error in struct. this gathers error handling in one place.


とりあえず Writer と Reader について作っておけば、 Write や Read の処理が多くなるほどメリットが大きくなる。

prepare a struct for Writer and Reader has a merit when you write a lot of Write()/Read() process.


素朴だけど Go らしいコードですね。

so simple but powerful as golang way :)

Movie

すっごく見づらいけど、ぶっちゃけ後で人に見せる映像取るよりも、その場でロブ先生の話聞く方が大事だしそれで一杯一杯だったので、まあ雰囲気だけみてください。

Sorry for hard to watch, but it's more important to see and hear Mr Robs Exmplain haha. be patient :)

Mr. Rob Rike taught me about practice of error handling in Go at GoCon 2014 from Jxck on Vimeo.

Special Thanks

thanks Mr.Rob Pike !

from your student Jxck :)

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 じゃないが、作業中。

謝辞

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

追記

Stream API がブラウザにやってくる

Intro

今日は、フロントのプログラミングスタイルに、にまた一つ大きな変化をもたらすであろう Stream という API についてです。

この仕様は現時点でまだ策定中であるため、 API は変更される恐れがある点にご注意ください。

Stream API

以前 「Node.js の Stream API で「データの流れ」を扱う方法」 という記事を書きましたが、簡単に言うとあれがブラウザにもやってくるという話です。

非同期処理おさらい

もう何度も書いた話なので駆け足で。

JS はシングルスレッドでイベント駆動な世界なので、何をするにも非同期であり、コールバックを登録することで完了した結果を受け取る API が基本です。 これは、ブラウザの DOM の API でも、 Node.js でも共通しています。

概念を疑似コードで書くと以下のような感じです。

console.log('1');

file.open('path', (err, data) => {
  // 非同期なデータの読み出し終わってから実行される
  console.log(data);
});

console.log('2');

// 1 -> data -> 2

実行順序についてはもういいでしょう。

そして、最近ではこれを抽象化(あるいは部品化)する仕組みとして、 Promise の導入が進んでいます。 ざっくり言うとこんな感じ。

var p = file.open('path');

p.then((data) => {
  console.log(data);
}).catch((err) => {
  console.error(err);
});

非同期に取得/生成される結果自体をオブジェクトにし、コールバックの適応方法を切り離したことにより、 例えば部品化やエラー処理の集約ができるようになりました。 API が Promise を返さないものは、自分で Promsie オブジェクトにくるんで、同様のインタフェースに寄せることができます。

統一したインタフェース(thenable)に則っていることにより、他の部品との組み合わせも以下のようにできます。

promise
 .then(b)
 .then(c)
 .catch(console.error.bind(console));

しかし、どちらもコールバックは、処理の完了後に一回実行される実装で考えるのが普通です。 つまり上の例はいずれも、「ファイルを読み終わったら、その結果をまとめ、一回だけコールバックを実行する」となります。

連続したイベントを表現する

上記の例は、一度だけその時のファイル内容をまるっと表示しますが、大きなファイルを読む場合は、 読み出せたところから表示できる方が適した場面があります。

そうした連続したイベントを扱うのが Stream API です。

Node では Stream が以前からあり、今 v1, v2, v3 と来てちょっと移行段階なので、 これもざっくり概念疑似コードで書くと以下のようなイメージ。

filestream = file.createReadStream(path);

filestream.on('data', (data) => {
  console.log(data);
}).on('error', (err) => {
  console.error(err);
});

例えば Node.js では、 Stream の実装が EventEmitter になっており、ファイルを開くとその中身を Chunk ごとに読み出して、その結果を引数にして逐一 data というイベントを発火します。

これにより、 Promise の例とは違い大きなファイルも読み出した端から表示されます。

すばらしい点は、 Stream も他の Stream と組み合わせられるところです。 Node.js では pipe() というメソッドに stream を渡すと、それらを連結されることができます。これは Unix のパイプ (|) と同じです。

Node.js の Stream には 4 つの種類があります。

  • ReadableStream: そこからデータが読み出せる(データ生成源のラッパー: file, socket, stdin etc)
  • WritableStream: そこにデータを書き込める(データ入力先のラッパー: file, socket, stdout etc)
  • TransferStream: 左から右に処理しながら流す(データの改変などを行う: 圧縮, 暗号化, パース etc)
  • DuplexStream: HTTP サーバのようにデータの送受信を両方になう: http server etc

Transfer, Duplex は Readable と Writable の組み合わせでもあります。

こんな感じ。

// readable.pipe(transform).pipe(writable);
getStream.pipe(transferStream).pipe(fileWritableStream);

データがイベントループの導きによって、流れるように処理されて行く様が見て取れますね。

「Stream を制す者は Node.js を制す」

は決して大げさではないのです。

フロントにも Stream が欲しいよね

せっかくサーバサイドで流れるように処理されたデータを、せっかく WebSocket のようなステートフルな接続で送っても、 送った先がコールバックや Promise の世界に戻ってしまうと、片手落ちです。

「フロントでも Stream を使いたい。」

こうして人類はブラウザでも動く Stream の実装を吐いて捨てるほど作ってきました。 ちなみに俺は「全く同じものを!」ということで、 Node.js の Stream をコツコツ移植しています。 (その副作用で移植した Assert の方が使われてるというのは、また別のお話)

そして、同様に「この仕組みを標準にしよう」という話が進んでおり、もうすぐフロントも Stream ベースな時代が来る!というのが今日のお話。

ちなみにこままで駆け足で説明してきたことを、 Node.js のコミッタが熱く語る話はこちら。 mozaic.fm #10 node.js sideshow

WHATWG Stream API

本題です。ずばり WHATWG がメンテするドラフトに Stream API が追加されました!!

現時点での仕様はこちらです。

Streams Living Standard: https://streams.spec.whatwg.org/

この仕様では、 readable, writable, transform の三種類が定義されています。(duplex はありません)

これらの仕様は fetch, service worker, source extension, video, audio などで、流れるデータを表現するのに使われていくことになります。

例えば、 video や audio のデータを readable stream として受け取り、それを transform stream に繋ぐ(pipe)ことで、エフェクトをかけたりすることができるし、 圧縮されたデータを xhr で取得し、その readable stream を、解凍する transform stream に通してから、ファイルに書き込むための writable stream に渡すなんて使い方が想定されます。

完全に Node.js と同じですね。

構成図

全体の構成はこんな感じになります。 詳細は以降順次解説していきます。

f:id:Jxck:20141101161522p:plain

実装は ES6 の class ベースになっており、継承による拡張についても仕様に言及されています。

ReadableStream

ReadableStream は以下のような E@6 のクラスとして定義されています。 実際にデータが生成される生成源は underlying source と呼ばれます。 そこから取り出したデータを chunk ごとに内部に管理されている queue に追加して行きます。

class ReadableStream {
  constructor({
    start = (enqueue, close, error) => {},
    pull = (enqueue, close, error) => {},
    cancel = (reason) => {},
    strategy = %DefaultReadableStreamStrategy%
  } = {})

  get closed()
  get state()

  cancel(reason)
  pipeThrough({ writable, readable }, options)
  pipeTo(dest, { preventClose, preventAbort, preventCancel } = {})
  read()
  wait()
}

動く環境はまだ無いですが、以下のようになる予定です。 WebSocket をラップした ReadableStream です。

function makeReadableWebSocketStream(url, protocols) {
  const ws = new WebSocket(url, protocols);
  ws.binaryType = "arraybuffer";

  return new ReadableStream({
    start(enqueue, close, error) {
      ws.onmessage = event => enqueue(event.data);
      ws.onend = close;
      ws.onerror = error;
    },

    cancel() {
      ws.close();
    }
  });
}

var webSocketStream = makeReadableWebSocketStream("http://example.com", 80);

webSocketStream.pipeTo(writableStream)
  .then(() => console.log("All data successfully written!"))
  .catch(e => console.error("Something went wrong!", e));

WritableStream

同様に WritableStream の定義と実装例です。 write() で渡されてきた chunk を処理します。

class WritableStream {
  constructor({
    start = (error) => {},
    write = (chunk) => {},
    close = () => {},
    abort = (reason) => close(),
    strategy = %DefaultWritableStreamStrategy%
  } = {})

  get closed()
  get state()

  abort(reason)
  close()
  wait()
  write(chunk)
}

start() の部分は Promise を許容する仕様になているので、解決するように橋渡しします。

function makeWritableWebSocketStream(url, protocols) {
  const ws = new WebSocket(url, protocols);

  return new WritableStream({
    start(error) {
      ws.onerror = error;
      return new Promise(resolve => ws.onopen = resolve);
    },

    write(chunk) {
      ws.send(chunk);
    },

    close() {
      return new Promise((resolve, reject) => {
        ws.onclose = resolve;
        ws.close();
      });
    }
  });
}

var webSocketStream = makeWritableWebSocketStream("http://example.com", 80);

readableStream.pipeTo(webSocketStream)
  .then(() => console.log("All data successfully written!"))
  .catch(e => console.error("Something went wrong!", e));

Queuing Strategies と Back Pressure

TODO: WIP

Stream はデータの流れを表現し、 pipeTo でそれらを組み合わせる訳ですが、 source や sink もしくは transfer が行う処理などによっては、データの流れる早さにギャップが生じることがあります。

例えば、ローカルのファイルからデータを読み出す ReadableStream から、それをネットワークに流す WritableStream と繋ぐような場合は、 前者のデータ生成が、後者のデータ処理よりも早くなる可能性があります。

すると、ReadableStream の内部 Queue に処理待ちの chunk が溜まり続け、 そのままでは溢れてしまうため、この場合はデータ生成源であるファイルからの読み出しを止める必要があります。

こうした処理は Back Pressure と呼ばれ、各 Stream の中でそれぞれの Queue は適切に管理する必要があります。

各クラスにある strategy というプロパティは、この内部 Queue の管理戦略です。

実装としてはこんな感じらしい。

  • ByteLengthQueuingStrategy: Queue を byte サイズをベースに管理
  • CountQueuingStrategy: Queue を chunk の数をベースに管理

両方定義としてはこんな感じ。 コンストラクタで HWM (HighWaterMark) を決める。

class Strategy {
  constructor({ highWaterMark })
  shouldApplyBackpressure(queueSize)
  size(chunk)
}

Outro

Node.js の Stream API でもちょくちょく話題になる、 Backpressure の話や、ソースが Pull/Push ベースなのかどうかに関する話題も、きちんと議論されているようです。

これでサーバからクライアントまで、全て奇麗にデータが流れる Stream の列が出来ると、非常に奇麗にリアルタイムな表現が実装できるようになりそうでうs。

すでに Chromium への実装が始まっており、 polyfill の実装 も公開されています(ES6 ですよ時代は)。

間違いでした。 domenic と dominic さんです。