.9-浅析express源码之请求处理流程(2)
上节漏了几个地方没有讲。
1、process_params
2、trim_prefix
3、done
分别是动态路由,深层路由与最终回调。
这节就只讲这三个地方,案例还是express-generator,不过请求的方式更为复杂。
process_params
在讲这个函数之前,需要先进一下path-to-regexp模块,里面对字符串的正则化有这么一行replace:
path = ('^' + path + (strict ? '' : path[path.length - 1] === '/' ? '?' : '/?'))
// .replace...
.replace(/(\\\/)?(\\\.)?:(\w+)(\(.*?\))?(\*)?(\?)?/g, function (match, slash, format, key, capture, star, optional, offset) {
// ...
keys.push({
name: key,
optional: !!optional,
offset: offset + extraOffset
});
// ...
});
这里会对path里面的:(...)进行匹配,然后获冒号后面的字符串,然后作为key传入keys数组,而这个keys数组是layer的属性,后面要用。
另外还要看一个地方,就是layer.mtach,在上一节,由于传的是根路径,所以直接从fast_slash跳出了。
如果是正常的带参数路径,执行过程如下:
/**
* @example path = /users/params
* @example router.get('/users/:id')
*/
Layer.prototype.match = function match(path) {
var match if (path != null) {
// ...快速匹配 match = this.regexp.exec(path)
} if (!match) { /*...*/ } // 缓存params
this.params = {};
this.path = match[0] // [{ name: prarms,... }]
var keys = this.keys;
var params = this.params; for (var i = 1; i < match.length; i++) {
var key = keys[i - 1];
var prop = key.name;
// decodeURIComponent(val)
var val = decode_param(match[i]);
// layer.params.id = params
if (val !== undefined || !(hasOwnProperty.call(params, prop))) {
params[prop] = val;
}
} return true;
};
根据注释的案例,可以看出路由参数的匹配过程,这里仅仅以单参数为例。
下面可以进入process_params方法了,分两步讲:
proto.process_params = function process_params(layer, called, req, res, done) {
var params = this.params;
// 获取keys数组
var keys = layer.keys;
if (!keys || keys.length === 0) return done();
var i = 0;
var name;
var paramIndex = 0;
var key;
var paramVal;
var paramCallbacks;
var paramCalled;
function param(err) {
if (err) return done(err);
if (i >= keys.length) return done();
paramIndex = 0;
key = keys[i++];
name = key.name;
// req.params = layer.params
paramVal = req.params[name];
// 后面讨论
paramCallbacks = params[name];
// 初始为空对象
paramCalled = called[name];
if (paramVal === undefined || !paramCallbacks) return param();
// param previously called with same value or error occurred
if (paramCalled && (paramCalled.match === paramVal || (paramCalled.error && paramCalled.error !== 'route'))) {
// error...
}
// 设置值
called[name] = paramCalled = {
error: null,
match: paramVal,
value: paramVal
};
paramCallback();
}
// single param callbacks
function paramCallback(err) {
//...
}
param();
};
这里除去遍历参数,有几个变量,稍微解释下:
1、paramVal => 请求路径带的路由参数
2、paramCallbacks => 调用router.params会填充该对象,请求带有指定路由参数会触发的回调函数
3、paramCalled => 一个标记对象
当参数匹配之后,会调用回调函数paramCallback:
function paramCallback(err) {
// 依次取出callback数组的fn
var fn = paramCallbacks[paramIndex++];
// 标记val
paramCalled.value = req.params[key.name];
if (err) {
// store error
paramCalled.error = err;
param(err);
return;
}
if (!fn) return param();
// 调用回调函数
try {
fn(req, res, paramCallback, paramVal, key.name);
} catch (e) {
paramCallback(e);
}
}
仅仅只是调用在param方法中预先填充的函数。用法参见官方文档的示例:
router.param('user', function(req, res, next, id) {
// ...do something
next();
})
每当路由参数是user时,就会触发调用后面注入的函数,其中4个参数可以跟上面源码的形参对应。虽然源码提供了5个参数,但是示例只有4个。
trim_prefix
这个就比较简单了。
案例还是按照上一节的,假设有这样的请求:
// app.js
app.use('/user',userRouter);
// userRouter.js
router.get('/abcd',()=>{...});
// client的get请求
path => '/users/abcd'
此时,内部路由将其分发给了usersRouter,但是在分发之前有一个问题。
在自定义的路由中,是不需要指定根路径的,因为在app.use中已经写明了,如果将完整的路径传递进去,在路径正则匹配时会失败,这时候就需要进行trim_prefix了。
源码如下:
/**
*
* @param layer 匹配到的layer
* @param layerError error
* @param layerPath layer.path => '/users'
* @param path req.url.pathname => '/users/abcd'
*/
function trim_prefix(layer, layerError, layerPath, path) {
if (layerPath.length !== 0) {
// 保证路径后面的字符串合法
var c = path[layerPath.length]
if (c && c !== '/' && c !== '.') return next(layerError) debug('trim prefix (%s) from url %s', layerPath, req.url);
// 缓存被移除的path
removed = layerPath;
req.url = protohost + req.url.substr(protohost.length + removed.length); // 保证移除后的路径以/开头
if (!protohost && req.url[0] !== '/') {
req.url = '/' + req.url;
slashAdded = true;
} // 基本路径拼接
req.baseUrl = parentUrl + (removed[removed.length - 1] === '/' ?
removed.substring(0, removed.length - 1) :
removed);
} debug('%s %s : %s', layer.name, layerPath, req.originalUrl); // 将新的req.url传进去处理
if (layerError) {
layer.handle_error(layerError, req, res, next);
} else {
layer.handle_request(req, res, next);
}
}
可以看出,源码就是去掉路径的头,然后将新的路径传到二级layer对象中做匹配。
done
这个最终回调麻烦的要死。
注意:如果调用了res.send()后,源码内部会调用res.end结束响应,回调将不会被执行,这是为了防止意外情况所做的保险工作。
一层一层的来看最终回调的结构,首先是handle方法中的直接定义:
var done = restore(out, req, 'baseUrl', 'next', 'params');
从方法名可以看出这就是一个值恢复的函数:
function restore(fn, obj) {
var props = new Array(arguments.length - 2);
var vals = new Array(arguments.length - 2);
// 在请求到来的时候先缓存原始信息
/**
* props = ['baseUrl', 'next', 'params']
* vals = ['url','next方法','动态路由的params']
*/
for (var i = 0; i < props.length; i++) {
props[i] = arguments[i + 2];
vals[i] = obj[props[i]];
}
return function () {
// 在请求处理完后对值进行回滚
for (var i = 0; i < props.length; i++) {
obj[props[i]] = vals[i];
}
return fn.apply(this, arguments);
};
}
简单。
下面来看看这个fn是个啥玩意,默认情况下来源于一个工具:
var done = callback || finalhandler(req, res, {
env: this.get('env'),
onerror: logerror.bind(this)
});
function finalhandler(req, res, options) {
// 获取配置参数
var opts = options || {}
var env = opts.env || process.env.NODE_ENV || 'development'
var onerror = opts.onerror
return function (err) {
// ...
}
}
在获取参数后,返回了一个新函数,简单看一下done的调用地方:
// 遇到router标记直接调用done
if (layerError === 'router') {
setImmediate(done, null)
return
} // 走完了layer匹配
if (idx >= stack.length) {
setImmediate(done, layerError);
return;
} // path为null
var path = getPathname(req); if (path == null) {
return done(layerError);
}
基本上正常情况下就是null,错误情况下会传了一个err,基本上符合node的err first模式。
进入finalhandler方法:
function done(err) {
var headers
var msg
var status
// 请求已发送的情况
if (!err && headersSent(res)) {
debug('cannot 404 after headers sent')
return
}
// unhandled error
if (err) {
// ...
} else {
// not found
status = 404
msg = 'Cannot ' + req.method + ' ' + encodeUrl(getResourceName(req))
}
debug('default %s', status)
// 处理错误
if (err && onerror) {
defer(onerror, err, req, res)
}
// 请求已发送销毁req的socket实例
if (headersSent(res)) {
debug('cannot %d after headers sent', status)
req.socket.destroy()
return
}
// 发送请求
send(req, res, status, headers, msg)
}
原来这里才是响应的实际地点,在保证无错误并且响应未手动提前发送的情况下,调用本地方法发送请求。
这里的send过程十分繁杂,暂时不想深究,直接看最终的发送代码:
function write () {
// response body
var body = createHtmlDocument(message)
// response status
res.statusCode = status
res.statusMessage = statuses[status]
// response headers
setHeaders(res, headers)
// security headers
res.setHeader('Content-Security-Policy', "default-src 'self'")
res.setHeader('X-Content-Type-Options', 'nosniff')
// standard headers
res.setHeader('Content-Type', 'text/html; charset=utf-8')
res.setHeader('Content-Length', Buffer.byteLength(body, 'utf8'))
// 只请求页面的首部
if (req.method === 'HEAD') {
res.end()
return
}
res.end(body, 'utf8')
}
因为注释都解释的很明白了,所以这里简单的贴一下代码,最终调用的是node的原生res.end进行响应。
至此,基本上完事了。
.9-浅析express源码之请求处理流程(2)的更多相关文章
- .8-浅析express源码之请求处理流程(1)
这一节就讲从一个请求到来,express内部是如何将其转交给合适的路由,路由又是如何调用中间件的. 以express-generator为例,关键代码如下: // app.js app.use('/' ...
- .5-浅析express源码之Router模块(1)-默认中间件
模块application已经完结,开始讲Router路由部分. 切入口仍然在application模块中,方法就是那个随处可见的lazyrouter. 基本上除了初始化init方法,其余的app.u ...
- 渣渣菜鸡的 ElasticSearch 源码解析 —— 启动流程(下)
关注我 转载请务必注明原创地址为:http://www.54tianzhisheng.cn/2018/08/12/es-code03/ 前提 上篇文章写完了 ES 流程启动的一部分,main 方法都入 ...
- 渣渣菜鸡的 ElasticSearch 源码解析 —— 启动流程(上)
关注我 转载请务必注明原创地址为:http://www.54tianzhisheng.cn/2018/08/11/es-code02/ 前提 上篇文章写了 ElasticSearch 源码解析 -- ...
- Netty 源码学习——客户端流程分析
Netty 源码学习--客户端流程分析 友情提醒: 需要观看者具备一些 NIO 的知识,否则看起来有的地方可能会不明白. 使用版本依赖 <dependency> <groupId&g ...
- apiserver源码分析——启动流程
前言 apiserver是k8s控制面的一个组件,在众多组件中唯一一个对接etcd,对外暴露http服务的形式为k8s中各种资源提供增删改查等服务.它是RESTful风格,每个资源的URI都会形如 / ...
- express源码分析之Router
express作为nodejs平台下非常流行的web框架,相信大家都对其已经很熟悉了,对于express的使用这里不再多说,如有需要可以移步到www.expressjs.com自行查看express的 ...
- 从express源码中探析其路由机制
引言 在web开发中,一个简化的处理流程就是:客户端发起请求,然后服务端进行处理,最后返回相关数据.不管对于哪种语言哪种框架,除去细节的处理,简化后的模型都是一样的.客户端要发起请求,首先需要一个标识 ...
- .7-浅析express源码之Router模块(3)-app[METHODS]
之前的讨论都局限于use方法,所有方式的请求会被通过,这一节讨论express内部如何处理特殊请求方法. 给个流程图咯~ 分别给出app.METHODS与router.METHODS: // app. ...
随机推荐
- kubernetes入门(05)kubernetes的核心概念(2)
一.使用 kubectl run 创建 pod(容器) 命令 kubectl run类似于 docker run,可以方便的创建一个容器(实际上创建的是一个由deployment来管理的Pod): 等 ...
- Docker加速器(阿里云)
1. 登录阿里开发者平台: https://dev.aliyun.com/search.html,https://cr.console.aliyun.com/#/accelerator,生成专属链接 ...
- SpringBoot应用的前台目录
一.两个重要目录 templates:存放web页面的模板文件,需要在controller返回视图名称,框架转发才能找到的html. static :存放静态资源,如:html(放在这里可直接访问,如 ...
- SiteMesh入门(1-1)SiteMesh是什么?
1.问题的提出 在开发Web 应用时,Web页面可能由不同的人参与开发,因此开发出来的界面通常千奇百怪.五花八门,风格难以保持一致. 为了统一界面的风格,Struts 框架提供了一个标签库Tiles ...
- centos虚拟机nat模式,可以上内网,不能上外网
http://sky425509.iteye.com/blog/1996085 我这边的问题是,好久没用虚拟机了,重启之后,变成了dhcp模式,整个网卡配置变了. 重新配置了静态ip,网关,dns后才 ...
- 二、配置QtDesigner、PyUIC及PyRcc
配置QtDesigner.PyUIC及PyRcc 安装完PyQt 5 及PyQt5-tools 后,则需要在Pycharm中配置QtDesigner.PyUIC及PyRcc. 配置QtDesigner ...
- Struts(十二):异常处理:exception-mapping元素
配置当前action的声明异常处理 1.exception-mapping元素中有2个属性 exception:指定需要捕获的异常类型 result:指定一个响应结果,该结果将在捕获到异常时被执行.即 ...
- Python系列 - 进程和线程
进程和线程 可以通过ucos-Ⅱ来学习相关的基础,很好的学习资料 进程 假如有两个程序A和B,程序A在执行到一半的过程中,需要读取大量的数据输入(I/O操作), 而此时CPU只能静静地等待任务A读取完 ...
- 06、NetCore2.0依赖注入(DI)之整合Autofac
06.NetCore2.0依赖注入(DI)之整合Autofac 除了使用NetCore2.0系统的依赖注入(DI)框架外,我们还可以使用其他成熟的DI框架,如Autofac.Unity等.只要他们支持 ...
- 【java】doc转pdf
市场上主流的 WORD 转 PDF 工具有两个:OpenOffice 和 Microsoft Office 转换插件,可以通过部署这两个工具实现 WORD 转 PDF 功能. 1: Microsoft ...