.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进行响应。
至此,基本上完事了。