阅读express的感悟
在github上看了半天的源码,也是云里雾里,勉强也算看完了,通过查看很多人的讲解也方便了我的理解,今天记录下来,也算是做个笔记。
进入express的源码文件里我们可以看到8个文件:middleware router application.js express.js request.js response.js utils.js view.js
官网给出一个最简单的例子:
1 var express = require('express'); 2 var app = express(); 3 4 app.get('/', function (req, res) { 5 res.send('Hello World!'); 6 }); 7 8 var server = app.listen(3000, function () { 9 var host = server.address().address; 10 var port = server.address().port; 11 12 console.log('Example app listening at http://%s:%s', host, port); 13 });
让我们看看它背后是怎么实现的:
首先找到入口文件,见名知意,打开express.js,我们看到这样的代码:
1 function createApplication() { 2 //创建app对象 3 var app = function(req, res, next) { 4 app.handle(req, res, next); 5 }; 6 //继承node的事件对象 7 mixin(app, EventEmitter.prototype, false); 8 //继承./application对象 9 mixin(app, proto, false); 10 //app.request和response继承node原生的request和response对象 11 app.request = { __proto__: req, app: app }; 12 app.response = { __proto__: res, app: app }; 13 //初始化app对象 14 app.init(); 15 return app; 16 }
app.init()方法调用的是继承自./application.js的方法。
下面是application.js中的init方法:
1 /** 2 * 初始化服务器 3 * 4 * - 设置默认配置 5 * - 设置默认中间件 6 * - 设置默认映射路由方法 7 * 8 * @private 9 */ 10 11 app.init = function init() { 12 this.cache = {}; 13 this.engines = {}; 14 this.settings = {}; 15 16 this.defaultConfiguration(); 17 };
1 //初始化app的默认配置 2 app.defaultConfiguration = function defaultConfiguration() { 3 var env = process.env.NODE_ENV || 'development'; 4 5 // default settings 6 this.enable('x-powered-by'); 7 this.set('etag', 'weak'); 8 this.set('env', env); 9 this.set('query parser', 'extended'); 10 this.set('subdomain offset', 2); 11 this.set('trust proxy', false); 12 13 // trust proxy inherit back-compat 14 Object.defineProperty(this.settings, trustProxyDefaultSymbol, { 15 configurable: true, 16 value: true 17 }); 18 19 debug('booting in %s mode', env); 20 //监听mount事件,当我们向express中添加中间件的时候会触发mount事件, 21 //这里会将每个中间件的request对象,response对象,engines对象,settings对象通过__proto__形成原型连, 22 //最顶层的request和response对象是Node原生的request和response对象,在createApplication中定义 23 this.on('mount', function onmount(parent) { 24 // inherit trust proxy 25 if (this.settings[trustProxyDefaultSymbol] === true 26 && typeof parent.settings['trust proxy fn'] === 'function') { 27 delete this.settings['trust proxy']; 28 delete this.settings['trust proxy fn']; 29 } 30 31 // inherit protos 32 this.request.__proto__ = parent.request; 33 this.response.__proto__ = parent.response; 34 this.engines.__proto__ = parent.engines; 35 this.settings.__proto__ = parent.settings; 36 }); 37 38 // setup locals 39 this.locals = Object.create(null); 40 41 // top-most app is mounted at / 42 this.mountpath = '/'; 43 44 // default locals 45 this.locals.settings = this.settings; 46 47 // default configuration 48 this.set('view', View); 49 this.set('views', resolve('views')); 50 this.set('jsonp callback name', 'callback'); 51 52 if (env === 'production') { 53 this.enable('view cache'); 54 } 55 56 Object.defineProperty(this, 'router', { 57 get: function() { 58 throw new Error('\'app.router\' is deprecated!\nPlease see the 3.x to 4.x migration guide for details on how to update your app.'); 59 } 60 }); 61 };
express()分析结束。
1 app.get('/', function (req, res) { 2 res.send('Hello World!'); 3 });
该api的作用是创建到"/"的get请求的路由处理器,app.get/post/head等方法在application文件中的下述代码中定义:
1 methods.forEach(function(method) { 2 app[method] = function(path) { 3 if (method === 'get' && arguments.length === 1) { 4 //get方法特殊处理,只有一个参数的时候,获取app.settings[path] 5 return this.set(path); 6 } 7 8 //给app对象绑定一个路由管理器Router 9 this.lazyrouter(); 10 11 //使用路由管理器给指定的path创建一个路由对象处理对象 12 var route = this._router.route(path); 13 //调用路由处理对象的相应方法,即:去除第二个参数,作为处理程序,并传入route[method] 14 route[method].apply(route, slice.call(arguments, 1)); 15 return this; 16 }; 17 });
原来,这些方法都是动态添加的。methods是一个数组,里面存放了一系列web请求方法,以上方法通过对其进行遍历,给app添加了与请求方法同名的一系列方法,即:app.get()、app.post()、app.put()等,
在这些方法中,首先通过调用lazyrouter实例化一个Router对象,然后调用this._router.route方法实例化一个Route对象,最后调用route[method]方法并传入对应的处理程序完成path与handler的关联。
在这个方法中需要注意以下几点:
- lazyrouter方法只会在首次调用时实例化Router对象,然后将其赋值给app._router字段
- 要注意Router与Route的区别,Router可以看作是一个中间件容器,不仅可以存放路由中间件(Route),还可以存放其他中间件,在lazyrouter方法中实例化Router后会首先添加两个中间件:query和init;而Route仅仅是路由中间件,封装了路由信息。Router和Route都各自维护了一个stack数组,该数组就是用来存放中间件和路由的。
这里先声明一下,本文提到的路由容器(Router)代表“router/index.js”文件的到导出对象,路由中间件(Route)代表“router/route.js”文件的导出对象,app代表“application.js”的导出对象。
Router和Route的stack是有差别的,这个差别主要体现在存放的layer(layer是用来封装中间件的一个数据结构)不太一样,
由于Router.stack中存放的中间件包括但不限于路由中间件,而只有路由中间件的执行才会依赖与请求method,因此Router.stack里的layer没有method属性,而是将其动态添加(layer的定义中没有method字段)
到了Route.stack的layer中;layer.route字段也是动态添加的,可以通过该字段来判断中间件是否是路由中间件。
可以通过两种方式添加中间件:app.use和app[method],前者用来添加非路由中间件,后者添加路由中间件,这两种添加方式都在内部调用了Router的相关方法来实现:
1 //添加非路由中间件 2 proto.use = function use(fn) { 3 /* 此处略去部分代码 4 其实我们不关注path是什么,默认是"/",当然他也会通过参数去 5 查找path,但是我们关注的是layer的生成,还有他的.route属性。 6 7 */ 8 callbacks.forEach(function (fn) { 9 if (typeof fn !== 'function') { 10 throw new TypeError('Router.use() requires middleware function but got a ' + gettype(fn)); 11 } 12 // add the middleware 13 debug('use %s %s', path, fn.name || '<anonymous>'); 14 ////实例化layer对象并进行初始化 15 var layer = new Layer(path, { 16 sensitive: this.caseSensitive, 17 strict: false, 18 end: false 19 }, fn); 20 //非路由中间件,该字段赋值为undefined 21 layer.route = undefined; 22 this.stack.push(layer); 23 }, this); 24 return this; 25 }; 26 27 //添加路由中间件 28 proto.route = function(path){ 29 //实例化路由对象 30 var route = new Route(path); 31 //实例化layer对象并进行初始化 32 var layer = new Layer(path, { 33 sensitive: this.caseSensitive, 34 strict: this.strict, 35 end: true 36 }, route.dispatch.bind(route)); 37 //指向刚实例化的路由对象(非常重要),通过该字段将Router和Route关联来起来 38 layer.route = route; 39 this.stack.push(layer); 40 return route; 41 };
对于路由中间件,路由容器中的stack(Router.stack)里面的layer通过route字段指向了路由对象,那么这样一来,Router.stack就和Route.stack发生了关联,关联后的示意模型如下图所示:
在运行过程中,路由容器(Router)只会有一个实例,而路由中间件会在每次调用app.route、app.use或app[method]的时候生成一个路由对象,在添加路由中间件的时候路由容器相当于是一个代理,
Router[method]实际上是在内部调用了Route[method]来实现路由添加的,路由容器中有一个route方法,相当于是路由对象创建工厂。通过添加一个个的中间件,在处理请求的时候会按照添加的顺序逐个调用,
如果遇到路由中间件,会逐个调用该路由对象中stack数组里存放的handler,这就是express的流式处理,是不是有点类似asp.net中的管道模型,调用过程如下图所示:
我们按顺序来看下这3行源码:
this.lazyrouter();
var route = this._router.route(path);
route[method].apply(route, slice.call(arguments, 1));
1 /** 2 * lazily adds the base router if it has not yet been added. 3 * 4 * We cannot add the base router in the defaultConfiguration because 5 * it reads app settings which might be set after that has run. 6 * 7 * @private 8 */ 9 app.lazyrouter = function lazyrouter() { 10 if (!this._router) { 11 this._router = new Router({ 12 caseSensitive: this.enabled('case sensitive routing'), 13 strict: this.enabled('strict routing') 14 }); 15 16 this._router.use(query(this.get('query parser fn'))); 17 this._router.use(middleware.init(this)); 18 } 19 };
那Router是什么? 我们先看下Router模块的构造函数:在./router/index.js文件中:
1 var proto = module.exports = function(options) { 2 var opts = options || {}; 3 4 function router(req, res, next) { 5 router.handle(req, res, next); 6 } 7 8 // mixin Router class functions 9 router.__proto__ = proto; 10 11 router.params = {}; 12 router._params = []; 13 router.caseSensitive = opts.caseSensitive; 14 router.mergeParams = opts.mergeParams; 15 router.strict = opts.strict; 16 router.stack = []; 17 18 return router; 19 };
这里是个知识点,其实这个router就相当于this,我们new出一个对象也就3个部分:
-
创建一个空对象,并且 this 变量引用该对象,同时还继承了该函数的原型。
-
属性和方法被加入到 this 引用的对象中。
-
新创建的对象由 this 所引用,并且最后隐式的返回 this 。
proto是Router的原型对象,上面有很多方法,稍后分析,我们再看
this.lazyrouter();后的
var route = this._router.route(path);
我们来看一下在./router/index.js文件中定义的route方法:
1 proto.route = function route(path) { 2 3 //创建并初始化route对象 4 var route = new Route(path); 5 6 //创建一个layer对象,并放入栈中, 7 //在处理业务的时候会根据path的匹配规则,来匹配对应的route, 8 //如果是使用use接口(比如添加中间件的时候),是不指定route的, 9 var layer = new Layer(path, { 10 sensitive: this.caseSensitive, 11 strict: this.strict, 12 end: true 13 }, route.dispatch.bind(route)); 14 15 layer.route = route; 16 17 this.stack.push(layer); 18 console.log("app.get(path,cb) return route:" + JSON.stringify(route)); 19 return route; 20 };
注意!在route方法里我们用了route模块,注意和Router的区别,稍后总结。我们先去在./router/route.js中看看Route的构造方法:
1 function Route(path) { 2 this.path = path; 3 this.stack = []; 4 5 debug('new %s', path); 6 7 // route handlers for various http methods 8 this.methods = {}; 9 }
不知道大家注意到没有,Router模块初始化有一个stack数组,route模块也有一个stack数组:Router模块的stack是装的Layer,我们去./router/layer.js下看Layer模块:
1 function Layer(path, options, fn) { 2 if (!(this instanceof Layer)) { 3 return new Layer(path, options, fn); 4 } 5 6 debug('new %s', path); 7 var opts = options || {}; 8 9 this.handle = fn; 10 this.name = fn.name || '<anonymous>'; 11 this.params = undefined; 12 this.path = undefined; 13 this.regexp = pathRegexp(path, this.keys = [], opts); 14 15 if (path === '/' && opts.end === false) { 16 this.regexp.fast_slash = true; 17 } 18 }
上边是Layer的构造函数,我们可以看到这里定义handle,params,path和regexp等几个主要的属性:
- 其中最重要的就是handle,它就是我们刚刚在route中创建Layer对象传入的中间件函数。
- params其实就是req.params,至于如何实现的我们可以以后再做探讨,今天先不做说明。
- path就是我们定义路由时传入的path。
- regexp对于Layer来说是比较重要的一个属性,因为下边进行路由匹配的时候就是靠它来搞定的,而它的值是由pathRegexp得来的,其实这个pathRegexp对应的是一个第三方模块path-to-regexp,它的功能是将path转换成regexp,具体用法大家可以自行查看。
上边的代码中在创建Layer对象的时候传入的handle函数为route.dispatch.bind(route),我们来看看route.js中的route.dispatch:
1 Route.prototype.dispatch = function dispatch(req, res, done) { 2 var idx = 0; 3 var stack = this.stack; 4 if (stack.length === 0) { 5 return done(); 6 } 7 8 var method = req.method.toLowerCase(); 9 if (method === 'head' && !this.methods['head']) { 10 method = 'get'; 11 } 12 13 req.route = this; 14 15 next(); 16 17 function next(err) { 18 if (err && err === 'route') { 19 return done(); 20 } 21 22 var layer = stack[idx++]; 23 if (!layer) { 24 return done(err); 25 } 26 27 if (layer.method && layer.method !== method) { 28 return next(err); 29 } 30 31 if (err) { 32 layer.handle_error(err, req, res, next); 33 } else { 34 layer.handle_request(req, res, next); 35 } 36 } 37 };
我们发现dispatch中通过next()获取stack中的每一个layer来执行相应的路由中间件,这样就保证了我们定义在路由上的多个中间件函数被按照定义的顺序依次执行。到这里我们已经知道了单个路由是被如何执行的,那我们定义的多个路由之间又是如何被依次执行的呢,现在我们来看看index.js中的handle函数:
1 proto.handle = function handle(req, res, out) { 2 var self = this; 3 4 debug('dispatching %s %s', req.method, req.url); 5 6 var search = 1 + req.url.indexOf('?'); 7 var pathlength = search ? search - 1 : req.url.length; 8 var fqdn = req.url[0] !== '/' && 1 + req.url.substr(0, pathlength).indexOf('://'); 9 var protohost = fqdn ? req.url.substr(0, req.url.indexOf('/', 2 + fqdn)) : ''; 10 var idx = 0; 11 var removed = ''; 12 var slashAdded = false; 13 var paramcalled = {}; 14 15 // store options for OPTIONS request 16 // only used if OPTIONS request 17 var options = []; 18 19 // middleware and routes 20 var stack = self.stack; 21 22 // manage inter-router variables 23 var parentParams = req.params; 24 var parentUrl = req.baseUrl || ''; 25 var done = restore(out, req, 'baseUrl', 'next', 'params'); 26 27 // setup next layer 28 req.next = next; 29 30 // for options requests, respond with a default if nothing else responds 31 if (req.method === 'OPTIONS') { 32 done = wrap(done, function(old, err) { 33 if (err || options.length === 0) return old(err); 34 sendOptionsResponse(res, options, old); 35 }); 36 } 37 38 // setup basic req values 39 req.baseUrl = parentUrl; 40 req.originalUrl = req.originalUrl || req.url; 41 42 next(); 43 44 function next(err) { 45 var layerError = err === 'route' 46 ? null 47 : err; 48 49 // remove added slash 50 if (slashAdded) { 51 req.url = req.url.substr(1); 52 slashAdded = false; 53 } 54 55 // restore altered req.url 56 if (removed.length !== 0) { 57 req.baseUrl = parentUrl; 58 req.url = protohost + removed + req.url.substr(protohost.length); 59 removed = ''; 60 } 61 62 // no more matching layers 63 if (idx >= stack.length) { 64 setImmediate(done, layerError); 65 return; 66 } 67 68 // get pathname of request 69 var path = getPathname(req); 70 71 if (path == null) { 72 return done(layerError); 73 } 74 75 // find next matching layer 76 var layer; 77 var match; 78 var route; 79 80 while (match !== true && idx < stack.length) { 81 layer = stack[idx++]; 82 match = matchLayer(layer, path); 83 route = layer.route; 84 85 if (typeof match !== 'boolean') { 86 // hold on to layerError 87 layerError = layerError || match; 88 } 89 90 if (match !== true) { 91 continue; 92 } 93 94 if (!route) { 95 // process non-route handlers normally 96 continue; 97 } 98 99 if (layerError) { 100 // routes do not match with a pending error 101 match = false; 102 continue; 103 } 104 105 var method = req.method; 106 var has_method = route._handles_method(method); 107 108 // build up automatic options response 109 if (!has_method && method === 'OPTIONS') { 110 appendMethods(options, route._options()); 111 } 112 113 // don't even bother matching route 114 if (!has_method && method !== 'HEAD') { 115 match = false; 116 continue; 117 } 118 } 119 120 // no match 121 if (match !== true) { 122 return done(layerError); 123 } 124 125 // store route for dispatch on change 126 if (route) { 127 req.route = route; 128 } 129 130 // Capture one-time layer values 131 req.params = self.mergeParams 132 ? mergeParams(layer.params, parentParams) 133 : layer.params; 134 var layerPath = layer.path; 135 136 // this should be done for the layer 137 self.process_params(layer, paramcalled, req, res, function (err) { 138 if (err) { 139 return next(layerError || err); 140 } 141 142 if (route) { 143 return layer.handle_request(req, res, next); 144 } 145 146 trim_prefix(layer, layerError, layerPath, path); 147 }); 148 }
从上边的代码我们可以看出,这里也是利用next(),来处理stack中的每一个Layer,这里的stack是Router的stack,stack中存贮了多个route对应的layer,获取到每个layer对象之后,用请求的path与layer进行匹配,此处匹配用的是layer.match,如果能匹配到对应的layer,则获得layer.route,如果route不为空则执行对应的layer.handle_request(),如果route为空说明这个layer是通过use()添加的非路由中间件,需要特别说明的是,如果通过use()添加的非路由中间件没有指定path,则会在layer.match中默认返回true,也就是说,没有指定path的非路由中间件会匹配所有的http请求。
回过头来看看route模块里的stack是存的什么:
1 methods.forEach(function(method){ 2 Route.prototype[method] = function(){ 3 var handles = flatten(slice.call(arguments)); 4 5 for (var i = 0; i < handles.length; i++) { 6 var handle = handles[i]; 7 8 if (typeof handle !== 'function') { 9 var type = toString.call(handle); 10 var msg = 'Route.' + method + '() requires callback functions but got a ' + type; 11 throw new Error(msg); 12 } 13 14 debug('%s %s', method, this.path); 15 16 var layer = Layer('/', {}, handle); 17 layer.method = method; 18 19 this.methods[method] = true; 20 this.stack.push(layer); 21 } 22 23 return this; 24 }; 25 });
我们看到他这个layer其实都是默认"/",我们可以当他是用.use方法加的中间件。
在本节的最后一起来看下app.listen的实现(application.js中):
1 app.listen = function listen() { 2 var server = http.createServer(this); 3 return server.listen.apply(server, arguments); 4 };
其中的this指的是app对象,在前面express()中已经分析过app的原型了,其中app的构造函数就是:
1 var app = function(req, res, next) { 2 app.handle(req, res, next); 3 };
所以app.listen翻译下其实就是我们常见的下述写法:
1 app.listen = function listen() { 2 var server = http.createServer(function(req,res,next){ 3 app.handle(req,res,next) 4 }); 5 return server.listen(arguments); 6 };
app.handle的源码在./application.js中:
1 app.handle = function handle(req, res, callback) { 2 var router = this._router; 3 4 // final handler 5 var done = callback || finalhandler(req, res, { 6 env: this.get('env'), 7 onerror: logerror.bind(this) 8 }); 9 10 // no routes 11 if (!router) { 12 debug('no routes defined on app'); 13 done(); 14 return; 15 } 16 17 router.handle(req, res, done); 18 };
该方法将监听到的用户请求转入router中处理,即第一阶段处理。
上面分析了express启动加载的过程,重点是对router和route的管理。
以下面的用户请求为例:
1 app.get("/a/b", function(req, res) { 2 res.end("hello,world"); 3 });
router中按照用户定义的中间件和请求映射顺序注册处理栈,下面是一个完成初始化的处理栈截图:
该栈中有6个layer,按照用户初始化顺序添加到数组中(前两个是express默认添加的),其中第三个layer用来处理/a/b的请求。
比如当用户在发送/a/b的请求的时候,在router中的layer栈中做循环处理,依次判断路径是否匹配,route是否为空,直到找到匹配的layer为止.
该layer的定义如下:
1 proto.route = function route(path) { 2 console.log("app.get(path,cb) path value:" + JSON.stringify(path)); 3 var route = new Route(path); 4 5 var layer = new Layer(path, { 6 sensitive: this.caseSensitive, 7 strict: this.strict, 8 end: true 9 }, route.dispatch.bind(route));//指定该layer的处理函数dispatch 10 11 layer.route = route;//指定该layer对应的二级处理路由器 12 13 this.stack.push(layer); 14 console.log("app.get(path,cb) return route:" + JSON.stringify(route)); 15 return route; 16 };
然后调用layer的handle_request的方法
1 Layer.prototype.handle_request = function handle(req, res, next) { 2 var fn = this.handle; 3 4 if (fn.length > 3) { 5 // not a standard request handler 6 return next(); 7 } 8 9 try { 10 fn(req, res, next); 11 } catch (err) { 12 next(err); 13 } 14 };
其中的this.handler即在定义layer的时候指定的route的dispatch方法,调用fn方法即调用dispatch方法。
1 Route.prototype.dispatch = function dispatch(req, res, done) { 2 var idx = 0; 3 var stack = this.stack; 4 if (stack.length === 0) { 5 return done(); 6 } 7 8 var method = req.method.toLowerCase(); 9 if (method === 'head' && !this.methods['head']) { 10 method = 'get'; 11 } 12 13 req.route = this; 14 15 next(); 16 17 function next(err) { 18 if (err && err === 'route') { 19 return done(); 20 } 21 22 var layer = stack[idx++]; 23 if (!layer) { 24 return done(err); 25 } 26 27 if (layer.method && layer.method !== method) { 28 return next(err); 29 } 30 31 if (err) { 32 layer.handle_error(err, req, res, next); 33 } else { 34 layer.handle_request(req, res, next); 35 } 36 } 37 };
这里将用户的请求导入到了route的layer栈中(该栈在application.js中和router中的栈一同完成初始化)
1 var route = this._router.route(path);//注册router的layer栈 2 route[method].apply(route, slice.call(arguments, 1));//注入route的layer栈,该栈中的处理函数即用户定义的回调函数
同router的栈处理顺序类似,不同的是这里的layer.handle_request最终调用的将是用户定义的回调函数,而不再是dispach函数。
最后:我还是想说一下,我们的路由机制!其实是通过内部的next函数,进行递归的,他其实有2个next函数,一个定义在proto.handle里,这是处理中间件的,另外一个在dispatch里,是用来处理路由的。具体大家可以看下这篇文章,反正讲的很明白了