这一节就讲从一个请求到来,express内部是如何将其转交给合适的路由,路由又是如何调用中间件的。

  以express-generator为例,关键代码如下:

// app.js
app.use('/', indexRouter);
app.use('/users', usersRouter);
// indexRouter
router.get('/', function(req, res, next) {
console.log('first middleware');
next();
},(req,res,next)=>{
res.render('index', { title: 'Express' });
});
// usersRouter
router.get('/', function(req, res, next) {
res.send('respond with a resource');
});

  在两个路由的JS中,两次router.get调用会分别生成2个path层级的layer对象,中间件函数为内部方法route.dispatch,push进了router的stack数组中,并挂载了2个route对象。而这两个route对象根据后面的中间件函数数量又独立生成了对应的内部layer,仅处理中间件函数,同时push到了route的stack中。

  在最外层的app.js中,调用app.use,传入挂载路径与返回的router对象,由于router对象没有set方法,不是express应用,所以直接走的router.use方法。在use方法里,生成了两个Layer对象,路径为app.use的第一个参数,fn为返回的router函数对象。

  最最后,2个Layer对象会被push进app的内部独立router对象中。示意图如下:

  提前简单说一下涉及的四个模块app、router、layer、route。

1、app => 主要负责全局配置参数读取,所有的方法最终都会指向后面的工具模块,本身不做事

2、router => 所有app应用内部会有一个默认的router对象,该router对象上stack数组中的Layer主要根据路径把请求分发给处理对应路径的自定义router。而自定义的router上layer对象也不会直接处理请求,而是再次根据路径把请求分发给对应的route对象。route对象会遍历stack数组,依次取出layer调用中间件处理请求。

3、layer => 请求分发对象,虽然说3个参数分别为路径、配置参数、处理函数,但是在实际情况中只会单独处理一件事。

4、route => 最底层的对象,负责处理请求。

  这时候,假设有一个'/'根路径的get请求过来了。

app.handle

  入口函数就是第一节就见过,但是一直没有管的app.hanle:

function createApplication() {
var app = function(req, res, next) {
app.handle(req, res, next);
};
// mixin && init
return app;
}
app.handle = function handle(req, res, callback) {
var router = this._router;
// 单app应用时为默认的finalhandler
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);
};

  这里假设只有一个app应用,请求进来后封装了一个默认的callback,然后调用了router模块的handle方法。

router.handle

  这个函数太长了,分几段来说吧。

proto.handle = function handle(req, res, out) {
var self = this; debug('dispatching %s %s', req.method, req.url); var idx = 0;
// 获取请求地址的protocol + host
var protohost = getProtohost(req.url) || ''
var removed = '';
// 标记斜杠
var slashAdded = false;
var paramcalled = {}; // 应付OPTIONS方式的请求
var options = []; // 获取本地的layer数组
var stack = self.stack; // manage inter-router variables
var parentParams = req.params;
var parentUrl = req.baseUrl || '';
var done = restore(out, req, 'baseUrl', 'next', 'params'); // 挂载next方法
req.next = next; // options请求的默认返回
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()...
}

  函数在最开始还是整理参数,这里的restore没看懂具体作用,暂时跳过这里。

  总结来说第一部分做了以下事情:

1、获取协议+基本地址的字符串

2、获取stack数组,里面装的是layer对象

3、定义标记变量

4、对OPTIONS请求做特殊处理

5、done方法是所有layer跑完后的最终回调,此时需要还原url

  对于OPTIONS方式的请求,若没有做特殊处理,则会返回一个默认的响应。而在servlet中,则有一个特殊的doOptions的方法专门来设置Allow请求头响应,感觉差不多。

  接下来调用一个next方法,该方法会被挂载到req上面,这是第一次调用:

proto.handle = function handle(req, res, out) {
// ...
next();
function next(err) {
// next('route')不会被当成错误
var layerError = err === 'route' ?
null :
err; // 去掉斜杠
if (slashAdded) {
req.url = req.url.substr(1);
slashAdded = false;
} // 还原被更改的req.url
if (removed.length !== 0) {
req.baseUrl = parentUrl;
req.url = protohost + removed + req.url.substr(protohost.length);
removed = '';
} // 退出路由的信号
if (layerError === 'router') {
setImmediate(done, null)
return
} // 所有的layer都遍历完毕
if (idx >= stack.length) {
setImmediate(done, layerError);
return;
} // 获取请求的pathname
var path = getPathname(req); if (path == null) {
return done(layerError);
} // 寻找下一个匹配的layer
var layer;
var match;
var route; // ...more code
}
// ...
}

  这一部分主要做了下列事情:

1、判断是否有err定义layerError变量,其中next('route')会被忽略

2、根据slashAdded变量决定是否需要切割一下url,还原完整的url(二级路由匹配)

3、除了route,router字符串似乎在next中也有特殊意义?

  下面开始真正的匹配layer,如下:

while (match !== true && idx < stack.length) {
// 取出一个layer
layer = stack[idx++];
// 检测layer是否匹配该路径
match = matchLayer(layer, path);
route = layer.route; // ...
}

  这里涉及到了Layer对象的原型方法,matchLayer(layer, path)实际上就是layer.match(path)。

  以假设条件看一下match的匹配过程:

// app.use('/',indexRouter)满足fast_slash条件
Layer.prototype.match = function match(path) {
var match if (path != null) {
// layer匹配路径为/时 匹配所有
if (this.regexp.fast_slash) {
this.params = {}
this.path = ''
return true
} // layer匹配路径为*时 匹配所有:param
// 调用decodeURIComponent转义path
if (this.regexp.fast_star) {
this.params = { '0': decode_param(path) }
this.path = path
return true
} // 用生成的正则解析
match = this.regexp.exec(path)
}
// 路径不匹配 返回false
if (!match) {
this.params = undefined;
this.path = undefined;
return false;
} // 其余情况下匹配的路径
// 后面讨论... return true;
}

  由于假设请求路径为'/',所以这里会跳过match阶段,直接返回true。

  继续看代码:

while (match !== true && idx < stack.length) {
// 取出一个layer
layer = stack[idx++];
match = matchLayer(layer, path);
route = layer.route;
// 报错
if (typeof match !== 'boolean') layerError = layerError || match;
// Layer未匹配
if (match !== true) continue;
// app内部router对象的layer不存在route
if (!route) continue;
// 处理错误
if (layerError) {
// routes do not match with a pending error
match = false;
continue;
} // ...处理外部router对象上的layer
}

  需要注意的是,这里的匹配是对app的内部路由上的Layer进行遍历,而这些layer是没有route对象挂载的,仅仅是用来分发外部路由,因此这里会continue直接跳过后面的流程。

  由于已经匹配到对应的Layer,所以while循环跳出,继续下面的流程:

// 根据配置参数处理参数合并
req.params = self.mergeParams ?
mergeParams(layer.params, parentParams) :
layer.params;
// 获取layer匹配的path => ''
var layerPath = layer.path; // this should be done for the layer
self.process_params(layer, paramcalled, req, res, function(err) {
// ...trim_prefix(layer, layerError, layerPath, path)
});

  在生成路由会有一个合并参数的选项,决定是否将父路由的参数合并到子路由,默认为false。

  接下来获取layer匹配的path后,调用了另外一个方法,而这个方法主要是处理/path:prarms这种形式的参数,所以跳过。

  而回调的trim_prefix函数内容也直接跳过,后面讲,直接进入layer.handle_request函数:

Layer.prototype.handle_request = function handle(req, res, next) {
// 这里的handle是router函数对象
var fn = this.handle;
// 错误处理中间件有4个参数
if (fn.length > 3) {
return next();
}
// 调用具体外部路由的handle方法
try {
fn(req, res, next);
} catch (err) {
next(err);
}
};

  这里从app的内部路由handle方法跳到了外部路由的handle中,再走一遍流程。

  由于req、res始终是一个,所以大部分的都可以跳过,这里挑不同的地方来讲:

1、stack

  var stack = self.stack;

  由于换了router,所以stack也换成了外部路由的stack,里面装的是有route挂载的layer。

2、while循环的后半段

// 获取请求的方式
var method = req.method;
var has_method = route._handles_method(method); // OPTIONS请求特殊处理
if (!has_method && method === 'OPTIONS') {
appendMethods(options, route._options());
} // 如果route未处理该方式请求 直接跳过
if (!has_method && method !== 'HEAD') {
match = false;
continue;
} Route.prototype._handles_method = function _handles_method(method) {
// router.all
if (this.methods._all) {
return true;
}
var name = method.toLowerCase();
// head默认视为get请求
if (name === 'head' && !this.methods['head']) {
name = 'get';
}
// 判断route是否有处理该请求方式的中间件
return Boolean(this.methods[name]);
}; // route[METHODS]
var layer = Layer('/', {}, handle);
layer.method = method; this.methods[method] = true;

  这里做了一个提前判断,在调用app[METHODS]、router[METHODS]时,最后指向底层的route[METHODS]。除了生成一个layer对象,还会同时将route的本地属性methods对象上对应方式的键设为true,表示这个route有处理对应请求方式的layer。

  在跳过process_params、trim_prefix后,还是回到了handle_request方法。

  然而,这里的layer对应的handle并不指向中间件函数,而是route.dispatch.bind(route),如下:

// router.get('/',fn1,fn2)...
var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: this.strict,
end: true
}, route.dispatch.bind(route));

  真正的中间件函数是在layer.route上,所以这个是另外一个分发方法,负责把对应方式的请求转给对应的route。

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';
}
// 最终匹配的route
req.route = this; next(); function next(err) {
// signal to exit route
if (err && err === 'route') return done(); // err...
// 依次取出route对象stack中的layer
var layer = stack[idx++];
// err... if (err) {
layer.handle_error(err, req, res, next);
} else {
// 又是这个方法
layer.handle_request(req, res, next);
}
}
};

  这个dispatch与handle方法十分类似,依次取出layer并再次调用其handle_request方法,这里的layer里面的handle是最终处理响应请求的中间件函数。

  在文档中指出,需要执行中间件的第三个参数next中间件才会继续走下去,从这里也能看出,调用next后回到dispatch方法,会从stack上取出下一个layer,然后继续执行中间件函数,直到所有的layer都过了一遍,会调用回调函数done,这个方法就是最初router.handle里面的next函数,开始下一轮读取。

  当内部路由上的layer都过完,请求就处理完毕。正常情况下,会结束响应。接下来会调用最终回调,简单看一下比较复杂,后面单独讲。

  完结。

.8-浅析express源码之请求处理流程(1)的更多相关文章

  1. .9-浅析express源码之请求处理流程(2)

    上节漏了几个地方没有讲. 1.process_params 2.trim_prefix 3.done 分别是动态路由,深层路由与最终回调. 这节就只讲这三个地方,案例还是express-generat ...

  2. .5-浅析express源码之Router模块(1)-默认中间件

    模块application已经完结,开始讲Router路由部分. 切入口仍然在application模块中,方法就是那个随处可见的lazyrouter. 基本上除了初始化init方法,其余的app.u ...

  3. 渣渣菜鸡的 ElasticSearch 源码解析 —— 启动流程(下)

    关注我 转载请务必注明原创地址为:http://www.54tianzhisheng.cn/2018/08/12/es-code03/ 前提 上篇文章写完了 ES 流程启动的一部分,main 方法都入 ...

  4. 渣渣菜鸡的 ElasticSearch 源码解析 —— 启动流程(上)

    关注我 转载请务必注明原创地址为:http://www.54tianzhisheng.cn/2018/08/11/es-code02/ 前提 上篇文章写了 ElasticSearch 源码解析 -- ...

  5. Netty 源码学习——客户端流程分析

    Netty 源码学习--客户端流程分析 友情提醒: 需要观看者具备一些 NIO 的知识,否则看起来有的地方可能会不明白. 使用版本依赖 <dependency> <groupId&g ...

  6. apiserver源码分析——启动流程

    前言 apiserver是k8s控制面的一个组件,在众多组件中唯一一个对接etcd,对外暴露http服务的形式为k8s中各种资源提供增删改查等服务.它是RESTful风格,每个资源的URI都会形如 / ...

  7. express源码分析之Router

    express作为nodejs平台下非常流行的web框架,相信大家都对其已经很熟悉了,对于express的使用这里不再多说,如有需要可以移步到www.expressjs.com自行查看express的 ...

  8. 从express源码中探析其路由机制

    引言 在web开发中,一个简化的处理流程就是:客户端发起请求,然后服务端进行处理,最后返回相关数据.不管对于哪种语言哪种框架,除去细节的处理,简化后的模型都是一样的.客户端要发起请求,首先需要一个标识 ...

  9. .7-浅析express源码之Router模块(3)-app[METHODS]

    之前的讨论都局限于use方法,所有方式的请求会被通过,这一节讨论express内部如何处理特殊请求方法. 给个流程图咯~ 分别给出app.METHODS与router.METHODS: // app. ...

随机推荐

  1. [jquery]如何实现页面单块DIV区域滚动展示

    // 未实现功能的代码 1(自己写的代码) var _cur_top = $(window).scrollTop(); var num = $(".class_section"). ...

  2. 分形之谢尔宾斯基(Sierpinski)四面体

    前面讲了谢尔宾斯基三角形,这一节的将对二维三角形扩展到三维,变成四面体.即将一个正四面体不停地拆分,每个正四面体可以拆分成四个小号的正四面体.由二维转变到三维实现起来麻烦了许多.三维的谢尔宾斯基四面体 ...

  3. Django:查询后,分页功能为全部对象分页,丢失查询查询参数

    问题: 原始的链接为 http://127.0.0.1:8000/article/list-article-titles-bysomeone/guchen/?column=django 有一个colu ...

  4. dialog里屏蔽ESC和回车

    重载PreTranslateMessage,在return之前加一句判断,只要是按下ESC和回车的消息,就直接置之不理即可,代码如下: if( pMsg->message == WM_KEYDO ...

  5. WPF 介绍一种在MVVM模式下弹出子窗体的方式

    主要是通过一个WindowManager管理类,在window后台代码中通过WindowManager注册需要弹出的窗体类型,在ViewModel通过WindowManager的Show方法,显示出来 ...

  6. .net图表之ECharts随笔02-字符云

    后续每一类图表,若无特殊说明,都将建立在01的基础上,修改参数option,且参数均以json的格式 要形成如图所示的字符云,一般需要设置两个大参数——title和series 其中,title就是图 ...

  7. 日期时间类:Date,Calendar,计算类:Math

    日期时间类 计算机如何表示时间? 时间戳(timestamp):距离特定时间的时间间隔. 计算机时间戳是指距离历元(1970-01-01 00:00:00:000)的时间间隔(ms). 计算机中时间2 ...

  8. Java虚拟机7:垃圾收集(GC)-2(并行和并发的区别)

    1.并发编程下 这两个名词都是并发编程中的概念,在并发编程的模型下的定义: 并发:是在同一个cpu上同时(不是真正的同时,而是看来是同时,因为cpu要在多个程序间切换)运行多个程序. 并行:是多个或同 ...

  9. updateByPrimaryKey 和 updateByPrimaryKeySelective

    1. 数据库记录 2. updateByPrimaryKey Preparing: UPDATE t_token_info SET entity_id = ?,entity_type = ?,time ...

  10. Django模版结构优化和加载静态文件

    引入模版 有时候一些代码是在许多模版中都用到的.如果我们每次都重复的去拷贝代码那肯定不符合项目的规范.一般我们可以把这些重复性的代码抽取出来,就类似于Python中的函数一样,以后想要使用这些代码的时 ...