刚刚接触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. python爬虫学习(三):使用re库爬取"淘宝商品",并把结果写进txt文件

    第二个例子是使用requests库+re库爬取淘宝搜索商品页面的商品信息 (1)分析网页源码 打开淘宝,输入关键字“python”,然后搜索,显示如下搜索结果 从url连接中可以得到搜索商品的关键字是 ...

  2. HTML如何实现滚动文字

    HTML如何实现滚动文字 一.总结 一句话总结:marquee标签,也可以用js和css来实现 marquee标签 也可jss和css <marquee><span style=&q ...

  3. 验证码之SimpleCaptcha (二)

    上回说到了简单的使用simpleCaptcha,这次我们这次我们将讲解扩张simpleCaptcha.       回到正题,我们需要一些自定义的验证码,比如验证码的字体大小,背景,颜色等等,默认的验 ...

  4. JavaScript 第七章总结

    前言 主要介绍了关于 JavaScript 中有关 type 的问题.讲了很多关于各种 type 的 idiosyncrasies. 谈谈JavaScript types 在 JavaScript 中 ...

  5. 一个使用Jmeter做接口性能测试的实战案例

    1 安装并配置Jmeter Jmeter的安装不在这里阐述,安装步骤非常简单. 直接进入主题 1.1 数据库连接配置 由于测试过程需要调用数据库获取响应部署数据,因此需要先建立与数据库的连接. 如果不 ...

  6. C# ftp 上传、下载、删除

    public class FtpHelper { public static readonly FtpHelper Instance = new FtpHelper(); /// <summar ...

  7. android------基础面试题

    1. Android的四大组件是哪些,它们的作用? 答:Activity:Activity是Android程序与用户交互的窗口,是Android构造块中最基本的一种,它需要为保持各界面的状态,做很多持 ...

  8. linux bash基本特性

    一.bash 基础特性 (1)命令历史的功能 history: 环境变量 HISTSIZE:命令历史记录的条数 HISTFILE: ~/.bash_history 每个用户都有自己独立的命令历史文件 ...

  9. CentOS下yum命令详解

    CentOS下yum命令详解 Yum: 即Yellowdog Update Modifier,是一种基于rpm的包管理工具 yum命令使用示例 显示yum仓库 显示所有仓库 yum repolist ...

  10. JUnit 4.0学习笔记

    JUnit命令整理 @Test(timeout=  ) : 测试时间超过范围即失败 @Test(expected=) : 申明出会发生的异常 @Before :  在每一个测试方法前执行 @After ...