ジンジャー研究室

長めのつぶやき。難しいことは書きません。

HTTP/2で再帰的にPUSH_PROMISEする場合の注意点

タイトルの通りで、今ぶち当たっている問題を共有するのが目的。

f:id:jinjor:20150311140752p:plain:w400

図のように、あるHTMLを起点にして依存しているすべてのリソースに対して、再帰的にPUSH_PROMISEしたい。

PUSH_PROMISEはPROMISEであるため、サブリソース本体が起点となるリソースよりも早く返却される必要はない。つまり、ブラウザはサブリソースの返却にブロックされずに、先に目的のリソースの評価を開始できるというメリットがある。

f:id:jinjor:20150227224512p:plain:w400

再帰的なPUSH_PROMISEとは

ただし、これを再帰的にやろうとした場合に問題が生じる。 実は、PUSH_PROMISEはクライアントからのリクエストにあたるストリームからしか発行できない。 だから、Stream 2からStream 4を生み出すのは無理。

f:id:jinjor:20150311143601p:plain:w500

そうすると、Stream 1からPUSH_PROMISEを発行しなければならないのだが、この時にStream 1が既に閉じられているとエラーになる。

f:id:jinjor:20150311143606p:plain:w430

つまり、依存するすべてのリソースに対してPUSH_PROMISEするまで、Stream 1を閉じてはいけないことになる。 Stream 1を閉じずにindex.htmlを送ることもできるが、そうするとPUSH_PROMISEされる予定のリソースを先にリクエストして干渉する恐れがあるので避けたい(SHOULD)。

そもそもの動機

これがいつ問題になるかというと、動的にHTMLなりCSSなりをパースしてPUSH_PROMISEしたい場合、パース中は最初のストリームが返却待ちになってしまう。依存ツリーがが深ければ深いほど待ち時間は長い。

かといって、事前に静的に依存関係を調べる方法だと、そのタイミングとキャッシュの管理を考える必要が出てくる。

まとめると、

  • 動的に依存を調べる場合はリクエストの返却が遅れる(PROMISEの恩恵が受けられない)。
  • 静的に依存を調べる場合はキャッシュの管理などが必須。

こんな感じなので注意が必要。もう仕様もFIXしちゃったし…。

そしてこのエントリは、最初の図のように出来ると思って色々作り始めてしまった愚痴でもある。

シングルトンモジュールのテスト

シングルトン(singleton.js)。

module.exports = {
    value: 0
};

その値をインクリメントするやつ(incr.js)。

var singleton = require('./singleton.js');

module.exports = function incr() {
    singleton.value++;
};

incr()を呼んだら値が増えますというテストを2つ記述。

var assert = require('assert');
var incr = require('../src/incr.js');

describe('a', function() {
    it('1', function() {
        var singleton = require('../src/singleton.js');
        incr();
        assert(singleton.value === 1);
    });
    it('2', function() {
        var singleton = require('../src/singleton.js');
        var incr = require('../src/incr.js');
        incr();
        assert(singleton.value === 1);
    });
});

なんと同じテストなのに2つ目だけ失敗!ガーン!

改良?

シングルトンだからそりゃそうだよねと思って、調べてみたらrequireにキャッシュされているモジュールを削除できるらしい。というわけで、使い終わったシングルトンを削除っと。

var assert = require('assert');
var incr = require('../src/incr.js');

describe('a', function() {
    it('1', function() {
        var singleton = require('../src/singleton.js');
        incr();
        assert(singleton.value === 1);
        delete require.cache[require.resolve('../src/singleton.js')];//追加
    });
    it('2', function() {
        var singleton = require('../src/singleton.js');
        incr();
        assert(singleton.value === 1);
    });
});

また失敗!ズコー!

ちなみにこの時のsingleton.value0。実はincrが参照しているのは最初にrequireしたシングルトンなので、消したつもりで消えていなかった。

これも駄目

内部でrequireしてみたけど、incr自体がキャッシュされているのでこれも駄目。

var assert = require('assert');

describe('a', function() {
    it('1', function() {
        var singleton = require('../src/singleton.js');
        var incr = require('../src/incr.js');//移動
        incr();
        assert(singleton.value === 1);
        delete require.cache[require.resolve('../src/singleton.js')];
    });
    it('2', function() {
        var singleton = require('../src/singleton.js');
        var incr = require('../src/incr.js');//移動
        incr();
        assert(singleton.value === 1);
    });
});

直った

最終的にこうすると直る。

var assert = require('assert');

describe('a', function() {
    it('1', function() {
        var singleton = require('../src/singleton.js');
        var incr = require('../src/incr.js');
        incr();
        assert(singleton.value === 1);
        delete require.cache[require.resolve('../src/singleton.js')];
        delete require.cache[require.resolve('../src/incr.js')];//追加
    });
    it('2', function() {
        var singleton = require('../src/singleton.js');
        var incr = require('../src/incr.js');
        incr();
        assert(singleton.value === 1);
    });
});

より一般的には、require.cacheのkeyで回して全部消す。中には初期化がクソ重たいモジュールがあるかもしれないが、そんなことを気にしてはいけない。

というわけで、この例くらい単純ならいいんだけど、思わぬところに思わぬ依存があって死ぬので確実に全モジュールを削除 & 毎回関数内でrequireを徹底する必要があるという話。最近Fluxなアプリがシングルトンモジュールをせっせと作っていて、こういうテストをするためにJestに依存するの嫌だなーと思っている。

あと、効果音がちょっと古い。

HTTP/2のサーバープッシュを自動化するNode.jsライブラリを作った

そろそろRFCとして公表されるHTTP/2ですが、GoogleがHTTP/2を使ったRPCフレームワークを出してみたりとか、その界隈は大盛り上がりですね!

HTTP/2の目玉機能と言ったら、やっぱりPUSH_PROMISE!

f:id:jinjor:20150227224512p:plain:w340

PUSH_PROMISEと言えば、「index.htmlが必要だったらapp.jsとかstyle.cssも要るよね!」とサーバ側で勝手に判断して送りつける、というのが良くある説明なのだが、それを自動でやってくれるわけではないので、Node.jsサーバ用にライブラリを作ってみた。

jinjor/auto-push · GitHub

npmにもpublishしたので、npm install auto-push可能。

やっていることは、レスポンスの直前にHTMLをパースして、必要そうなJavaScriptなりCSSなり画像なりをプッシュする。それだけ。

HTML Importsにもたぶん対応してるけど、まだちゃんとテストしていない。

サーバーに組み込む

典型的な使い方。

var fs = require('fs');
var autoPush = require('auto-push');
var http2 = require('http2');
var ecstatic = require('ecstatic');

var options = {
  key: fs.readFileSync(__dirname + '/ssl/key.pem'),
  cert: fs.readFileSync(__dirname + '/ssl/cert.pem')
};

http2.createServer(options, autoPush(ecstatic(__dirname + '/public'))).listen(8443);

Express.js上でもいけると思ったけど意外とトラブルに見舞われているので、対応にはもう少しかかりそう。

プロキシとして使う

var autoPush = require('auto-push');
var http = require('http');
var ecstatic = require('ecstatic');
var request = require('request');
var fs = require('fs');
var http2 = require('http2');

// server
http.createServer(ecstatic(__dirname + '/public')).listen(8080);

// proxy
var options = {
  key: fs.readFileSync(__dirname + '/ssl/key.pem'),
  cert: fs.readFileSync(__dirname + '/ssl/cert.pem')
};
http2.createServer(options, autoPush(function(req, res) {
  request({
    method: req.method,
    url: 'http://localhost:8080' + req.url,
    headers: req.headers
  }).pipe(res);
})).listen(8443);

プロキシならNode.js以外のサーバにも対応できる。

課題

304(Not Modified)をどう扱えばいいのか分からない。 具体的には、クライアントから直接リクエストされたもの以外のif-modified-sinceヘッダ情報が無いので、適当にHTMLファイルと同じ情報を使いまわすとまあバグる。 仕方が無いので、現在は更新があろうと無かろうと200で送り返すという雑なことをしている。

その辺の仕様的に妥当な処置とか、教えてもらえたら喜びます。

まとめ

たぶん既に誰かやってるとは思う。

副作用って何だっけ

関数型界隈で、「状態がある=副作用」みたいな話を何度か聴いてちょっと違和感があった。 副作用とは、主たる目的の他に外部に悪影響を与えることだと思っていたのだが。 つまり「Hello, World」を表示することは、あまり副作用と呼びたくない。(主作用?)

そこで、そもそも副作用がどんなもので何が悪かったのかを思い出してみた。

以下、ひたすら例を挙げて感想を述べていくことにする。 JavaScriptで記述しているが、任意の言語に読み替えて問題ない。

データの更新1

function completeTodo(todo) {
  todo.done = true;
}

引数を書き換えているが、そういう関数だと思って使えば特に問題にはならない。

データの更新2

function completeTodo(todo) {
  todo.done = true;
  return todo;
}

似ているがこっちは紛らわしい。新しいのは戻り値だけだと錯覚してしまう。

データの更新3

function completeTodo(todo) {
  todo.done = true;
  updateDB(todo);
}

与えられた変数でDBを更新するパターン。変数がDBと同期しているという意図なのか、単なるミスなのか。

データの更新中にエラーが発生した

function completeTodo(todo) {
  todo.done = true;
  sometimesThrowError();
}

「データの更新1」の途中で失敗してしまったパターン。巻き戻しが必要。

ついでに何かを収集する

function calculate(a, b, warnings) {
  if(a === 0) {
    warnings.push('a is zero');
  }
  return a + b;
}

あるあるパターン。これはあまり怒っても仕方ない気がする。

計算過程で引数が変化する

function lastTodo(todos) {
  todos.reverse();
  return todos[0];
}

これは完全にアウト。

計算過程で引数が変化するので戻した

function lastTodo(todos) {
  todos.reverse();
  var last = todos[0];
  todos.reverse();
  return last;
}

…。

危ういreduce1

var newValue = array.reduce(function(memo, data) {
  memo[data.id] = data;
  return memo;
}, {});

JavaScriptだとこうなりがち。この時点では無害。

危ういreduce2

var oldValue = {
  '_1': 'foo'
};
var newValue = array.reduce(function(memo, data) {
  memo[data.id] = data;
  return memo;
}, oldValue);

oldValueとnewValueは同一変数。どうしてこうなった。

遅延読込み

var cache = {};
function findById(id) {
  if(cache[id]) {
    return cache[id];
  }
  cache[id] = getFromDB(id);
  return cache[id];
}

var data = findById('_1');

変数に影響を及ぼしてはいるが、特に問題はならない。

IDの自動生成

var n = 0;
function nextId() {
  return '_' + n++;
}

function createTodo(name) {
  return {
    id: nextId(),
    name: name
  };
}

副作用と言えなくもないが、意図したものだと思う。

コンソール表示

function printTodo(todo) {
  console.log(todo.name);
}

これは副作用とは言わないのではないか。

分類

危険度による分類

  • バグ
  • 危うい(書いた本人の認識が薄い)
  • 危うい(書いた本人は意図しているが認識の共有が必要)
  • 無害(設計パターン)
  • 無害(状態更新を目的とした関数や画面表示)

副作用を及ぼす対象による分類

  • 引数
  • 関数外
  • 言語外

まとめ

ヤバい奴らはやっぱりヤバい。危険な状況は単純に書き換えるか否かというよりも、意図せず起こしがちなパターンに潜んでいる。 それから、mutableなデータ構造がしばしばそのような状況を生み出していることは紛れもない事実だと思う。

HTTP/2でHello Worldしてみた

アドベントカレンダー26日目。嘘です。

Node.jsで簡単なHTTP/2サーバを作ってみた。Hello, Worldしかできない。

説明のために誰でも読める風にしたかったので、ストリームもオブジェクト指向もなく、決めうちの多いシンプル実装。あとはブラウザで動かないとつまらないので、少なくともFirefox Nightlyでは動くようにはしておいた。

あと書きながら一部まだ理解できていない部分があったりするのも何とかしたい。

HPACKの実装は summerwind/sasazka · GitHub からお借りしました。感謝。

参考

HTTP/2 Draft 16 日本語訳 – SummerWind

HTTP/2 最速実装 v3 // Speaker Deck

脱PolymerなWeb Componentsデザパタ

はじめに

今朝話題になっていたWeb Componentsの基本的な使い方・まとめ に触発されてみる。

すごい…!私にはこんな緻密な資料は書けないorz

だけど何か書きたい。書くぞ。

そろそろWeb Componentsしたい

Web開発に革命をもたらすと噂のWeb Componentsだが、そろそろプロダクションへの導入を検討したい。 2014年12月現在、Chrome 36+で全機能が使えるので、デモ画面や開発(テスト)ツールでは既にPolyfillなしで色々出来る。積極的に遊んでみたい。

とはいえ、現状と言えばWeb Componentsに関する情報は紹介記事がほとんどで、あまりプラクティカルな領域に踏み込めていない感がある。まあそんなものは実際に使い始めればわんさか出るので、別にその時になってからでも良いのだが、せっかくなので戦略の一つや二つ練っておきたい。

もうひとつ、Google製PolyfillライブラリのPolymerについて。 少し触ってみた感じでは特に不自由もなく素晴らしいライブラリだと思うのだが、知らず知らずのうちにロックインされてしまう微妙さがある。 それには多分いくつかの要因がある。

  • Web Componentsの記事を調べに行くと一緒にPolymerを紹介される
  • Template要素に十分な機能を持たせるライブラリがPolymerにしかない
  • webcomponents.js(旧platform.js)とpolymer.jsの機能の境目が曖昧
  • polymer-elementとして流通しているライブラリを使うと伝染する

そういうわけで、「なんだか知らないけどPolymer使っとけば安心でしょ」みたいな思考放棄の果てにjQuery的世界を再現する前になんとかしたい。 PolymerもjQueryも悪くないけど。

というわけで、実際使っていく上で考えうる限りの使い方のバリエーション、留意点などをざっと並べてみようと思う。

以下ベストプラクティス集じゃないので、念のため。

4つの仕様のうち使いたいものだけを慎重に選ぶ

Web Componentsは以下の4つの仕様からなる。

  • Custom Element
  • Shadow DOM
  • Template Element
  • HTML Imports

もちろんだが、これらは全てを統合して使ってもいいし、独立して使うことも出来る。 全てが統合された理想世界については、既にPolymerの方で語られているのでここでは特に触れない。

ここからはPolyfillをどこまで信用するかという話でもあるのだが、やはり心情的にあまり危険な橋を渡りたくないので、自分のアプリにとって必要最小限の機能を使っていきたい。 (Webサイトならいざ知らず、ブラウザのバージョンアップによるデグレードに対してパッチを当てるまでに時間がかかり、しかもそれが致命的になる場合もある)

HTML Importsを使わない

まず第一歩。 勝手な推測では大多数の人の目的はCustom Element(+Shadow DOM)なのでは、と思わなくもない。 そこで、HTML Importsを使わずに普通に.jsファイルで済ましてしまうことが可能だ。

Template Elementを使わない

また、Template要素を使う必要性も必ずしもないので、ここも好みのテンプレートに置き換え可能だ。従来の使い慣れたテンプレートエンジンや、多分Virtual DOMも使える。 ES6では複数行の文字列リテラルを宣言できるので、うるさいバックスラッシュともオサラバできる。

Shadow DOMを使わない

ここはちょっと何が正しいのか判断が付かない。というのは、個人的にCSSの干渉で「そこまで」困ったことが無いので、いまいち重要性がピンと来ないというのがある。 個人的にはそれよりも、「共通のスタイルが欲しい時にCSSコンポーネント内にインライン展開するのってどうなの?」という方が気になる。(←見解があれば教えてください)

さておき、カプセル化を気にしなければShadow DOMも削ることが出来る。(Shadow DOMのPolyfill難易度は高い) 以上のように色々そぎ落としていくとCustom Elementだけで事が済む。ミニマル主義ならこれで行きたい。

非動作時の代替手段を用意する

何らかの事情でWeb Componentsが動作しない場合に、デフォルトの動きを用意するといったことも可能だ。

  • ブラウザがWeb Componentsをサポートしていない
  • ブラウザのJavaScript機能がオフになっている
  • CDNが落ちている
  • バグ

記憶によればGitHub<time is="relative-time">要素がそのような実装になっていたと思う。 通常動作時は相対時間を表示し、動作しない場合は絶対時間が表示される。私GitHub賢い思う。

Custom Elementの生成を動的にカスタマイズする

Custom Elementの扱い方として常に紹介されるやり方は、「HTML Importなりscriptタグで読み込むなりすれば後は自由に使えるよ」というものだ。ただ、それだけでは済まない場合、もっと柔軟に扱うことが可能だ。

別の実装に切り替える

まず準備として、scriptの実行時点ではdocument.registerElement()せず、何らかの関数が呼ばれた時に登録するようにする。 これによって動的に決まるコンテクストにおいて、同じインターフェイスを持つ別の実装に切り替えることが可能になる。

if(debug) {
  registerDebuggableXxxElement();
} else {
  registerXxxElement();
}

コンテクストを渡す

パラメータを渡すことも可能。

registerXxxElement(userSettings);

要素名を変える

もちろん代わりの要素名を渡すことも可能。

registerXxxElement(userSettings, 'another-name');

フォーム部品の動作に気を使う

最早あるある現象なのだが、ハイユーザビリティを謳いながらブラウザの機能を殺していく、という皮肉なことが気をつけていないと起こりかねない。 Custom Elementだからと言って調子に乗ってオレオレ実装を始めてしまうと、元々普通に出来ていた機能をどんどん奪ってしまう。その傾向は特にフォーム部品に顕著に現れる。

  • 適切なタイミングでchangeイベントを発火する
  • <select>相当の要素の場合、valueへの代入が配下のoptionに影響する
  • 上の逆
  • フォーム送信時にシリアライズされる
  • フォーカスがあたる
  • :checked擬似クラスによる選択、スタイルの変更が出来る

正直、これらを全て正しく実装することを考えると、私ならしんどい。テストも書いたがそれでもしんどい。 しかも、:checked擬似クラスを再現する方法は用意すらされていない。 手軽に機能を継承するならis属性(is="xxx")を使うのが良いだろうか。この辺りの知見がもっと欲しい。

ここで機能を奪ってしまうことによるデメリットは以下のようなものが考えられる。

  • 他のライブラリとの親和性…たとえば:checkedなどを当たり前のように利用する
  • より大掛かりなシステムへの依存…たとえばmy-formのような特別な要素と密結合する
  • 特別な使い方を覚えるための学習コスト

調べてもあまり出てこないが、この辺の議論や見解も欲しい。

Template Elementを使って画面の初期化を遅らせる

例えば、ページ内に複数のタブがあった場合、隠れている部分の初期化は後回しにしたいという要求がある。

<div id="content-a">
  <template id="tmpl-a">
    ...
    <script src="init-a.js"></script>
  </template>
</div>
<div id="content-b" style="display:none;">
  <template id="tmpl-b">
    ...
    <script src="init-b.js"></script>
  </template>
</div>

表示されるべきコンテンツの初期化が、隠れているコンテンツの初期化にブロックされてしまうのは勿体無い。 そこで、上のように隠れている#tmpl-bのアクティベートのタイミングを遅らせることによって、初期化順序を最適化することが出来る。特にtemplate中のスクリプトが重い処理であるほど有効だ(手元での検証結果)。 もちろん銀の弾丸では無いが、武器のひとつにはなる。

Vulcanizeは早めに検討する

HTML Importsを多用するとどうしてもネットワークコストが高くなるため、JavaScriptと同様にHTMLもひとつに結合したい。 そんな時にPolymerチーム製のVulcanizeというツールが必要になる。

ただしこれには条件があって、静的にURLが宣言されている場合に使用が限られる

つまり、柔軟にURLをあれこれするフレームワークをがっつり構築した後でVulcanizeの導入を検討すると泣くことになる。(しかもエラーメッセージがはっちゃけていて分かりにくい) しかも現状HTML Importsの結合ツールはこれしか無いので、完全に詰んでしまう。結合の可能性は最初から念頭に置いておきたい。

HTML Importのモジュール管理

hrefを工夫する

HTML Importを活用して、例えば、foo.htmlbar.htmlというモジュールを独立に作成して提供したいとする。 ここで、foo.htmlbar.htmlに依存しており、アプリケーションからの利用イメージは次のようであるとする。

Application => foo.html => bar.html

この場合、foo.htmlからbar.htmlへのリンクをどうしたら良いだろうか。 Polymerは次のようにしている

<link rel="import" href="../bar/bar.html"></link>

何をしているかというと、モジュールがフラットに展開されたときに相対パスが通るように記述されている。

│
├── bower_components
│  ├── foo
│  │   └─ foo.html
│  └── bar
│      └─ bar.html

ここでひとつ問題になるのが、Bowerあるいは他のパッケージマネージャがフラットにモジュールを展開することに依存している。

│
├── bower_components
│  ├── foo
│  │   └─ foo.html -- requires bar@1.0.0
│  ├── baz
│  │   └─ baz.html -- requires bar@0.8.0
│  └── bar
│      └─ bar.html

そうすると、フラットにならない例外があった場合や別のパッケージマネージャを使いたい場合に、おそらく困った事態になる。 この辺りもコミュニティが盛り上がらないと議論が進まない気がしているし、自分もあんまり考えたくないからES6モジュールでの提供に逃げたい気持ちでいる。

菱形依存によるスクリプトの重複実行を避ける

こちらは既にちらほら話題に上がっているのだが、例えば、foo.htmlbar.htmlの両方がjQueryに依存していた場合。 具体的には次のように菱形(ダイヤモンド)の依存関係になっている場合、

Application => foo.html => jquery.js
Application => bar.html => jquery.js

普通に双方からscriptタグでjQueryを読み込むと、2度スクリプトが実行されてしまう。 これを避けるためには、jquery.jsをHTMLでラップすると良い。

Application => foo.html => jquery.html => jquery.js
Application => bar.html => jquery.html => jquery.js

こうすることで、jquery.htmlは2度目のアクセス時からは読み込み済みという扱いになり、内部で呼ばれているjquery.jsの実行も1回で済む。

その他

Resource Timing APIが、HTML Importを通じて呼び出されるサブサブリソースの情報を拾わない気がする…。

まとめ

私的見解をぶちまけてみた。まぁどれも結論は出ていない。

もうとにかく使い方のバリエーションが沢山あって、ベストプラクティス欲しい!という感想。(←Polymer使え)

Object.observe() のコールバック実行タイミング

ざっくり分かるスクリプト

下のスクリプトを実行すると、どのようにalertが表示されるでしょう。

var obj = {};
setTimeout(function() {
  alert('D');
});
Object.observe(obj, function() {
  alert(obj.a);
  obj.a = 3;
});
alert('A');
obj.a = 1;
alert('B');
obj.a = 2;
alert('C');

結果(反転) ⇒ A B C 2 3 D
Chrome 36で確認)

即時 > Object.observe > setTimeout
の優先度で実行されるようです。