刚刚接触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对象。可以看到:

  1. 这个对象是一个遵从http回调签名的函数,后面可以看到,例程最后一行的代码app.listen('8080')中,app.listen函数中就是将app最为一个回调函数初始化了一个http server。
  2. 在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方法的加载与调用的更多相关文章

  1. Spring5.0源码学习系列之浅谈懒加载机制原理

    前言介绍 附录:Spring源码学习专栏 在上一章的学习中,我们对Bean的创建有了一个粗略的了解,接着本文挑一个比较重要的知识点Bean的懒加载进行学习 1.什么是懒加载? 懒加载(Lazy-ini ...

  2. Spring 源码学习(4)—— bean的加载part 1

    前面随笔中,结束了对配置文件的解析工作,以及将配置文件转换成对应的BeanDefinition存储在容器中.接下来就该进行bean的加载了. public Object getBean(String ...

  3. 【requireJS源码学习03】细究requireJS的加载流程

    前言 这个星期折腾了一周,中间没有什么时间学习,周末又干了些其它事情,这个时候正好有时间,我们一起来继续学习requireJS吧 还是那句话,小钗觉得requireJS本身还是有点难度的,估计完全吸收 ...

  4. Spring源码学习(5)—— bean的加载 part 2

    之前归纳了从spring容器的缓存中直接获取bean的情况,接下来就需要从头开始bean的加载过程了.这里着重看单例的bean的加载 if(ex1.isSingleton()) { sharedIns ...

  5. WorldWind源码剖析系列:星球球体的加载与渲染

    WorldWind源码剖析系列:星球球体的加载与渲染 WorldWind中主函数Main()的分析 在文件WorldWind.cs中主函数Main()阐明了WorldWind的初始化运行机制(如图1所 ...

  6. springboot源码解析-管中窥豹系列之BeanDefine如何加载(十三)

    一.前言 Springboot源码解析是一件大工程,逐行逐句的去研究代码,会很枯燥,也不容易坚持下去. 我们不追求大而全,而是试着每次去研究一个小知识点,最终聚沙成塔,这就是我们的springboot ...

  7. ‎Cocos2d-x 学习笔记(26) 从源码学习 DrawCall 的降低方法

    [Cocos2d-x]学习笔记目录 本文链接:https://www.cnblogs.com/deepcho/cocos2dx-drawcall-glcalls 1. 屏幕左下角 我们通常在Cocos ...

  8. android源码解析(十七)-->Activity布局加载流程

    版权声明:本文为博主原创文章,未经博主允许不得转载. 好吧,终于要开始讲讲Activity的布局加载流程了,大家都知道在Android体系中Activity扮演了一个界面展示的角色,这也是它与andr ...

  9. 一文带你解读Spring5源码解析 IOC之开启Bean的加载,以及FactoryBean和BeanFactory的区别。

    前言 通过往期的文章我们已经了解了Spring对XML配置文件的解析,将分析的信息组装成BeanDefinition,并将其保存到相应的BeanDefinitionRegistry中,至此Spring ...

随机推荐

  1. 自定义广播(BroadcastReceiver)事件 --Android开发

    本例演示自定义广播事件.我们需要做的是,在主活动中写发送广播的代码,然后在接收广播的类中写接收广播的代码. 1.主活动中点击按钮后发送广播 MainActivity.java: public clas ...

  2. LeetCode--459--重复的字符串

    问题描述: 给定一个非空的字符串,判断它是否可以由它的一个子串重复多次构成.给定的字符串只含有小写英文字母,并且长度不超过10000. 示例 1: 输入: "abab" 输出: T ...

  3. 关于OkHttp同步请求的小错误

    今天进行OkHttp的同步请求 写的都是按照官方的去写的 但是返回的东西却不是我想要的 原因是我直接拿到Response后,直接Response.toString,想要拿到返回值 但是这样是错误的,正 ...

  4. Linux简介和安装

    Andrew S. Tanenbaum参考Unix,写了Minix,并开源,Linus Torvalds以其为模板写了Linux. Linux包含内核版本和发行版本. Linux内核版本 Linux内 ...

  5. JS中循环逻辑和判断逻辑的使用实例

    源代码见: https://github.com/Embrace830/JSExample &&和||的理解 a || b:如果a是true,那么b不管是true还是false,都返回 ...

  6. selenium chrome 自动加载flash

    #coding:utf-8from selenium import webdriverfrom selenium.webdriver.support.select import Selectfrom ...

  7. 46. 47. Permutations and Permutations II 都适用(Java,字典序 + 非字典序排列)

    解析: 一:非字典序(回溯法) 1)将第一个元素依次与所有元素进行交换: 2)交换后,可看作两部分:第一个元素及其后面的元素: 3)后面的元素又可以看作一个待排列的数组,递归,当剩余的部分只剩一个元素 ...

  8. 解决audio 在部分移动端浏览器不能自动播放(目前包括ios、微博)

    问题描述:项目需要在页面加载完成后自动播放音乐,但在ios中却无法自动播放,需要用户主动触发 解决办法: $('html').one('touchstart',function(){ document ...

  9. [Uva P11168] Airport

    题目是英文的,这里就不给出来了. 题目的大意是说,在平面上有n个点,要找一条直线,使所有点到直线的平均距离最小,且这些点都在该直线的同一侧(包括直线上). 那么,既然要使距离最小化,还要使所有点一定在 ...

  10. web服务器-----Tomcat 7.0安装

    下载地址:http://tomcat.apache.org/ 1.下载 2.解压缩---c盘 3.运行bin\startup.bat 启动Tomcat服务器 运行bin\shutdown.bat关闭T ...