源码学习:一个express().get方法的加载与调用

时间:2023-03-08 18:23:25

刚刚接触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,很多问题就清楚了...