如何详细解析Express框架中加载中间件的机制及其应用场景?

2026-04-01 12:501阅读0评论SEO资讯
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计1503个文字,预计阅读时间需要7分钟。

如何详细解析Express框架中加载中间件的机制及其应用场景?

目录+前言+Express中间件加载+总结+前言+作为Node web框架的鼻祖,Express和Koa都是每个写Node.js的都会使用的两个框架。那么这两个框架在中间件的加载机制上有什么区别?Koa的洋葱模型究竟是什么?

目录
  • 前言
  • 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在处理请求时,寿险调用的是express app 的handle方法,该方法比较简单,核心逻辑是调用router.handle(req, res, done)方法

本文共计1503个文字,预计阅读时间需要7分钟。

如何详细解析Express框架中加载中间件的机制及其应用场景?

目录+前言+Express中间件加载+总结+前言+作为Node web框架的鼻祖,Express和Koa都是每个写Node.js的都会使用的两个框架。那么这两个框架在中间件的加载机制上有什么区别?Koa的洋葱模型究竟是什么?

目录
  • 前言
  • 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在处理请求时,寿险调用的是express app 的handle方法,该方法比较简单,核心逻辑是调用router.handle(req, res, done)方法