expressコードリーディング その2 connect/http.jsを読む
前回に引き続きconnect.jsを読む。今回はconnect/http.js。ここがconnectのコア。
まず最初にコンストラクタ。ここでhttp.Serverを継承してる。んでhttp.Server.call(this, this.handle)ってやってるのでリクエストハンドラをthis.handleにしてる。リクエストは全部このthis.handleで処理されるということになる。
var Server = exports.Server = function HTTPServer(middleware) { this.stack = []; middleware.forEach(function(fn){ this.use(fn); }, this); http.Server.call(this, this.handle); }; /** * Inherit from `http.Server.prototype`. */ Server.prototype.__proto__ = http.Server.prototype;
んで次にServer.prototype.use。これは受け取ったハンドラをstackにpushするだけ。引数のタイプとかでちょいちょい何かやってるけど基本それだけ。大事なのはハンドラ(とroute)をstackにpushするってこと。
Server.prototype.use = function(route, handle){ this.route = '/'; // default route to '/' if ('string' != typeof route) { handle = route; route = '/'; } // wrap sub-apps if ('function' == typeof handle.handle) { var server = handle; server.route = route; handle = function(req, res, next) { server.handle(req, res, next); }; } // wrap vanilla http.Servers if (handle instanceof http.Server) { handle = handle.listeners('request')[0]; } // normalize route to not trail with slash if ('/' == route[route.length - 1]) { route = route.substr(0, route.length - 1); } // add the middleware this.stack.push({ route: route, handle: handle }); // allow chaining return this; };
ここの引数のhandleってのは普通にリクエストハンドラで、res、req、nextの引数を受け取る。nextが呼ばれると次のハンドラを順々に呼んでいくっていうのがconnectの仕組み。
routeっていうのはベースとなるパスを設定できる。
var connect = require('connect'); connect.createServer() .use('/foo', function(req, res, next) { res.write('request: ' + req.method + ' ' + req.url); res.end(); }) .listen(3000);
こう書いたら、http://localhost:3000/foo/* のみでハンドラが実行される。http://localhost:3000/foo/barの結果は「request: GET /bar」になる(req.urlが置き換わってる)。
んで最後にServer.prototype.handle。これは全体のリクエストハンドラで、リクエストがきたらとりあえずこいつが実行される。useでpushされたstackを順々に実行していってnextの処理とかエラー処理とかrouteの処理とかをうまいことやってくれる。
Server.prototype.handle = function(req, res, out) { var writeHead = res.writeHead , stack = this.stack , removed = '' , index = 0; function next(err) { var layer, path, c; req.url = removed + req.url; req.originalUrl = req.originalUrl || req.url; removed = ''; layer = stack[index++]; // all done if (!layer) { // but wait! we have a parent if (out) return out(err); // otherwise send a proper error message to the browser. if (err) { var msg = 'production' == env ? 'Internal Server Error' : err.stack || err.toString(); // output to stderr in a non-test env if ('test' != env) console.error(err.stack || err.toString()); res.statusCode = 500; res.setHeader('Content-Type', 'text/plain'); res.end(msg); } else { res.statusCode = 404; res.setHeader('Content-Type', 'text/plain'); res.end('Cannot ' + req.method + ' ' + req.url); } return; } try { path = parse(req.url).pathname; if (undefined == path) path = '/'; // skip this layer if the route doesn't match. if (0 != path.indexOf(layer.route)) return next(err); c = path[layer.route.length]; if (c && '/' != c && '.' != c) return next(err); // Call the layer handler // Trim off the part of the url that matches the route removed = layer.route; req.url = req.url.substr(removed.length); // Ensure leading slash if ('/' != req.url[0]) req.url = '/' + req.url; var arity = layer.handle.length; if (err) { if (arity === 4) { layer.handle(err, req, res, next); } else { next(err); } } else if (arity < 4) { layer.handle(req, res, next); } else { next(); } } catch (e) { if (e instanceof assert.AssertionError) { console.error(e.stack + '\n'); next(e); } else { next(e); } } } next(); };
connectのコアなコードはこれくらい。そんなに難しくない。同梱されてるmiddlewareがけっこうたくさんあるのでそのコード見るとどういう感じでmiddleware書けばいいかわかりそう。