yo_waka's blog

418 I'm a teapot

Node塾その2(connectソースコードリーディング)に行ってきた

昨日開催されたNode塾その2でconnectのソースコードリーディングをしてきました。

connectの中身を見るのは初めてだったのですが、id:scalar さん(@hakobera さん)の説明がとても丁寧で理解も進み、後半はひたすらミドルウェアのソースを読んでました。

※バージョンは1.8.1。githubのmasterは2系みたいだけど、expressの安定板が1.8.1なので。


以下メモ。

connectはとても小さいソース群でできてるのでソースコード読むのにぴったり。

connect.js

./middlewares以下のJSファイルを読み込む。
requireされるタイミングはここではなく、アプリコードが実行されるときに遅延ロードされる。
__defineGetter__に突っ込むことで、呼び出し時に初めてrequireする関数が実行されるようになっていた。
クライアントJSでも遅延処理でよく使うやりかた。

fs.readdirSync(__dirname + '/middleware').forEach(function(filename){
  if (/\.js$/.test(filename)) {
    var name = filename.substr(0, filename.lastIndexOf('.'));
    exports.middleware.__defineGetter__(name, function(){
      return require('./middleware/' + name);
    });
  }
});

http.js

expressでおなじみのcreateServer()の実体。
よく見るサンプルだといきなりuseにミドルウェア渡してる例が多いけど、useには第一引数にパスを指定することができる
 -> パスに対してハンドラを指定できる
   というか中ではパスとハンドラのペアがミドルウェアスタックとして登録されている
   (何も指定されてなければ"/"がパスとして使われる)
   つまり、パスを変えて同じmiddlewareを登録することができる
第二引数のhandleにはHTTPサーバも指定できるみたい。プロキシっぽいことができる?
単純に指定された順にpushされてくので、依存関係に注意して順番指定しないと動かないよ。

ミドルウェアの話

仕様はとてもシンプル。
リクエスト(req)、レスポンス(res)、次のミドルウェアを実行する関数(next)を引数に取る関数を返す関数を作ればいいだけ。
エラーハンドリング系のミドルウェアは第一引数がエラー(err)になる。

ミドルウェアはプロバイダとフィルタという2つの概念があるらしい。

  • プロバイダ

 レスポンスを返すもの

  • フィルタ

 リクエストやレスポンスをフィルタリングしたり機能を追加するもの
 フィルタとして使う場合、最後に必ずnext()を呼ぶ必要がある

routerミドルウェア使えば簡易expressが作れちゃう。便利!

ハマりどころ

  • node.jsのHTTPリクエストはすべて小文字

 toLowerCaseしてマッチしたほうが確実

  • テストはmake test

 1.x系ではexpresso使われてるが、多分後継のmochaになると思う。expressは既にmocha。
 TAPで書けるのでCIでも実行しやすそう。


この2時間でミドルウェアを自作できるくらいになってるはずとのこと!

というわけで、行ってきたので簡単なミドルウェアを作ってみる。
connect内部でエラーが起きた場合、コンソールにエラーが出力されるのですが、それをファイルに書き出すミドルウェアを作ってみました。

/**
 * Module dependencies.
 */
var fs = require('fs'),
    path = require('path'),
    util = require('util');


/**
 * Porting console.js in node.js
 * @param {*} f
 * @return {string}
 */
function format(f) {
    var formatRegExp = /%[sdj]/g;
    
    if (typeof f !== 'string') {
        var objects = [];
        for (var i = 0; i < arguments.length; i++) {
            objects.push(util.inspect(arguments[i]));
        }
        return objects.join(' ').split('\\n');
    }

    var i = 1;
    var args = arguments;
    var str = String(f).replace(formatRegExp, function(x) {
        switch (x) {
            case '%s':
                return String(args[i++]);
            case '%d':
                return Number(args[i++]);
            case '%j':
                return JSON.stringify(args[i++]);
            default:
                return x;
        }
    });
    
    for (var len = args.length, x = args[i]; i < len; x = args[++i]) {
        if (x === null || typeof x !== 'object') {
            str += ' ' + x;
        } else {
            str += ' ' + util.inspect(x);
        }
    }
    
    return str.split('\\n');
};


/**
 * @param {string} root
 * @param {string=} opt_filename
 * @return {Function}
 * @api public
 */
exports = module.exports = function errorLog(root, opt_filename) {
    // root required
    if (typeof root !== 'string') {
        throw new Error('errorLog() log path required');
    }
    root = path.normalize(root);
    var filename = opt_filename || 'error.log';

    return function errorLog(req, res, next) {
        console.error = function() {
            var messages = format(arguments);
            var stream = fs.createWriteStream(root + '/' + filename, {'flags': 'a'});
            stream.once('open', function(fd) {
                messages.forEach(function(msg, idx) {
                    if (idx === 0) {
                        msg = '[' + (new Date()).toString() + '] ' + msg;
                    }
                    stream.write(msg + '\n');
                });
            });
        };
        next();
    };
};


connect内では例外が起きたらconsole.errorで出力してるので、console.errorをログファイル(error.log)に書き出す関数として豪快に上書いています。
いつ起きたかの日時も記録するようにしたので結構便利かも。


expressで使う場合はこんな感じ。

var errorLog = require('path/to/connect-errorlog');

app.configure(function(){
  app.use(errorLog(__dirname));
  // use other middlewares
});

connectの中が分かるとexpress並びにnode.jsがもっと楽しくなりますね!