express 是 nodejs 中最流行的 web 框架。express 中对 http 中的 request 和 response 的处理,还有以中间件为核心的处理流程,非常灵活,足以应对任何业务的需求。
而 connect 曾经是 express 3.x 之前的核心,而 express 4.x 已经把 connect 移除,在 express 中自己实现了 connect 的接口。可以说 connect 造就了 express 的灵活性。
因此,我很好奇,connect 是怎么写的。
争取把每一行代码都弄懂。
connect 解析
我们要先从 connect 的官方例子开始
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | var connect = require('connect'); var http = require('http'); var app = connect(); // gzip/deflate outgoing responses var compression = require('compression'); app.use(compression()); // store session state in browser cookie var cookieSession = require('cookie-session'); app.use(cookieSession({ keys: ['secret1', 'secret2'] })); // parse urlencoded request bodies into req.body var bodyParser = require('body-parser'); app.use(bodyParser.urlencoded({extended: false})); // respond to all requests app.use(function(req, res){ res.end('Hello from Connect!\n'); }); //create node.js http server and listen on port http.createServer(app).listen(3000); |
从示例中可以看到一个典型的 connect 的使用:
1 2 3 4 5 6 7 8 | var app = connect()// 初始化 app.use(function(req, res, next) { // do something }) // http 服务器,使用 http.createServer(app).listen(3000); |
先倒着看,从调用的地方更能看出来,模块怎么使用的。我们就先从 http.createServer(app)来看看。
从 nodejs doc 的官方文档中可以知, createServer 函数的参数是一个回调函数,这个回调函数是用来响应 request 事件的。从这里看出,示例代码中 app 中函数签就是 (req, res),也就是说 app 的接口为 function (req, res)。
但是从示例代码中,我们也可以看出 app 还有一个 use 方法。是不是觉得很奇怪,js 中函数实例上,还以带方法,这在 js 中就叫 函数对象,不仅能调用,还可以带实例变量。给个例子可以看得更清楚:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | function handle () { function app(req, res, next) { app.handle(req, res, next)} app.handle = function (req, res, next) { console.log(this); } app.statck = []; return app; } var app = handle(); app() // ==> { [Function: app] handle: [Function], stack: [] } app.apply({}) // ==>{ [Function: app] handle: [Function], stack: [] } |
可以看出:函数中的实例函数中的 this 就是指当前的实例,不会因为你使用 apply 进行环境改变。
其他就跟对象没有什么区别。
再次回到示例代码,因该可以看懂了, connect 方法返回了一个函数,这个函数能直接调用,有 use 方法,用来响应 http 的 request 事件。
到此为此,示例代码就讲完了。 我们开始进入到 connect 模块的内部。
connect 只有一个导出方法。就是如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | var merge = require('utils-merge'); module.exports = createServer; var proto = {}; function createServer() { // 函数对象,这个对象能调用,能加属性 function app(req, res, next){ app.handle(req, res, next); } merge(app, proto); // ===等于调用 Object.assign merge(app, EventEmitter.prototype); // === 等于调用 Object.assign app.route = '/'; app.stack = []; return app; } |
从代码中可以看出,createServer 函数把 app 函数返回了,app 函数有三个参数,多了一个 next (这个后面讲),app函数把 proto 的方法合并了。还有 EventEmitter 的方法也合并了,还增加了 route 和 stack 的属性。
从前面代码来看,响应 request 的事件的函数,是 app.handle 方法。这个方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | proto.handle = function handle(req, res, out) { var index = 0; var protohost = getProtohost(req.url) || ''; //获得 http://www.baidu.com var removed = ''; var slashAdded = false; var stack = this.stack; // final function handler var done = out || finalhandler(req, res, { env: env, onerror: logerror }); // 接口 done(err); // 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); // 没有中间件,调用 finalhandler 进行处理,如果 err 有值,就返回 404 进行处理 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[route.length]; if (c !== undefined && '/' !== 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 方法就是一个递归调用,不断的对比 route 是否匹配,如果匹配则调用 handle, 如果不匹配,则调用下一个 handle.
call 函数的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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); } |
可以看出一个重点:对错误处理,connect 的要求 是函数必须是 四个参数,而 express 也是如此。如果有错误, 中间件没有一个参数的个数是 4, 就会错误一直传下去,直到后面的 defer(done, err); 进行处理。
还有 app.use 添加中间件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | proto.use = function use(route, fn) { var handle = fn; // fn 只是一个函数的话 三种接口 // 1. err, req, res, next 2. req, res, 3, req, res, next 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) { // req, res, next 中间件 server.handle(req, res, next); }; } // wrap vanilla http.Servers if (handle instanceof http.Server) { handle = handle.listeners('request')[0]; // (req, res) // 最后的函数 } // 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; }; |
从代码中,可以看出,use 方法添加中间件到 this.stack 中,其中 fn 中间件的形式有两种: function (req, res, next) 和 handle.handle(req, res, next) 这两种都可以。还有对 fn 情况进行特殊处理。
总的处理流程就是这样,用 use 方法添加中间件,用 next 编历中间件,用 finalHandle 进行最后的处理工作。
在代码中还有一个函数非常奇怪:
1 2 3 4 | /* istanbul ignore next */ var defer = typeof setImmediate === 'function' ? setImmediate : function(fn){ process.nextTick(fn.bind.apply(fn, arguments)) } |
defer 函数中的 fn.bind.apply(fn, arguments),这个方法主要解决了,一个问题,不定参的情况下,第一个参数函数,怎样拿到的问题,为什么这样说呢?如果中我们要达到以上的效果,需要多多少行代码?
1 2 3 4 5 6 7 | function () { var cb = Array.from(arguments)[0]; var args = Array.from(arguments).splice(1); process.nextTick(function() { cb.apply(null,args); }) } |
这还是 connect 兼容以前的 es5 之类的方法。如果在 es6 下面,方法可以再次简化
1 | function(..args){ process.nextTick(fn.bind(...args)) } |