上節漏了幾個地方沒有講。 1、process_params 2、trim_prefix 3、done 分別是動態路由,深層路由與最終回調。 這節就只講這三個地方,案例還是express-generator,不過請求的方式更為複雜。 process_params 在講這個函數之前,需要先進一下path ...
上節漏了幾個地方沒有講。
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進行響應。
至此,基本上完事了。