ジンジャー研究室

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

proxyquireでNode.jsのrequireをモックする

発端

React+Fluxのアプリのテストをするのに、requireのモックが必要だった。 だけどJest使いたくない。

そこでこれを使う。

github.com

簡単な使い方

something.js

module.exports = 'music';

love.js

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

console.log('I love ' + something + '!');

index.js

var proxyquire = require('proxyquire');

require('./love.js');// => I love music!

proxyquire('./love.js', {
  './something.js': 'shit'
});// => I love shit!

require('./something.js');をモックして何でも言わせちゃう。

ユニットテスト用途には十分

requireで指定する文字列の曖昧さを許容しないのがちょっと微妙だけど、ユニットテストに使う分にはまぁ問題にはならない。

呼ばれた回数をカウントしたり色々したい場合は、Sinon.jsを使うのが便利。

DOM要素のidはグローバル変数

DOM: element IDs are global variables

知らなかった…orz

とはいえ普段は使わないほうがよさそう。

既に定義されているグローバル変数と被っていると上書きされない。

  <div id="a">Hello!</div>
  <script>console.log(a);//<div id="a">Hello!</div></script>


  <script>var b;</script>
  <div id="b">Hello!</div>
  <script>console.log(b);//undefined</script>


  <script>c = null;</script>
  <div id="c">Hello!</div>
  <script>console.log(c);//null</script>


  <script>d = undefined;</script>
  <div id="d">Hello!</div>
  <script>console.log(d);//undefined</script>


  <script>e = undefined;</script>
  <script>delete e;</script>
  <div id="e">Hello!</div>
  <script>console.log(e);//<div id="e">Hello!</div></script>

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