Block Rockin’ Codes

back with another one of those block rockin' codes

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

これにより、以下を調べる取り組みのようです。

  • Web の持つ機能を再現可能かを調べることで、足りてない API が無いかを調べる。
  • Custom Elements の API が、プラットフォームを再現するものとして十分なデザインかを検証する。

つまり、 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 を使って完全に再現できる筈です。

http://fetch.spec.whatwg.org

TCP and UDP Socket API

WebSocket は「Web 上での双方向通信」を目的としており純粋な TCP ではないし、 WebRTC は「Web 上での P2P」を目的としており純粋な UDP ではありません。 fetch() は「ブラウザからの」 HTTP リクエストとして一番低レベルでした。

そこで、もういっそ TCP/UDP のソケット API がそのままブラウザから使えるようにしてしまえ、という感じのコンセプトです。 Socket APITCP/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 のパース/シリアライズが可能で、パーセントエンコーディングpunycodeIPv6 や 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() するくらいが良いと思います。

https://url.spec.whatwg.org/

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 と同じようなイメージです。

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

特にイベントが連続的に発生する 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 自体は、標準化してブラウザが実装しないといけません。 だから、理想のサイクルが回るにはもう少し時間がかかります。

その辺が最近自分が取り組んでいる事なんですが、長くなったのでその話はまたいずれ。