源码学习:一个express().get方法的加载与调用
刚刚接触express,它的中间件确实把我搞得头晕。get的回调中要不要加next?不加载还会执行下一个中间件么?给get指定'/'路径是不是所有以'/'开头的访问在没有确切匹配时都能执行?use件又有什么区别,use中不加next是不是也可以继续执行下一个next?这些问题就是最困扰我的问题。为了搞清楚这些问题,我开始查看express的源码。下面就以一次get方法分析express的加载与调用流程。
以下面的代码为例,命名为test.js:
var express = require('express');
var app = express();
app.get('/',function myFunc(req, res) {
res.send('this is the Homepage');
});
app.listen('8080');
1.加载
var express = require('express');
导入了express目录的express.js文件,文件中声明的输出为createApplication函数。
var app = express();
调用了上述createApplication函数,来看这个函数(部分):
function createApplication() {
var app = function(req, res, next) {
app.handle(req, res, next);
};
mixin(app, EventEmitter.prototype, false);
mixin(app, proto, false);
app.init();
return app;
}
在这个函数中,初始化了一个app对象。可以看到:
- 这个对象是一个遵从http回调签名的函数,后面可以看到,例程最后一行的代码app.listen('8080')中,app.listen函数中就是将app最为一个回调函数初始化了一个http server。
- 在createApplication函数中,通过mixin(app, proto, false),将proto的属性和方法传给了app,而proto即通过require('./application')引用的同目录下application.js文件。Mixin语句将application.js中为app实例声明的属性和方法传递给app。之后,createApplication函数又调用了app.init(),这个方法在application.js文件中声明(在application.js中,声明了一个app实例的大部分属性和方法)。
例程test.js第三行调用了app.get方法。application.js对get方法的声明:
methods.forEach(function(method){
app[method] = function(path){
if (method === 'get' && arguments.length === 1) {
// app.get(setting)
return this.set(path);
}
this.lazyrouter();
var route = this._router.route(path);
route[method].apply(route, slice.call(arguments, 1));
return this;
};
});
函数首先针对get方法只有一个参数时作出了定义,此时get方法返回app的设定属性,跟我们的例程没有关系。
this.lazyrouter()为app实例初始化了基础router对象,并调用router.use方法为这个router添加了两个基础层,回调函数分别为query和middleware.init。我们不去管这个过程。
下一句var route = this._router.route(path)就以第一个参数path调用了router.route方法(router在lazyrouter初始化)。router在router目录中index.js文件中声明,它的属性stack存储了以layer描述的各个中间层。route方法定义在proto.route函数中,代码如下:
proto.route = function route(path) {
var route = new Route(path);
var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: this.strict,
end: true
}, route.dispatch.bind(route));
layer.route = route;
this.stack.push(layer);
return route;
};
可以看到,首先创建了一个新的route实例;然后将route.dispatch函数作为回调函数创建了一个新的layer实例,并将layer的route属性设置为这个route实例之后,将这个layer推入router(this.stack的this是router)的stack中。
形象地说,这个过程就是新建了一个layer作为中间层放入了router的stack数组中。这个layer的回调为route.dispatch。
执行完这个router.route方法后,又通过route[method].apply(route, slice.call(arguments, 1));让生成的这个route(不是router)调用了route.get。route.get中的关键流如下:
var handles = flatten(slice.call(arguments));//传入的回调在route[method].apply(route, slice.call(arguments, 1));中明确,即用户定义的回调函数myFunc
var layer = Layer('/', {}, handle);//以myFunc作为回调新建一个layer,设置method属性
layer.method = method;
this.methods[method] = true;
this.stack.push(layer);//这里的this是route对象,它也维护了一个stack(不是router的stack),存放了当前route对象的所有layer,每个layer包装了一个回调函数。
到此,程序就完成了对get方法的加载。
我们简短地回顾下这个过程:首先为app实例化一个router对象,这个对象的stack属性是一个数组,保存了app的不同中间层。一个中间层以一个layer实例表征,这个layer的handle属性引用了回调函数。对于get等方法创建的layer,它的handle为route.dispatch函数,而在get方法中自定义的回调函数是存放在route的stack中的。如果例程中继续为app添加其他路由,则router对象会继续生成新的layer存储这些中间件,并放入自己的stack中。
2.调用
来看例程最后一句app.listen('8080')。listen方法如下:
app.listen = function listen() {
var server = http.createServer(this);//this是例程中的app
return server.listen.apply(server, arguments);
};
listen方法以this为参数生成了一个http server,this是例程中的app,就是以app为回调函数生成了一个http server。前面提到,app是一个遵从http回调签名的函数,就是因为它就是在request发生时候的回调函数。
当server收到一个request时,调用app函数。app函数内只有一个语句:
app.handle(req, res,next);
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)
});
router.handle(req, res, done);
};
首先获取了app的router对象,然后调用router.handle方法。此时callback继承express.js中调用时的next,为undifined,所以done就为finalhandler函数,这个函数在服务器结束响应时调用。
继续看router.handle,这个函数的关键是闭包next()函数,此时先执行了next一次。进入while (match !== true && idx < stack.length)循环。通过debug可以知道,此时stack(是router的stack) 有3层,next函数内部:
layer = stack[idx++];
match = matchLayer(layer, path);
//('path matches layer?'+match);
route = layer.route;
先取出第一层,判断与request的path是否match。第一、二层是router初始化时的query函数和middleware.init函数,它们都会进入执行trim_prefix(layer, layerError, layerPath, path);的分支,并调用其中的layer.handle_request(req,res, next);,这个next就是router.handle函数里的闭包next。执行了这两层后,继续回调next函数。
这时就执行到了加载时生成的route所在的层,判断request路径是否匹配,这里的匹配执行的是严格匹配,比如这层的regexp属性(从加载时的路由确定)是'/',那么'/a'也不能匹配。
若路径不匹配,while循环会直接跳过当此循环,对router.stack的下一层进行匹配;如果path与这个route的regexp匹配,就会执行layer.handle_request(req, res, next);。
layer.handle_request函数:
Layer.prototype.handle_request = function handle(req, res, next) {
var fn = this.handle;
try {
fn(req, res, next);
} catch (err) {
next(err);
}
};
执行这层的回调函数fn=this.handle,我们在加载时分析过,这层的回调函数是route.dispatch函数,这个函数用来处理route实例内的路由选择。来看这个函数(部分):
Route.prototype.dispatch = function dispatch(req, res, done) {
var idx = 0;
var stack = this.stack;//this是route,它的stack中存储的用户定义的函数myFunc
var method = req.method.toLowerCase();
req.route = this;
next();
function next(err) {
var layer = stack[idx++];//第一次调用时获得用户定义的函数myFunc所在层
if (!layer) {
return done(err);
}
if (err) {
layer.handle_error(err, req, res, next);
} else {
layer.handle_request(req, res, next);// myFunc所在层调用handle_request
}
}
};
到layer.handle_request(req, res, next);时,myFunc所在层调用handle_request。
var fn = this.handle;//fn=myFunc
如果myFunc中没有引用next,它执行完后就回到了layer.handle_request(req, res, next);中。这样layer.handle_request(req, res, next);执行结束,回到调用它的route的next函数中,这个next运行结束,dispatch函数也运行结束。由于没有引用done签名参数,done所引用的router->next也不再运行。这样router->layer[dispatch]层的handle_request函数也执行完毕。router->next也执行完毕,从而依次执行完router初始化的两层[query]和[middleware.init]后,router.handle也执行完毕。整个流程处理结束。
如果myFunc中引用了next,则route.dispatch->next会再次被调用,如果这个route只有这一个handle函数,则在运行到
var layer = stack[idx++];
if (!layer) {
return done(err);
}
时会返回done(err)。这里的done函数是从layer.handle_request中传递来的router->next,于是调用router.handle->next(err);这样就实现了对router.stack中的下一个中间件的调用。
从上面的分析可以看出,express在处理中间层时,主要用了router、layer、route三个类。一个app实例有一个基础的router,用来处理所有的中间件;针对每个get等方法请求,都会实例化一个新的route对象。router和route实例都会维护自己的stack数组属性,以存放其路由信息。stack的每个元素都是一个layer,layer对中间件的回调函数进行了包装。
而且看到,循环进入下一个中间件的next函数,都是定义在router和route中,而调用一个中间件后再进入下一层则是通过layer实例的接口实现。
3.篇头的问题
get的回调中要不要加next,不加载next还会执行下一个中间件么?
如果这个方法回调后不需要再执行其他中间件,不需要引用next。但不添加next并不影响其它不同路由的执行,若当前的get方法不匹配请求路径,router会继续向下寻找;匹配路径后,执行完当前回调就不再寻找。
给get指定’/’路径是不是所有以‘/’开头的访问在没有确切匹配时都能执行?
不行。Router在执行匹配时是严格匹配,如果只有’/’,’/a’是不能匹配的。要想所有的请求匹配,可以指定’/*’为最后一个路径,这样所有查找不到的请求会被这个回调相应。
use件又有什么区别,use中不加next是不是也可以继续执行下一个next?
在next的使用上,use与get等方法是一致的。use方法不添加next,则执行完use的回调后不会再执行其他中间件。
use在执行匹配的时候与get等方法是有区别的。例如app.use('/'){}中,请求地址为/aaa也可以访问到;但如果是app.get('/'),请求中在/之后再加其他字母就无法访问。
转载一篇express源码分析的文章,写得很好:
从express源码中探析其路由机制
后记:其实仔细看了express的API,很多问题就清楚了...
源码学习:一个express().get方法的加载与调用的更多相关文章
- Spring5.0源码学习系列之浅谈懒加载机制原理
前言介绍 附录:Spring源码学习专栏 在上一章的学习中,我们对Bean的创建有了一个粗略的了解,接着本文挑一个比较重要的知识点Bean的懒加载进行学习 1.什么是懒加载? 懒加载(Lazy-ini ...
- Spring 源码学习(4)—— bean的加载part 1
前面随笔中,结束了对配置文件的解析工作,以及将配置文件转换成对应的BeanDefinition存储在容器中.接下来就该进行bean的加载了. public Object getBean(String ...
- 【requireJS源码学习03】细究requireJS的加载流程
前言 这个星期折腾了一周,中间没有什么时间学习,周末又干了些其它事情,这个时候正好有时间,我们一起来继续学习requireJS吧 还是那句话,小钗觉得requireJS本身还是有点难度的,估计完全吸收 ...
- Spring源码学习(5)—— bean的加载 part 2
之前归纳了从spring容器的缓存中直接获取bean的情况,接下来就需要从头开始bean的加载过程了.这里着重看单例的bean的加载 if(ex1.isSingleton()) { sharedIns ...
- WorldWind源码剖析系列:星球球体的加载与渲染
WorldWind源码剖析系列:星球球体的加载与渲染 WorldWind中主函数Main()的分析 在文件WorldWind.cs中主函数Main()阐明了WorldWind的初始化运行机制(如图1所 ...
- springboot源码解析-管中窥豹系列之BeanDefine如何加载(十三)
一.前言 Springboot源码解析是一件大工程,逐行逐句的去研究代码,会很枯燥,也不容易坚持下去. 我们不追求大而全,而是试着每次去研究一个小知识点,最终聚沙成塔,这就是我们的springboot ...
- Cocos2d-x 学习笔记(26) 从源码学习 DrawCall 的降低方法
[Cocos2d-x]学习笔记目录 本文链接:https://www.cnblogs.com/deepcho/cocos2dx-drawcall-glcalls 1. 屏幕左下角 我们通常在Cocos ...
- android源码解析(十七)-->Activity布局加载流程
版权声明:本文为博主原创文章,未经博主允许不得转载. 好吧,终于要开始讲讲Activity的布局加载流程了,大家都知道在Android体系中Activity扮演了一个界面展示的角色,这也是它与andr ...
- 一文带你解读Spring5源码解析 IOC之开启Bean的加载,以及FactoryBean和BeanFactory的区别。
前言 通过往期的文章我们已经了解了Spring对XML配置文件的解析,将分析的信息组装成BeanDefinition,并将其保存到相应的BeanDefinitionRegistry中,至此Spring ...
随机推荐
- Linux awk命令详解 + 练习
https://www.cnblogs.com/ftl1012/p/9250541.html 练习步骤: 1.我先是在root文件下面创建一个yan.txt文件,然后在文件中随便敲了几个字符串,由空格 ...
- JS 日期比较方法
1.日期参数格式:yyyy-mm-dd // a: 日期a, b: 日期b, flag: 返回的结果 function duibi(a, b,flag) { var arr = a.split(&qu ...
- 雷林鹏分享:jQuery EasyUI 树形菜单 - 树形菜单添加节点
jQuery EasyUI 树形菜单 - 树形菜单添加节点 本教程向您展示如何附加节点到树形菜单(Tree).我们将创建一个包含水果和蔬菜节点的食品树,然后添加一些其他水果到已存在的水果节点. 创建食 ...
- 有序广播和标准广播 --Android开发
一.标准广播和有序广播也很容易理解的. 标准广播: (1)通过sendBroadcast()方法发送 (2)通过异步方式发送,广播接收者的执行顺序是不明确的 有序广播: (1)通过sendOrderB ...
- 动态加载DataGrid表头及数据
初始化表头 js生成前端 /*初始化表头*/ function initDataGridTitle(id) { $.ajax({ url: '/${appName}/report/***/***', ...
- 微信小程序页面内转发 按钮 转发
通过给 button 组件设置属性 open-type="share",可以在用户点击按钮后触发 Page.onShareAppMessage() 事件,如果当前页面没有定义此事件 ...
- apicloud 环信总结
点击链接先查看一下apicloud 环信的文档 https://docs.apicloud.com/Client-API/Open-SDK/easeChat 文档中写了很多,但官方给的文档还是有问题, ...
- [java]转:String Date Calendar之间的转换
String Date Calendar之间的转换 String Date Calendar 1.Calendar 转化 String Calendar calendat = Calendar.ge ...
- mybatis使用@param("xxx")注解传参和不使用的区别
public interface SystemParameterMapper { int deleteByPrimaryKey(Integer id); int insert(SystemParame ...
- mysql处理以逗号隔开的字段内容
有一个字段保存了CheckBox内容,比如职业目标选择对于数据库字段otWorkgoal,保存了1,2,3,4内容 现在需要使用纯mysql语句,将字段otWorkgoal根据内容,进行翻译成中文的内 ...