上节漏了几个地方没有讲。

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 matchif (path != null) {// ...快速匹配
match = this.regexp.exec(path)}if (!match) { /*...*/ }// 缓存paramsthis.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 = paramsif (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.paramsparamVal = req.params[name];// 后面讨论paramCallbacks = params[name];// 初始为空对象paramCalled = called[name];if (paramVal === undefined || !paramCallbacks) return param();// param previously called with same value or error occurredif (paramCalled && (paramCalled.match === paramVal || (paramCalled.error && paramCalled.error !== 'route'))) {// error...
        }// 设置值called[name] = paramCalled = {error: null,match: paramVal,value: paramVal};paramCallback();}// single param callbacksfunction paramCallback(err) {//...
    }param();
};

  这里除去遍历参数,有几个变量,稍微解释下:

1、paramVal => 请求路径带的路由参数

2、paramCallbacks => 调用router.params会填充该对象,请求带有指定路由参数会触发的回调函数

3、paramCalled => 一个标记对象

  当参数匹配之后,会调用回调函数paramCallback:

function paramCallback(err) {// 依次取出callback数组的fnvar fn = paramCallbacks[paramIndex++];// 标记valparamCalled.value = req.params[key.name];if (err) {// store errorparamCalled.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);// 缓存被移除的pathremoved = 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.onerrorreturn 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 headersvar msgvar status// 请求已发送的情况if (!err && headersSent(res)) {debug('cannot 404 after headers sent')return}// unhandled errorif (err) {// ...} else {// not foundstatus = 404msg = '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 bodyvar body = createHtmlDocument(message)// response statusres.statusCode = statusres.statusMessage = statuses[status]// response headers
  setHeaders(res, headers)// security headersres.setHeader('Content-Security-Policy', "default-src 'self'")res.setHeader('X-Content-Type-Options', 'nosniff')// standard headersres.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进行响应。

  至此,基本上完事了。

转载于:https://www.cnblogs.com/QH-Jimmy/p/8945483.html

.9-浅析express源码之请求处理流程(2)相关推荐

  1. express 源码阅读(全)

    1. 简介 这篇文章主要的目的是分析理解express的源码,网络上关于源码的分析已经数不胜数,这篇文章准备另辟蹊径,仿制一个express的轮子,通过测试驱动的开发方式不断迭代,正向理解expres ...

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

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

  3. 【Android 启动过程】Activity 启动源码分析 ( ActivityThread 流程分析 二 )

    文章目录 前言 一.ActivityManagerService.attachApplicationLocked 二.ActivityStackSupervisor.attachApplication ...

  4. [系统安全] 六.逆向分析之条件语句和循环语句源码还原及流程控制

    您可能之前看到过我写的类似文章,为什么还要重复撰写呢?只是想更好地帮助初学者了解病毒逆向分析和系统安全,更加成体系且不破坏之前的系列.因此,我重新开设了这个专栏,准备系统整理和深入学习系统安全.逆向分 ...

  5. [安全攻防进阶篇] 四.逆向分析之条件语句和循环语句源码还原及流程控制逆向

    从2019年7月开始,我来到了一个陌生的专业--网络空间安全.初入安全领域,是非常痛苦和难受的,要学的东西太多.涉及面太广,但好在自己通过分享100篇"网络安全自学"系列文章,艰难 ...

  6. 测试用例驱动阅读Express源码

    1.简介 Expres是基于Node.js平台,快速.开放.极简的web开发框架.(Expres中文官网首页原话).之所以引用这句话,是因为这句话简单明了的告知了大家,它到底是什么.基于Node的一个 ...

  7. 在线直播源码直播全流程探索

    在线直播源码直播全流程探索 在线直播源码直播全流程探索 生成阶段 生成阶段包括对音视频的采集和处理: 音视频的采集,采集阶段主要是对原始视频内容进行采集即直播内容的来源,根据应用场景的差别,我们可以分 ...

  8. Python源码学习:启动流程简析

    Python源码分析 本文环境python2.5系列 参考书籍<<Python源码剖析>> Python简介: python主要是动态语言,虽然Python语言也有编译,生成中 ...

  9. .17-浅析webpack源码之compile流程-入口函数run

    本节流程如图: 现在正式进入打包流程,起步方法为run: Compiler.prototype.run = (callback) => {const startTime = Date.now() ...

最新文章

  1. 江湖永在:金庸先生和阿里人的那些记忆
  2. 大家谈谈公司里的项目经理角色及职责都是干什么的?
  3. Leetcode1684. 统计一致字符串的数目[C++题解]:字符串O(n^2)简单题
  4. 自定义Android标题栏TitleBar布局
  5. 站着办公有助减轻体重
  6. 访问tomcat manager应用遇到的403 access denied错误
  7. java 数据库 事务 只读_java – odd SQLException – 无法检索转换只读状态服务器
  8. android 支付宝月账单 统计图_@三明人 支付宝年度账单来了!今天的你晒账单了吗?...
  9. 【Flink】改进的BLOB存储架构
  10. 测试工程师必备技能之缺陷分析
  11. 第四章 向量代数与空间解析几何
  12. 视觉中国:阶段性内部整改测试已结束,网站并未上线;易通贷平台因涉嫌非法吸收公众存款被立案侦查|嘟头条...
  13. Oracle前期准备
  14. selenium与自动化测试成神之路
  15. 微信测试公众号推送信息给女朋友(node版本)
  16. Python实现的《桌面视频壁纸程序 Mili Wallpaper》
  17. 顶级黑客泄密事件啼笑皆非
  18. 真·富文本编辑器的演进之路-富文本Span的边界探究
  19. 数码相框(五、使用freetype库在LCD显示几行文字)
  20. ora2og使用步骤

热门文章

  1. 祝贺!港中文助理教授周博磊宣布加入UCLA
  2. CVPR最佳作者新作!无监督学习可变形3D对象
  3. 谷歌开源EfficientNets:ImageNet准确率创纪录,效率提高10倍
  4. 新突破!CVPR2019接收论文:新的基于自编码变换的无监督表示学习方法—AET
  5. Facebook 开源图像处理库 Spectrum,优化移动端图像生成
  6. 【嵌入式工程师面试高频问题】你知道IIC吗(附程序说明)
  7. mysql 查询两张表结构相同的数据库_数据库原理习题(含答案)
  8. java实体类 判断 字段_java8 根据实体类中的某个字段对实体类去重
  9. Macbook Pro笔记本双系统MacOS和Windows切换默认启动
  10. 在 VMware ESXi 5.5 和 6.0.x 中支持大于 2 TB 的虚拟机磁盘 (2058287)