Connectとは
Connect is a middleware layer for Node.js
https://github.com/senchalabs/connect
Connectは,Node.jsのWebアプリケーションにおいて,ミドルウェアレイヤーを提供するライブラリです.
1つのJavaScriptファイルのみで構成されたシンプルなライブラリで,MITライセンスで公開されています.
今回は,その実装を読んでいきます.
Connectの使い方
基本的な使い方は以下のようになります.
const connect = require('connect');
const app = connect();
app.use((req, res, next) => {
console.log('common middleware');
next();
});
app.use('/greet', (req, res, next) => res.end('Hello!'));
app.listen(3000);
http://localhost:3000/greet
にアクセスすると "Hello!" と表示されるだけの実装です.
このコードの裏で,Connectが何を行っているのか確認していきます.
Connectソースコードリーディング
アプリケーションオブジェクトの生成
まずはこの行です.
const app = connect();
これにより,アプリケーションオブジェクトが生成されます.
実際のコードは以下のようになっています.
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;
}
proto
オブジェクトは,ミドルウェアの登録やサーバの起動等の実装を含んでいます.
つまり,以下のようなことが把握できます.
- サーバの起動やミドルウェアの登録はアプリケーションオブジェクトを通して行う
-
EventEmitter
を継承しているため,任意のイベントを発火することができる
ミドルウェアの登録
次は,ミドルウェアの登録処理について見ていきます.
サンプルコードでは,以下の行で行っていました.
app.use((req, res, next) => {
console.log('common middleware');
next();
});
app.use('/greet', (req, res, next) => res.end('Hello!'));
先程,アプリケーションオブジェクトを読んだ時に,proto
オブジェクトを通してミドルウェアの登録を行う,ということを把握していました.
つまり,app.use
は実際にはproto.use
で定義されています.
実際のコードは以下のようになっています.
proto.use = function use(route, fn) {
var handle = fn;
var path = route;
// default route to '/'
if (typeof route !== 'string') {
handle = route;
path = '/';
}
// wrap sub-apps
if (typeof handle.handle === 'function') {
var server = handle;
server.route = path;
handle = function (req, res, next) {
server.handle(req, res, next);
};
}
// wrap vanilla http.Servers
if (handle instanceof http.Server) {
handle = handle.listeners('request')[0];
}
// strip trailing slash
if (path[path.length - 1] === '/') {
path = path.slice(0, -1);
}
// add the middleware
debug('use %s %s', path || '/', handle.name || 'anonymous');
this.stack.push({ route: path, handle: handle });
return this;
};
proto.use
は route
とfn
を引数に取る関数です.
route
にはパスを指定しておきます.リクエストされたパスとミドルウェアのパスが一致すれば,fn
に渡された関数が実行されるようになっています.
fn
はreq
,res
, next
を引数に取る関数,アプリケーションオブジェクトまたはhttp.Server
オブジェクトです.
なお,引数が一つしか渡されなかった場合,route
が/
に設定され,fn
に渡された関数が設定されます.
この場合,ミドルウェアは常時実行されるようになります.
fn
にアプリケーションオブジェクトが渡された場合は,そのアプリケーションオブジェクトの処理を実行するようになっています.
つまり,以下のようなコードを書くと,/sub/greet
にリクエストがあった際はsubApp
に処理が渡り,
'Hello, sub-app'がレスポンスとして返されます.
const rootApp = connect();
const subApp = connect();
rootApp.use('/sub', subApp);
subApp.use('/greet', (req, res, next) => res.end('Hello, sub-app'));
http.Server
オブジェクトが渡された場合,request
イベントのリスナーのうち,最初のものが呼ばれます.
なお,http.createServer
に渡した関数は,内部的にrequest
イベントのリスナーとして登録されています.
最後に,末尾のスラッシュを除去し,ミドルウェアスタックに登録されます.
実際にミドルウェアスタックに登録されるオブジェクトは,
{route: '/', 'handle': fn}
のようになります.
リクエストのハンドル
さて,最後の行です.
js
app.listen(3000);
実装は以下のとおりです.
proto.listen = function listen() {
var server = http.createServer(this);
return server.listen.apply(server, arguments);
};
アプリケーションオブジェクトはcreateServer
に渡されるので,最終的にrequest
イベントのリスナーとして設定されます.
connect()
で返されるアプリケーションオブジェクトには以下のように関数が代入されています.
function app(req, res, next){ app.handle(req, res, next); }
アプリケーションオブジェクトの生成
で見たとおりです.
リクエストがあった際に,この関数が呼ばれ,app.handle
が実行されます.
このとき,ミドルウェアスタックに登録されたミドルウェアが探索され,リクエストにマッチするミドルウェアが実行されることになります.
では,app.handle
の実装を追っていきます.
実装は以下のようになっています.
proto.handle = function handle(req, res, out) {
var index = 0;
var protohost = getProtohost(req.url) || '';
var removed = '';
var slashAdded = false;
var stack = this.stack;
// final function handler
var done = out || finalhandler(req, res, {
env: env,
onerror: logerror
});
// store the original URL
req.originalUrl = req.originalUrl || req.url;
function next(err) {
if (slashAdded) {
req.url = req.url.substr(1);
slashAdded = false;
}
if (removed.length !== 0) {
req.url = protohost + removed + req.url.substr(protohost.length);
removed = '';
}
// 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;
// skip this layer if the route doesn't match
if (path.toLowerCase().substr(0, route.length) !== route.toLowerCase()) {
return next(err);
}
// skip if route match does not border "/", ".", or end
var c = path.length > route.length && path[route.length];
if (c && c !== '/' && c !== '.') {
return next(err);
}
// trim off the part of the url that matches the route
if (route.length !== 0 && route !== '/') {
removed = route;
req.url = protohost + req.url.substr(protohost.length + removed.length);
// ensure leading slash
if (!protohost && req.url[0] !== '/') {
req.url = '/' + req.url;
slashAdded = true;
}
}
// call the layer handle
call(layer.handle, route, err, req, res, next);
}
next();
};
いくつかの変数初期化処理と,next
関数の定義を読み飛ばすと,最後にnext
関数を引数なしで実行していることがわかります.
つまり,リクエストを受信すると,next
関数が実行されることになります.
next
関数では,まずいくつかのチェックを行い(現段階では読み飛ばします),layer
にミドルウェアを代入します.
index
は最初0で初期化されていることから,登録された順にミドルウェアが読み出されることがわかります.
次に,ミドルウェアがリクエストされたパスにマッチするかどうかをチェックしています.
if (path.toLowerCase().substr(0, route.length) !== route.toLowerCase())
path
はリクエストされたパス,route
はミドルウェアのパスなので,この条件分岐は「リクエストされたパスが,ミドルウェアのパスを含んでいるかどうか」と読むことができます.
いくつか例を挙げます.
ミドルウェア | リクエスト | マッチ? |
---|---|---|
/app | /app | ○ |
/app | /app/path | ○ |
しかし,以下のような場合もあります.
ミドルウェア | リクエスト |
---|---|
/app/path | /app/path.json |
/app | /apple |
/app
と/apple
は,一般的にはマッチしないのが望ましいでしょう.
この処理を行っているのが次の分岐です.
var c = path.length > route.length && path[route.length];
if (c && c !== '/' && c !== '.') {
return next(err);
}
つまり,最後の文字が.
でも/
でもなければ,マッチしない判定となります.
よって,先程の例は以下のようになります.
ミドルウェア | リクエスト | マッチ? |
---|---|---|
/app/path | /app/path.json | ○ |
/app | /apple | ☓ |
これでミドルウェアがマッチしているかどうかの判定が完了しました.
次に,リクエストされたURLから,ミドルウェアのパスを取り除きます.
こうして取り除かれたパスがreq.url
に渡されます.
ここでもいくつか例を挙げておきます.
ミドルウェア | リクエスト | req.url |
---|---|---|
/app/path | /app/path.json | /.json |
/app | /app/path | /path |
なお,req.url
にセットされるURLは必ず / から始まります.
if (route.length !== 0 && route !== '/') {
removed = route;
req.url = protohost + req.url.substr(protohost.length + removed.length);
// ensure leading slash
if (!protohost && req.url[0] !== '/') {
req.url = '/' + req.url;
slashAdded = true;
}
}
ミドルウェアの実行
パスの整形が完了したら,実際にミドルウェアが呼び出されます.
call(layer.handle, route, err, req, res, next);
call
関数の実装は以下のようになっています.
引数には,
* ミドルウェア関数
* ミドルウェアのパス
* エラーオブジェクト
* リクエストオブジェクト
* レスポンスオブジェクト
* next
関数
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);
}
arity
は,ミドルウェア関数の引数の数です.
err
がtruthlyで,ミドルウェア関数が引数を4つ取る場合,またはエラーがなく,ミドルウェア関数が4つ未満の引数を持つ場合はそのまま呼び出します.
ミドルウェア関数が引数を4つ持つ場合について,ミドルウェア関数は,req, res, nextに加え,errを引数に取ることで,エラー処理用のミドルウェアを定義することが出来ます.
function (err, req, res, next) ...
なお,ミドルウェア実行時に同期的に例外が発生した場合は,その例外をキャッチし,next
関数に渡します.
つまり,エラーが発生したミドルウェア以降に,エラー処理用のミドルウェアが登録されていれば,そのミドルウェアが実行されることになります.
まとめ
Connectでは,このようにミドルウェアスタックを探索していくことで,リクエストをハンドルする仕組みになっていました.
express.jsなどのもう少し高レベルなWebフレームワークでは,ルートパラメータや正規表現等,もう少し高度なルーティング機能を提供しており,実際にはそちらの方を利用することになると思います.