阅读express的感悟
在github上看了半天的源码,也是云里雾里,勉强也算看完了,通过查看很多人的讲解也方便了我的理解,今天记录下来,也算是做个笔记。
进入express的源码文件里我们可以看到8个文件:middleware router application.js express.js request.js response.js utils.js view.js
官网给出一个最简单的例子:
var express = require('express');
var app = express(); app.get('/', function (req, res) {
res.send('Hello World!');
}); var server = app.listen(3000, function () {
var host = server.address().address;
var port = server.address().port; console.log('Example app listening at http://%s:%s', host, port);
});
让我们看看它背后是怎么实现的:
首先找到入口文件,见名知意,打开express.js,我们看到这样的代码:
function createApplication() {
//创建app对象
var app = function(req, res, next) {
app.handle(req, res, next);
};
//继承node的事件对象
mixin(app, EventEmitter.prototype, false);
//继承./application对象
mixin(app, proto, false);
//app.request和response继承node原生的request和response对象
app.request = { __proto__: req, app: app };
app.response = { __proto__: res, app: app };
//初始化app对象
app.init();
return app;
}
app.init()方法调用的是继承自./application.js的方法。
下面是application.js中的init方法:
/**
* 初始化服务器
*
* - 设置默认配置
* - 设置默认中间件
* - 设置默认映射路由方法
*
* @private
*/ app.init = function init() {
this.cache = {};
this.engines = {};
this.settings = {}; this.defaultConfiguration();
};
//初始化app的默认配置
app.defaultConfiguration = function defaultConfiguration() {
var env = process.env.NODE_ENV || 'development'; // default settings
this.enable('x-powered-by');
this.set('etag', 'weak');
this.set('env', env);
this.set('query parser', 'extended');
this.set('subdomain offset', 2);
this.set('trust proxy', false); // trust proxy inherit back-compat
Object.defineProperty(this.settings, trustProxyDefaultSymbol, {
configurable: true,
value: true
}); debug('booting in %s mode', env);
//监听mount事件,当我们向express中添加中间件的时候会触发mount事件,
//这里会将每个中间件的request对象,response对象,engines对象,settings对象通过__proto__形成原型连,
//最顶层的request和response对象是Node原生的request和response对象,在createApplication中定义
this.on('mount', function onmount(parent) {
// inherit trust proxy
if (this.settings[trustProxyDefaultSymbol] === true
&& typeof parent.settings['trust proxy fn'] === 'function') {
delete this.settings['trust proxy'];
delete this.settings['trust proxy fn'];
} // inherit protos
this.request.__proto__ = parent.request;
this.response.__proto__ = parent.response;
this.engines.__proto__ = parent.engines;
this.settings.__proto__ = parent.settings;
}); // setup locals
this.locals = Object.create(null); // top-most app is mounted at /
this.mountpath = '/'; // default locals
this.locals.settings = this.settings; // default configuration
this.set('view', View);
this.set('views', resolve('views'));
this.set('jsonp callback name', 'callback'); if (env === 'production') {
this.enable('view cache');
} Object.defineProperty(this, 'router', {
get: function() {
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.');
}
});
};
express()分析结束。
app.get('/', function (req, res) { res.send('Hello World!'); });
该api的作用是创建到"/"的get请求的路由处理器,app.get/post/head等方法在application文件中的下述代码中定义:
methods.forEach(function(method) {
app[method] = function(path) {
if (method === 'get' && arguments.length === 1) {
//get方法特殊处理,只有一个参数的时候,获取app.settings[path]
return this.set(path);
} //给app对象绑定一个路由管理器Router
this.lazyrouter(); //使用路由管理器给指定的path创建一个路由对象处理对象
var route = this._router.route(path);
//调用路由处理对象的相应方法,即:去除第二个参数,作为处理程序,并传入route[method]
route[method].apply(route, slice.call(arguments, 1));
return this;
};
});
原来,这些方法都是动态添加的。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的相关方法来实现:
//添加非路由中间件
proto.use = function use(fn) {
/* 此处略去部分代码
其实我们不关注path是什么,默认是"/",当然他也会通过参数去
查找path,但是我们关注的是layer的生成,还有他的.route属性。 */
callbacks.forEach(function (fn) {
if (typeof fn !== 'function') {
throw new TypeError('Router.use() requires middleware function but got a ' + gettype(fn));
}
// add the middleware
debug('use %s %s', path, fn.name || '<anonymous>');
////实例化layer对象并进行初始化
var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: false,
end: false
}, fn);
//非路由中间件,该字段赋值为undefined
layer.route = undefined;
this.stack.push(layer);
}, this);
return this;
}; //添加路由中间件
proto.route = function(path){
//实例化路由对象
var route = new Route(path);
//实例化layer对象并进行初始化
var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: this.strict,
end: true
}, route.dispatch.bind(route));
//指向刚实例化的路由对象(非常重要),通过该字段将Router和Route关联来起来
layer.route = route;
this.stack.push(layer);
return route;
};
对于路由中间件,路由容器中的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));
/**
* lazily adds the base router if it has not yet been added.
*
* We cannot add the base router in the defaultConfiguration because
* it reads app settings which might be set after that has run.
*
* @private
*/
app.lazyrouter = function lazyrouter() {
if (!this._router) {
this._router = new Router({
caseSensitive: this.enabled('case sensitive routing'),
strict: this.enabled('strict routing')
}); this._router.use(query(this.get('query parser fn')));
this._router.use(middleware.init(this));
}
};
那Router是什么? 我们先看下Router模块的构造函数:在./router/index.js文件中:
var proto = module.exports = function(options) {
var opts = options || {}; function router(req, res, next) {
router.handle(req, res, next);
} // mixin Router class functions
router.__proto__ = proto; router.params = {};
router._params = [];
router.caseSensitive = opts.caseSensitive;
router.mergeParams = opts.mergeParams;
router.strict = opts.strict;
router.stack = []; return router;
};
这里是个知识点,其实这个router就相当于this,我们new出一个对象也就3个部分:
- 创建一个空对象,并且 this 变量引用该对象,同时还继承了该函数的原型。
- 属性和方法被加入到 this 引用的对象中。
- 新创建的对象由 this 所引用,并且最后隐式的返回 this 。
proto是Router的原型对象,上面有很多方法,稍后分析,我们再看 this.lazyrouter();后的 var route = this._router.route(path);
我们来看一下在./router/index.js文件中定义的route方法:
proto.route = function route(path) { //创建并初始化route对象
var route = new Route(path); //创建一个layer对象,并放入栈中,
//在处理业务的时候会根据path的匹配规则,来匹配对应的route,
//如果是使用use接口(比如添加中间件的时候),是不指定route的,
var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: this.strict,
end: true
}, route.dispatch.bind(route)); layer.route = route; this.stack.push(layer);
console.log("app.get(path,cb) return route:" + JSON.stringify(route));
return route;
};
注意!在route方法里我们用了route模块,注意和Router的区别,稍后总结。我们先去在./router/route.js中看看Route的构造方法:
function Route(path) {
this.path = path;
this.stack = []; debug('new %s', path); // route handlers for various http methods
this.methods = {};
}
不知道大家注意到没有,Router模块初始化有一个stack数组,route模块也有一个stack数组:Router模块的stack是装的Layer,我们去./router/layer.js下看Layer模块:
function Layer(path, options, fn) {
if (!(this instanceof Layer)) {
return new Layer(path, options, fn);
} debug('new %s', path);
var opts = options || {}; this.handle = fn;
this.name = fn.name || '<anonymous>';
this.params = undefined;
this.path = undefined;
this.regexp = pathRegexp(path, this.keys = [], opts); if (path === '/' && opts.end === false) {
this.regexp.fast_slash = true;
}
}
上边是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:
Route.prototype.dispatch = function dispatch(req, res, done) {
var idx = 0;
var stack = this.stack;
if (stack.length === 0) {
return done();
} var method = req.method.toLowerCase();
if (method === 'head' && !this.methods['head']) {
method = 'get';
} req.route = this; next(); function next(err) {
if (err && err === 'route') {
return done();
} var layer = stack[idx++];
if (!layer) {
return done(err);
} if (layer.method && layer.method !== method) {
return next(err);
} if (err) {
layer.handle_error(err, req, res, next);
} else {
layer.handle_request(req, res, next);
}
}
};
我们发现dispatch中通过next()获取stack中的每一个layer来执行相应的路由中间件,这样就保证了我们定义在路由上的多个中间件函数被按照定义的顺序依次执行。到这里我们已经知道了单个路由是被如何执行的,那我们定义的多个路由之间又是如何被依次执行的呢,现在我们来看看index.js中的handle函数:
proto.handle = function handle(req, res, out) {
var self = this; debug('dispatching %s %s', req.method, req.url); var search = 1 + req.url.indexOf('?');
var pathlength = search ? search - 1 : req.url.length;
var fqdn = req.url[0] !== '/' && 1 + req.url.substr(0, pathlength).indexOf('://');
var protohost = fqdn ? req.url.substr(0, req.url.indexOf('/', 2 + fqdn)) : '';
var idx = 0;
var removed = '';
var slashAdded = false;
var paramcalled = {}; // store options for OPTIONS request
// only used if OPTIONS request
var options = []; // middleware and routes
var stack = self.stack; // manage inter-router variables
var parentParams = req.params;
var parentUrl = req.baseUrl || '';
var done = restore(out, req, 'baseUrl', 'next', 'params'); // setup next layer
req.next = next; // for options requests, respond with a default if nothing else responds
if (req.method === 'OPTIONS') {
done = wrap(done, function(old, err) {
if (err || options.length === 0) return old(err);
sendOptionsResponse(res, options, old);
});
} // setup basic req values
req.baseUrl = parentUrl;
req.originalUrl = req.originalUrl || req.url; next(); function next(err) {
var layerError = err === 'route'
? null
: err; // remove added slash
if (slashAdded) {
req.url = req.url.substr(1);
slashAdded = false;
} // restore altered req.url
if (removed.length !== 0) {
req.baseUrl = parentUrl;
req.url = protohost + removed + req.url.substr(protohost.length);
removed = '';
} // no more matching layers
if (idx >= stack.length) {
setImmediate(done, layerError);
return;
} // get pathname of request
var path = getPathname(req); if (path == null) {
return done(layerError);
} // find next matching layer
var layer;
var match;
var route; while (match !== true && idx < stack.length) {
layer = stack[idx++];
match = matchLayer(layer, path);
route = layer.route; if (typeof match !== 'boolean') {
// hold on to layerError
layerError = layerError || match;
} if (match !== true) {
continue;
} if (!route) {
// process non-route handlers normally
continue;
} if (layerError) {
// routes do not match with a pending error
match = false;
continue;
} var method = req.method;
var has_method = route._handles_method(method); // build up automatic options response
if (!has_method && method === 'OPTIONS') {
appendMethods(options, route._options());
} // don't even bother matching route
if (!has_method && method !== 'HEAD') {
match = false;
continue;
}
} // no match
if (match !== true) {
return done(layerError);
} // store route for dispatch on change
if (route) {
req.route = route;
} // Capture one-time layer values
req.params = self.mergeParams
? mergeParams(layer.params, parentParams)
: layer.params;
var layerPath = layer.path; // this should be done for the layer
self.process_params(layer, paramcalled, req, res, function (err) {
if (err) {
return next(layerError || err);
} if (route) {
return layer.handle_request(req, res, next);
} trim_prefix(layer, layerError, layerPath, path);
});
}
从上边的代码我们可以看出,这里也是利用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是存的什么:
methods.forEach(function(method){
Route.prototype[method] = function(){
var handles = flatten(slice.call(arguments)); for (var i = 0; i < handles.length; i++) {
var handle = handles[i]; if (typeof handle !== 'function') {
var type = toString.call(handle);
var msg = 'Route.' + method + '() requires callback functions but got a ' + type;
throw new Error(msg);
} debug('%s %s', method, this.path); var layer = Layer('/', {}, handle);
layer.method = method; this.methods[method] = true;
this.stack.push(layer);
} return this;
};
});
我们看到他这个layer其实都是默认"/",我们可以当他是用.use方法加的中间件。
在本节的最后一起来看下app.listen的实现(application.js中):
app.listen = function listen() {
var server = http.createServer(this);
return server.listen.apply(server, arguments);
};
其中的this指的是app对象,在前面express()中已经分析过app的原型了,其中app的构造函数就是:
var app = function(req, res, next) { app.handle(req, res, next); };
所以app.listen翻译下其实就是我们常见的下述写法:
app.listen = function listen() {
var server = http.createServer(function(req,res,next){
app.handle(req,res,next)
});
return server.listen(arguments);
};
app.handle的源码在./application.js中:
app.handle = function handle(req, res, callback) {
var router = this._router; // final handler
var done = callback || finalhandler(req, res, {
env: this.get('env'),
onerror: logerror.bind(this)
}); // no routes
if (!router) {
debug('no routes defined on app');
done();
return;
} router.handle(req, res, done);
};
该方法将监听到的用户请求转入router中处理,即第一阶段处理。
上面分析了express启动加载的过程,重点是对router和route的管理。
以下面的用户请求为例:
app.get("/a/b", function(req, res) { res.end("hello,world"); });
router中按照用户定义的中间件和请求映射顺序注册处理栈,下面是一个完成初始化的处理栈截图:
该栈中有6个layer,按照用户初始化顺序添加到数组中(前两个是express默认添加的),其中第三个layer用来处理/a/b的请求。
比如当用户在发送/a/b的请求的时候,在router中的layer栈中做循环处理,依次判断路径是否匹配,route是否为空,直到找到匹配的layer为止.
该layer的定义如下:
proto.route = function route(path) {
console.log("app.get(path,cb) path value:" + JSON.stringify(path));
var route = new Route(path); var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: this.strict,
end: true
}, route.dispatch.bind(route));//指定该layer的处理函数dispatch layer.route = route;//指定该layer对应的二级处理路由器 this.stack.push(layer);
console.log("app.get(path,cb) return route:" + JSON.stringify(route));
return route;
};
然后调用layer的handle_request的方法
Layer.prototype.handle_request = function handle(req, res, next) {
var fn = this.handle; if (fn.length > 3) {
// not a standard request handler
return next();
} try {
fn(req, res, next);
} catch (err) {
next(err);
}
};
其中的this.handler即在定义layer的时候指定的route的dispatch方法,调用fn方法即调用dispatch方法。
Route.prototype.dispatch = function dispatch(req, res, done) {
var idx = 0;
var stack = this.stack;
if (stack.length === 0) {
return done();
} var method = req.method.toLowerCase();
if (method === 'head' && !this.methods['head']) {
method = 'get';
} req.route = this; next(); function next(err) {
if (err && err === 'route') {
return done();
} var layer = stack[idx++];
if (!layer) {
return done(err);
} if (layer.method && layer.method !== method) {
return next(err);
} if (err) {
layer.handle_error(err, req, res, next);
} else {
layer.handle_request(req, res, next);
}
}
};
这里将用户的请求导入到了route的layer栈中(该栈在application.js中和router中的栈一同完成初始化)
var route = this._router.route(path);//注册router的layer栈
route[method].apply(route, slice.call(arguments, 1));//注入route的layer栈,该栈中的处理函数即用户定义的回调函数
同router的栈处理顺序类似,不同的是这里的layer.handle_request最终调用的将是用户定义的回调函数,而不再是dispach函数。
最后:我还是想说一下,我们的路由机制!其实是通过内部的next函数,进行递归的,他其实有2个next函数,一个定义在proto.handle里,这是处理中间件的,另外一个在dispatch里,是用来处理路由的。具体大家可以看下这篇文章,反正讲的很明白了
阅读express的感悟的更多相关文章
- javascript设计模式阅读后的感悟与总结
单例模式 用于创建唯一的一个对象. 核心在于一个判断 var index if(index){ return index; } init(); 这样只会在第一次的时候初始化创建对象,以后都不会再创建对 ...
- express 快速教程
阅读 express 官方文档的记录. hello world example var express = require('express') var app = express() app.get ...
- 通过express搭建自己的服务器
前言 为了模拟项目上线,我们就需要一个服务器去提供API给我们调用数据.这次我采用express框架去写API接口.所有请求都是通过ajax请求去请求服务器来返回数据.第一次用node写后端,基本就是 ...
- 文献阅读方法 & 如何阅读英文文献 - 施一公(转)
附: 如何看懂英文文献?(好) 看需求,分层次 如何总结和整理学术文献? Mendeley & Everything 如何在pdf文献上做笔记?福晰阅读器 自己感悟: 一篇专业文献通常会有几页 ...
- 《重构网络-SDN架构与实现》阅读随笔
<重构网络-SDN架构与实现>: SDNLAB <重构网络-SDN架构与实现>新书有奖试读活动 资源下载 随笔 有幸拜读了李呈前辈和杨泽卫杨老师的作品<重构网络-SDN架 ...
- 写Java也得了解CPU--CPU缓存
CPU,一般认为写C/C++的才需要了解,写高级语言的(Java/C#/pathon...)并不需要了解那么底层的东西.我一开始也是这么想的,但直到碰到LMAX的Disruptor,以及马丁的博文,才 ...
- 如何系统地学习Node.js?
转载自知乎:http://www.zhihu.com/question/21567720 ------------------------------------------------------- ...
- 基础知识-Mockjs进行数据模拟
目录 1. 目标 2. 创建模拟数据服务器 3. 安装 mockjs, 熟悉 mockjs 语法 4. 设置代理,解决 vue 项目跨域问题 5. 设置响应头,解决无法获取获取 token 和 coo ...
- [C#学习笔记]分部类和分部方法
知识在于积累. 前言 好久没写博客了,因为在看<CLR via C#>的时候,竟然卡在了分部方法这一小节几天没下去.今天重新认真阅读,有些感悟,所以在此记录. 然后. 每天早晨第一句,&l ...
随机推荐
- php 解决乱码的通用方法
一,出现乱码的原因分析 1,保存文件时候,文件有自己的文件编码,就是汉字,或者其他国语言,以什么编码来存储 2,输出的时候,要给内容指定编码,如以网页的形势输入时<meta http-equiv ...
- HDU 4970 Killing Monsters
开始以为是线段树,算了一下复杂度也觉得能过...但是这题貌似卡了线段树... 具体做法: 对每一个塔,记录attack[l]+=d,attack[r+1]-=d;这样对于每个block,受到的伤害就是 ...
- Mysql基本类型(字符串类型)——mysql之二
转自: http://www.cnblogs.com/doit8791/archive/2012/05/28/2522556.html 1.varchar类型的变化 MySQL 数据库的varchar ...
- Why Does Qt Use Moc for Signals and Slots(QT官方的解释:GUI可以是动态的)
GUIs are Dynamic C++ is a standarized, powerful and elaborate general-purpose language. It's the onl ...
- HttpApplication中的异步线程
一.Asp.net中的线程池设置 在Asp.net的服务处理中,每当服务器收到一个请求,HttpRuntime将从HttpApplication池中获取一个HttpApplication对象处理此请求 ...
- CXF之webservice
使用 CXF 做 webservice 简单例子 Apache CXF 是一个开放源代码框架,提供了用于方便地构建和开发 Web 服务的可靠基础架构.它允许创建高性能和可扩展的服务,您可以将这 ...
- 过程化开发2048智力游戏WebApp
时间荏苒,唯编程与青春不可辜负,感觉自己一直没有专心去提升编程的技能,甚是惭愧!!! 周五,无意间看到一个开发2048的视频,有点兴趣就动起手来了,虽然不擅长前端开发,在此献丑,分享一下自己使用过程化 ...
- 水务新、老营收系统大PK
今天想撩一下咱水务界的大拿——营业收费管理系统. 营业收费 随着城市供水规模的不断扩大及户表改造的深入以及阶梯水价的实行还有移动平台等第三方支付的蓬勃发展,原有的营收软件系统已经不能适应目前水司的管理 ...
- C#.Net前台线程与后台线程的区别
本文来自:http://www.cnblogs.com/zfanlong1314/archive/2012/02/26/2390455.html .Net的公用语言运行时(Common Languag ...
- CTSC1999补丁VS错误题解
题目描写叙述 Description 错误就是人们所说的Bug.用户在使用软件时总是希望其错误越少越好.最好是没有错误的.可是推出一个没有错误的软件差点儿不可能,所以非常多软件公司都在疯狂地发放补丁( ...