express中間件加載機(jī)制示例詳解
前言
作為node web 框架的鼻祖,express和koa 是每個寫node的同學(xué)都會使用的兩個框架,那么兩個框架在中間件的加載機(jī)制上有什么區(qū)別?koa的洋蔥模型到底是什么?midway在這兩個框架之上又做了怎么樣的封裝?本文將帶走進(jìn)這個一點兒都不神奇的世界~
express 中間件加載
眾所周知,express定義中間件的時候,使用use方法即可,那么use方法到底做了些什么呢?讓筆者帶你來扒一扒源碼。 github.com/expressjs/e… 由于原始代碼較長,這里小編就拆開分解來解讀。
var offset = 0; var path = '/'; // default path to '/' // disambiguate app.use([fn]) if (typeof fn !== 'function') { var arg = fn; while (Array.isArray(arg) && arg.length !== 0) { arg = arg[0]; } // first arg is the path if (typeof arg !== 'function') { offset = 1; path = fn; } } var fns = flatten(slice.call(arguments, offset)); if (fns.length === 0) { throw new TypeError('app.use() requires a middleware function') }
這部分對應(yīng)源碼的195-218行,主要是獲取需要執(zhí)行的function,以及區(qū)分,傳入的是中間件,還是路由。 通過源碼可知,用戶在傳入的第一個參數(shù),如果不是function,則會判斷是不是數(shù)組,如果是數(shù)組的情況下,就會判斷數(shù)組的第0項是不是function,這部分邏輯是做什么呢? 這部分是對入?yún)⒌募嫒?,因為express的入?yún)⒖梢杂卸喾N形式,如下:
app.use('/users', usersRouter); app.use([function (req, res, next) { console.log('middleware 1....'); next(); }, function (req, res, next) { console.log('middleWare 2....'); next(); }]) // catch 404 and forward to error handler app.use(function (req, res, next) { next(); next(createError(404)); });
用戶可以傳入多中間件,也可以傳入單中間件,以及傳入路由。這部分代碼就是對這幾種情況的區(qū)分,明確之后用戶傳入的內(nèi)容到底是什么,然后再對其進(jìn)行針對性的處理。
// setup router this.lazyrouter(); var router = this._router;
這一部分是路由的準(zhǔn)備工作,由于use方法允許用戶創(chuàng)建路由,則需要在對其進(jìn)行處理之前,先初始化路由。這部分暫時不詳細(xì)展開說,待有緣再進(jìn)行詳細(xì)講解。
接下來就是中間件的的詳細(xì)處理邏輯
fns.forEach(function (fn) { // non-express app if (!fn || !fn.handle || !fn.set) { return router.use(path, fn); } debug('.use app under %s', path); fn.mountpath = path; fn.parent = this; // restore .app property on req and res router.use(path, function mounted_app(req, res, next) { var orig = req.app; fn.handle(req, res, function (err) { setPrototypeOf(req, orig.request) setPrototypeOf(res, orig.response) next(err); }); }); // mounted an app fn.emit('mount', this); }, this);
這里第一個if中的判斷就很有意思,如果fn不存在,或者不存在fn.handle, 或者不存在fn.set,那么就會直接return router.use(path, fn);
那么什么情況下會發(fā)生這種情況呢?好像我們上邊寫的中間件,路由都滿足這種情況,難不成中間件就是路由?而實際執(zhí)行debug的時候,也確實發(fā)現(xiàn),所有的我們定義的中間件,都走了return router.use(path, fn);這個方法,很神奇。
而什么情況下會走到下邊的方法呢?
當(dāng)傳入的function具有handle和set方法時,則會認(rèn)為執(zhí)行下邊的方法,同樣也是執(zhí)行router.use();
事已至此,如果不了解router到底做了什么,是不可能弄明白中間件加載機(jī)制了,好吧,那么我們就順藤摸瓜,前來看看router模塊都做了些什么事情吧。
書接上文,app.lazyrouter 是對路由進(jìn)行初始化,詳細(xì)代碼如下
app.lazyrouter = function lazyrouter() { if (!this._router) { this._router = new Router({ caseSensitive: this.enabled('case sensitive routing'), strict: this.enabled('strict routing') }); this._router.use(query(this.get('query parser fn'))); this._router.use(middleware.init(this)); } };
壽險判斷 _router是否存在,防止重復(fù)創(chuàng)建。
接下來就是router定義的詳細(xì)邏輯
var proto = module.exports = function(options) { var opts = options || {}; function router(req, res, next) { router.handle(req, res, next); } // mixin Router class functions setPrototypeOf(router, proto) router.params = {}; router._params = []; router.caseSensitive = opts.caseSensitive; router.mergeParams = opts.mergeParams; router.strict = opts.strict; router.stack = []; return router; };
初始化router對象,并且對其進(jìn)行初始化賦值。針對上文中使用的router.use,我們來看看其具體都做了什么吧。
由于use方法較長,我們也是拆分開來進(jìn)行探索。
var offset = 0; var path = '/'; // default path to '/' // disambiguate router.use([fn]) if (typeof fn !== 'function') { var arg = fn; while (Array.isArray(arg) && arg.length !== 0) { arg = arg[0]; } // first arg is the path if (typeof arg !== 'function') { offset = 1; path = fn; } } var callbacks = flatten(slice.call(arguments, offset));
這部分代碼與app.use代碼基本上是一致的,只是最后一個函數(shù)改了名字。這里就不再進(jìn)行詳細(xì)贅述。
接下來就是重中之重了
for (var i = 0; i < callbacks.length; i++) { var fn = callbacks[i]; if (typeof fn !== 'function') { throw new TypeError('Router.use() requires a middleware function but got a ' + gettype(fn)) } // add the middleware debug('use %o %s', path, fn.name || '<anonymous>') var layer = new Layer(path, { sensitive: this.caseSensitive, strict: false, end: false }, fn); layer.route = undefined; this.stack.push(layer); }
這部分代碼對于傳入的函數(shù)進(jìn)行了遍歷,然后對每一個function都新建了一個layer層。然后將layer放入了棧中,如果不出意外在真正調(diào)用的時候,將會執(zhí)行遍歷這個棧中的所有l(wèi)ayer,然后對其進(jìn)行遍歷執(zhí)行。
function Layer(path, options, fn) { if (!(this instanceof Layer)) { return new Layer(path, options, fn); } debug('new %o', path) var opts = options || {}; this.handle = fn; this.name = fn.name || '<anonymous>'; this.params = undefined; this.path = undefined; this.regexp = pathRegexp(path, this.keys = [], opts); // set fast path flags this.regexp.fast_star = path === '*' this.regexp.fast_slash = path === '/' && opts.end === false }
layer代碼相對簡單,定義了handle和regexp,并且設(shè)置了兩個快速檢索的flag。
那么真正調(diào)用的時候真的如我們想象的那樣嗎?真正的url請求來了以后express是如何處理的呢?
express在處理請求時,壽險調(diào)用的是express app 的handle方法,該方法比較簡單,核心邏輯是調(diào)用router.handle(req, res, done)
方法??,接下來我們就一起扒一扒route的handle方法吧~這段二百行的代碼,究竟做了些什么?好吧,代碼行確實太多了,相信你也不愿因看我的流水賬,接下來我就將代碼進(jìn)行一下歸納吧
proto.handle = function handle(req, res, out) { var self = this; var idx = 0; // middleware and routes var stack = self.stack; req.next = next; next(); function next(err) { var layer; var match; var route; // 找到match的layer while (match !== true && idx < stack.length) { layer = stack[idx++]; match = matchLayer(layer, path); route = layer.route; if (match !== true) { continue; } if (!route) { // process non-route handlers normally continue; } } // this should be done for the layer self.process_params(layer, paramcalled, req, res, function (err) { if (err) { return next(layerError || err); } if (route) { // 執(zhí)行l(wèi)ayer的handle_request方法,其實就是中間件傳入的函數(shù) return layer.handle_request(req, res, next); } trim_prefix(layer, layerError, layerPath, path); });
這個方法的原理其實很簡單,初始化idx=0,然后while循環(huán)找到第一個match的方法,就是我們定義的中間件/路由,然后執(zhí)行相對應(yīng)的function。
matchLayer方法中用到了layer初始化的時候定義的this.regexp.fast_slash變量
if (this.regexp.fast_slash) { this.params = {} this.path = '' return true } -------------------- this.regexp.fast_slash = path === '/' && opts.end === false
通過這個代碼以及fast_flash定義,以及上邊path的定義我們可以知道,我們初始化的中間件,全部都是以var path = '/';
的方式存儲的,layer初始化時傳入的end=false, 所以中間件的 this.regexp.fast_slash = true,即所有的中間件在所有的路由下都會執(zhí)行。
按照這個執(zhí)行邏輯,如果我們自定義一個path='/'的路由,是不是也都會執(zhí)行呢?以及如果出現(xiàn)兩個相同名字的路由,會怎么處理呢?按照這個推論,我測試了如下代碼
app.use('/', function (req, res, next) { console.log('hello world'); next() }); app.use('/users', function (req, res, next) { console.log('/users-------'); next(); }); //hello world // /users------- //GET /users 304 1.280 ms - -
試驗結(jié)果與我們的推論一致。
然而,當(dāng)我在測試的時候,發(fā)現(xiàn)中間件寫的位置也會有影響,寫在router之后的中間件就不會被執(zhí)行到,這個是什么原因呢? 通過看源碼發(fā)現(xiàn),在路由處理時,執(zhí)行了res.send ,之后并未執(zhí)行next()命令,導(dǎo)致其之后的代碼并未執(zhí)行。
總結(jié): express使用use方法加載中間件,中間件和路由以layer的形式保存到stack中,待真正需要使用的時候,再對其進(jìn)行遍歷,找到真正需要用到的中間件和路由。我們還可以通過路由的加載順序,攔截路由。
好吧,硬肝了兩天,終于把express的中間件加載機(jī)制給肝完了,邏輯層層深入,柳暗花明,其中還有很多地方值得深思,比如app.use方法那里傳入的到底還能是什么呢?留給有興趣的讀者深入研究吧。
總結(jié)
到此這篇關(guān)于express中間件加載機(jī)制的文章就介紹到這了,更多相關(guān)express中間件加載內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用upstart把nodejs應(yīng)用封裝為系統(tǒng)服務(wù)實例
這篇文章主要介紹了使用upstart把nodejs應(yīng)用封裝為系統(tǒng)服務(wù)實例,需要的朋友可以參考下2014-06-06NodeJS、NPM安裝配置步驟(windows版本) 以及環(huán)境變量詳解
本篇文章主要介紹了NodeJS、NPM安裝配置步驟(windows版本) 以及環(huán)境變量詳解,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-05-05初識NodeJS服務(wù)端開發(fā)入門(Express+MySQL)
本篇文章主要介紹了初識NodeJS服務(wù)端開發(fā)入門(Express+MySQL),可以對數(shù)據(jù)庫中的一張表進(jìn)行簡單的CRUD操作,有興趣的可以了解一下。2017-04-04Node.js中path.join()優(yōu)勢例舉分析
在本篇文章里小編給大家整理的是一篇關(guān)于Node.js中path.join()優(yōu)勢例舉分析,有興趣的朋友們可以學(xué)習(xí)下。2021-08-08