模塊application已經完結,開始講Router路由部分。 切入口仍然在application模塊中,方法就是那個隨處可見的lazyrouter。 基本上除了初始化init方法,其餘的app.use、app.route、app.param等等,所有涉及到路由的方法都會調用一次這個函數,用來初始 ...
模塊application已經完結,開始講Router路由部分。
切入口仍然在application模塊中,方法就是那個隨處可見的lazyrouter。
基本上除了初始化init方法,其餘的app.use、app.route、app.param等等,所有涉及到路由的方法都會調用一次這個函數,用來初始化一個應用的內部路由。
而這個內部路由對於每個應用來說是唯一的,可以看下源碼:
app.lazyrouter = function lazyrouter() { if (!this._router) { // 生成一個實例 this._router = new Router({ caseSensitive: this.enabled('case sensitive routing'), strict: this.enabled('strict routing') }); // params解析中間件調用 this._router.use(query(this.get('query parser fn'))); // express框架自定義的內部中間件 this._router.use(middleware.init(this)); } };
很清晰的步驟,在生成一個Router實例後,調用了兩個中間件。
這裡有一個問題,為什麼不在初始化的函數中直接生成一個預設路由呢?
原因在於設置路由的相關參數需要調用app.set方法,這個方法明顯需要有app實例,如果在獲取app實例的時候就初始化了一個路由,這個路由的參數就沒辦法配置了。因此,在獲取app實例後,必須先對路由參數進行配置,然後再調用對應的app.use等方法。
簡單看一眼構造函數:
var proto = module.exports = function(options) { var opts = options || {}; // 跟app一樣的函數 function router(req, res, next) { router.handle(req, res, next); } // 原型方法掛載 setPrototypeOf(router, proto) router.params = {}; router._params = []; /** * caseSensitive => 區分大小寫 /foo vs /Foo * mergeParmas => 保留父路由參數 * strict => 嚴格模式 /foo vs /foo/ */ router.caseSensitive = opts.caseSensitive; router.mergeParams = opts.mergeParams; router.strict = opts.strict; router.stack = []; return router; };
預設情況下,三個參數的值均為undefined,構造函數沒有任何初始化的操作,直接返回了router函數。
接下來是兩個中間件。
query
先把那行代碼單獨貼出來:
// var query = require('./middleware/query'); this._router.use(query(this.get('query parser fn')));
前面有講解3個特殊鍵的set會觸發對應的compile方法設置fn,這裡的query parser fn就是之一。
預設情況下,query parser值為extended,對應的query parser fn為qs.parse方法,因此這裡query方法的參數為一個函數。
看一眼query方法:
module.exports = function query(options) { // options為函數 merge後opts也是函數 var opts = merge({}, options) var queryparse = qs.parse; // 參數修正 if (typeof options === 'function') { queryparse = options; opts = undefined; } // 相容 設置配置參數 if (opts !== undefined && opts.allowPrototypes === undefined) opts.allowPrototypes = true; // 中間件標準結構 return function query(req, res, next) { if (!req.query) { var val = parseUrl(req).query; req.query = queryparse(val, opts); } next(); }; };
這裡的形參options既可以是配置參數,也可以是預設的解析方法。
如果將query parser設為false,這裡的options就是一個空對象,express還是會指定一個parser,即源碼中的qs.parse。搞了半天,設置false或者extended都是預設的qs.parse。
在確實了對應的parse方法與參數後,就開始進行url解析,先處理url,獲取query參數,再解析query設置到req對象上。
parseUrl
講這個之前,需要稍微理解下nodejs的url模塊,特別是Url與URL。
這兩東西在網上沒查到詳細的區別,通過試API,發現差別還挺大:
1、Url為遺留API,構造函數不接受參數,通過無參構造後,可以調用parse方法解析一個url路徑來獲得一個實例,實例屬性包含protocol、auth等一系列東西。
2、URL為WHATWG API,推薦使用的新API,可以直接通過new操作傳一個url進去獲得實例,屬性同樣包含那些,但是在鍵名與分類略有區別。
詳細情況可見:http://nodejs.cn/api/url.html#url_url_strings_and_url_objects。
雖然URL是新東西而且node推薦使用,但是在express源碼的這個方法中依然使用的是老Url,入口函數如下:
function parseurl(req) { // 這個屬性是原生的 var url = req.url; if (url === undefined) return undefined; // 嘗試獲取緩存屬性 var parsed = req._parsedUrl; // 判斷有沒有緩存 if (fresh(url, parsed)) return parsed; // 解析url parsed = fastparse(url); parsed._raw = url; // 添加緩存並返回結果 return (req._parsedUrl = parsed) };
所有的解析都基於一個原生的屬性,即req.url,該屬性返回請求的原始URL。
這裡的獲取緩存就不看了,比較簡單,直接看如何快速解析url路徑:
function fastparse(str) { // 當路徑結構為純path(例如:/path/ext?a=1)時,直接調用node原生的parse方法 if (typeof str !== 'string' || str.charCodeAt(0) !== 0x2f /* / */ ) { return parse(str) } var pathname = str var query = null var search = null // This takes the regexp from https://github.com/joyent/node/pull/7878 // 這個issue主要講當url是純路徑時 用node原生的Url.parse會更快 for (var i = 1; i < str.length; i++) { switch (str.charCodeAt(i)) { /** * 遇到問號開始切割路徑 * http://www.baidu.com?a=1 => * { * pathname: http://www.baidu.com, * query: a=1, * search: ?a=1, * } */ case 0x3f: /* ? */ if (search === null) { pathname = str.substring(0, i) query = str.substring(i + 1) search = str.substring(i) } break // 遇到其餘不合理的情況調用原生方法 case 0x09: /* \t */ case 0x0a: /* \n */ case 0x0c: /* \f */ case 0x0d: /* \r */ case 0x20: /* */ case 0x23: /* # */ case 0xa0: case 0xfeff: return parse(str) } } // 生成一個Url對象或者空對象 var url = Url !== undefined ? new Url() : {}; // 添加對應的屬性 url.path = str url.href = str url.pathname = pathname url.query = query url.search = search return url }
看似很長,實則很簡單。簡單來說,就是根據問號來切割url,特殊情況就全部扔給內置模塊解析,最後返回url對象。
在獲取到對應的url尾部參數後,調用parser方法解析生成一個參數對象掛載到req上,所以在實際應用中,我們可以直接調用req.query來得到請求參數值。
middleware.init
這個中間件是express自定義的,也不知道叫什麼,所以直接用調用名作為小標題了。
源碼如下:
exports.init = function(app) { return function expressInit(req, res, next) { // 這玩意兒預設生效的 if (app.enabled('x-powered-by')) res.setHeader('X-Powered-By', 'Express'); // 屬性各種掛載 req.res = res; res.req = req; req.next = next; // 本地模塊原型設置 setPrototypeOf(req, app.request) setPrototypeOf(res, app.response) res.locals = res.locals || Object.create(null); next(); }; };
這個中間件的主要作用就是把內置模塊的屬性、方法全部加到原生的req、res上面去,後面就能使用express的方法了。
解析完畢。