Node.jsのconnectをコードリーディングしてみる
connectは、Node.jsのHTTPサーバーにミドルウェアの機能を追加するためのライブラリです。
ここでのミドルウェアは、リクエストの処理の前や後に共通処理を実行するもののことです。ミドルウェアで実行する処理には、セッションの有効化やログ出力などがあります。
connectはexpressの3系まで使われていましたが、4系になったときにexpressから削除されたようです。今回はconnectの3.7.0を読みました。senchalabs/connect at 3.7.0
まずはconnectを使ってみる
リクエストが来たらHello, world
を返すHTTPサーバーを作成しました。ログを出力するミドルウェアを2つと、レスポンスに書き込むミドルウェアの合計3つを追加しています。
const connect = require("connect")
const app = connect();
app.use(function(req, res, next) {
console.log('middleware 1');
next();
});
app.use(function(req, res, next) {
console.log('middleware 2');
next();
});
app.use(function(req, res) {
res.end("Hello, world!")
})
app.listen(3000);
$ node playground/index.js
# 別の端末でcurlを実行すると以下が表示される
middleware 1
middleware 2
$ curl localhost:3000
Hello, world!
読んでみる
エントリーポイント
connectはindex.js
のみの単一ファイルのライブラリです。require
で読み込まれるのは、index.js
でエクスポートしているcreateServer
です。
createServer
では、app
という関数を定義した後、app
にいくつかのプロパティを追加して返却しています。
// 関係なさそうなところは省略
module.exports = createServer;
var finalhandler = require('finalhandler');
var http = require('http');
var merge = require('utils-merge');
var proto = {};
function createServer() {
function app(req, res, next){ app.handle(req, res, next); }
merge(app, proto);
merge(app, EventEmitter.prototype);
app.route = '/';
app.stack = [];
return app;
}
app.handle
のhandle
は、後ろのコードでproto
に追加されていて、それがmerge(app, proto)
でapp
に追加されています。「app
の中でapp
を参照できるのか?」と思いましたが、再帰関数を書くときもそうすることを思い出して納得しました。
app.use(fn)
app.use
は、渡したミドルウェアをstack
という配列に追加します。
proto.use = function use(route, fn) {
var handle = fn;
var path = route;
// default route to '/'
if (typeof route !== 'string') {
handle = route;
path = '/';
}
// 省略
// add the middleware
debug('use %s %s', path || '/', handle.name || 'anonymous');
this.stack.push({ route: path, handle: handle });
return this;
};
use
では第一引数にパスを指定できますが、あまり使わなさそうな機能だったので今回は読み飛ばしました。
app.listen([...])
httpモジュールを使ってHTTPサーバーを作成して、listen
で接続待ちしています。
proto.listen = function listen() {
var server = http.createServer(this);
return server.listen.apply(server, arguments);
};
http.createServer
には、requestイベントに紐付けられるリスナーを渡します。thisは呼び出された関数が属するオブジェクト(実行コンテキスト)を表すので、この場合はapp
関数のことです。
次の行は見慣れない処理です。ここは、app.listen
に渡した引数を、そのままserver.listen
に渡しています。server.listen(arguments)
と書くのとほぼ同じですが、可変長引数で渡すためにFunction.prototype.apply
が使われています。
app
関数の中ではapp.handle
が実行されているので、次はapp.handle
を読みます。
app.handle
ここがconnectのメインの処理です。handle
では、ミドルウェアを順番に実行します。finalhandlerは、エラーの内容に応じたエラーレスポンスを返すためのライブラリです。
// パスに関するところは省略
proto.handle = function handle(req, res, out) {
var index = 0;
var stack = this.stack;
// final function handler
var done = out || finalhandler(req, res, {
env: env,
onerror: logerror
});
function next(err) {
// next callback
var layer = stack[index++];
// all done
if (!layer) {
defer(done, err);
return;
}
// route data
var path = parseUrl(req).pathname || '/';
var route = layer.route;
// call the layer handle
call(layer.handle, route, err, req, res, next);
}
next();
};
handle
の中ではnext
という、ミドルウェアの呼び出してポインタを1つ進める関数を定義しています。next
はcall
に渡され、さらにcall
からミドルウェアに渡されます。ミドルウェアの中でnext
を実行するので、ちょっと分かりにくいですがこれは再帰関数になっています。
call
call
はミドルウェアの実行を担当する関数です。Function.prototype.length
で実引数の個数を取得し、4個かそれ未満かどうかで分岐しています。引数が4個のミドルウェアは、エラーミドルウェアという、エラーハンドリングを担当する特別なミドルウェアです。
function call(handle, route, err, req, res, next) {
var arity = handle.length;
var error = err;
var hasError = Boolean(err);
debug('%s %s : %s', handle.name || '<anonymous>', route, req.originalUrl);
try {
if (hasError && arity === 4) {
// error-handling middleware
handle(err, req, res, next);
return;
} else if (!hasError && arity < 4) {
// request-handling middleware
handle(req, res, next);
return;
}
} catch (e) {
// replace the error
error = e;
}
// continue
next(error);
}
ミドルウェアで例外が発生しない場合は、app.handle()
→next()
→1つ目のミドルウェアが実行→1つ目の中でnext()
→2つ目のミドルウェアが実行→2つ目の中でnext()
→…というように、通常のミドルウェアがすべて実行されます。この場合は、hasError
がfalse
のためエラーミドルウェアは実行されません。
□が普通のミドルウェアで、■がエラーミドルウェアです。
ミドルウェアの実行中に例外が発生した場合は、next(error)
が実行され、一番近くにあるエラーミドルウェアが実行されます。ミドルウェアの実行が続くかどうかは、エラーミドルウェアの中でnext()
を実行するかによります。
エラーミドルウェアでは、後続のエラーミドルウェアに引き継ぐためにnext(error)
を実行するか、next
を実行しないかのどちらかになると思います。
まとめ
- connectは、HTTPサーバーにミドルウェアの機能を提供するためのライブラリ
-
app.use
で渡したミドルウェアは、順番に配列の末尾に追加される - ミドルウェアの引数の
next
は、自身以降のミドルウェアを再帰的に呼び出す関数- つまり、
next()
以降に書いた処理は、自身以降のミドルウェアの実行が完了してから実行される
- つまり、
- 引数が4個のミドルウェアは、ミドルウェアの実行中に例外が発生したときだけ呼び出される特別なミドルウェア
おまけ
next
をミドルウェアの引数に渡すところが理解しづらかったので、その部分だけ自分で書いてみました。
Discussion