Movatterモバイル変換


[0]ホーム

URL:


Block Rockin’ Codes

back with another one of those block rockin' codes

この広告は、90日以上更新していないブログに表示しています。

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):  128getUint16(0): 32940getUint32(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);// URLdocument.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 と相互変換することができましたが、任意のエンコードとの変換はバイナリ操作によって自分で行う必要がありました。

EncodingAPI を用いると、文字列を「文字コードに応じた」バイナリデータと相互変換できます。インタフェースは、内部のエンコーディング形式をそれぞれに持つ事ができるように 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]))// 'あ'

デコードでは多くの形式が対応しているのは、例えば任意のテキストファイルを FileAPI で読み込んで、その内容を表示するといった場合に、多くのエンコーディングに対応する必要があったからかと思います。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 andUDP SocketAPI

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

そこで、もういっそTCP/UDP のソケットAPI がそのままブラウザから使えるようにしてしまえ、という感じのコンセプトです。SocketAPITCP/UDP への write() はもちろん、 listen() もできます。つまりサーバが立ててしまう可能性があるのです。

// writevar client =new TCPSocket('http://example.com', 80);client.writeable.write('ping').then(() =>{  client.readable.wait().then(() =>{    console.log(client.readable.read());});});// listenvar 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 が扱っていたレベルまで下がっている SocketAPI の実装ではそこが非常に重要になります。そこで 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) をイベントで検知することができたり、外から PushAPI で飛んで来たメッセージをイベントで受け取ることができます。

ブラウザとは独立したコンテキストで、ブラウザの裏でメタな処理が可能な環境を提供するのが、 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 CacheAPI とは違い、一部のリソースだけキャッシュを更新すると言った事が柔軟に可能です。

SW + Cache が Application CacheAPI に変わるオフラインアプリの作成で注目されているのはこの部分です。

// 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 がきちんと扱えるようになったためと言えます。

注意点として、標準の DOMAPI はこの 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 自体は、標準化してブラウザが実装しないといけません。だから、理想のサイクルが回るにはもう少し時間がかかります。

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

引用をストックしました

引用するにはまずログインしてください

引用をストックできませんでした。再度お試しください

限定公開記事のため引用できません。

読者です読者をやめる読者になる読者になる

[8]ページ先頭

©2009-2025 Movatter.jp