express 中间件加载机制
作为node web 框架的鼻祖,express和koa 是每个写node的同学都会使用的两个框架,那么两个框架在中间件的加载机制上有什么区别?koa的洋葱模型到底是什么?midway在这两个框架之上又做了怎么样的封装?本文将带走进这个一点儿都不神奇的世界~
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')
}
这部分对应源码的195-218行,主要是获取需要执行的function,以及区分,传入的是中间件,还是路由。 通过源码可知,用户在传入的第一个参数,如果不是function,则会判断是不是数组,如果是数组的情况下,就会判断数组的第0项是不是function,这部分逻辑是做什么呢? 这部分是对入参的兼容,因为express的入参可以有多种形式,如下:
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));
});
用户可以传入多中间件,也可以传入单中间件,以及传入路由。这部分代码就是对这几种情况的区分,明确之后用户传入的内容到底是什么,然后再对其进行针对性的处理。
// setup router
this.lazyrouter();
var router = this._router;
这一部分是路由的准备工作,由于use方法允许用户创建路由,则需要在对其进行处理之前,先初始化路由。这部分暂时不详细展开说,待有缘再进行详细讲解。 接下来就是中间件的的详细处理逻辑
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); 那么什么情况下会发生这种情况呢?好像我们上边写的中间件,路由都满足这种情况,难不成中间件就是路由?而实际执行debug的时候,也确实发现,所有的我们定义的中间件,都走了return router.use(path, fn);这个方法,很神奇。 而什么情况下会走到下边的方法呢? 当传入的function具有handle和set方法时,则会认为执行下边的方法,同样也是执行router.use(); 事已至此,如果不了解router到底做了什么,是不可能弄明白中间件加载机制了,好吧,那么我们就顺藤摸瓜,前来看看router模块都做了些什么事情吧。
书接上文,app.lazyrouter 是对路由进行初始化,详细代码如下
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是否存在,防止重复创建。 接下来就是router定义的详细逻辑
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对象,并且对其进行初始化赋值。针对上文中使用的router.use,我们来看看其具体都做了什么吧。 由于use方法较长,我们也是拆分开来进行探索。
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代码基本上是一致的,只是最后一个函数改了名字。这里就不再进行详细赘述。
接下来就是重中之重了
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);
}
这部分代码对于传入的函数进行了遍历,然后对每一个function都新建了一个layer层。然后将layer放入了栈中,如果不出意外在真正调用的时候,将会执行遍历这个栈中的所有layer,然后对其进行遍历执行。
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,并且设置了两个快速检索的flag。
那么真正调用的时候真的如我们想象的那样吗?真正的url请求来了以后express是如何处理的呢?
express在处理请求时,寿险调用的是express app 的handle方法,该方法比较简单,核心逻辑是调用router.handle(req, res, done)
方法🐶,接下来我们就一起扒一扒route的handle方法吧~这段二百行的代码,究竟做了些什么?好吧,代码行确实太多了,相信你也不愿因看我的流水账,接下来我就将代码进行一下归纳吧
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) {
// 执行layer的handle_request方法,其实就是中间件传入的函数
return layer.handle_request(req, res, next);
}
trim_prefix(layer, layerError, layerPath, path);
});
这个方法的原理其实很简单,初始化idx=0,然后while循环找到第一个match的方法,就是我们定义的中间件/路由,然后执行相对应的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,即所有的中间件在所有的路由下都会执行。
按照这个执行逻辑,如果我们自定义一个path='/'的路由,是不是也都会执行呢?以及如果出现两个相同名字的路由,会怎么处理呢?按照这个推论,我测试了如下代码
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 - -
试验结果与我们的推论一致。
然而,当我在测试的时候,发现中间件写的位置也会有影响,写在router之后的中间件就不会被执行到,这个是什么原因呢? 通过看源码发现,在路由处理时,执行了res.send ,之后并未执行next()命令,导致其之后的代码并未执行。
总结: express使用use方法加载中间件,中间件和路由以layer的形式保存到stack中,待真正需要使用的时候,再对其进行遍历,找到真正需要用到的中间件和路由。我们还可以通过路由的加载顺序,拦截路由。
好吧,硬肝了两天,终于把express的中间件加载机制给肝完了,逻辑层层深入,柳暗花明,其中还有很多地方值得深思,比如app.use方法那里传入的到底还能是什么呢?留给有兴趣的读者深入研究吧。
一遍读代码,一遍写的blog,可能读起来像是流水账一样,以后有时间再来整理一下吧。
转载自:https://juejin.cn/post/7132710712777900046