深入理解 Koa2 中间件机制
我们知道,Koa 中间件是以级联代码(Cascading) 的方式来执行的。类似于回形针的方式,可参照下面这张图:
今天这篇文章就来分析 Koa 的中间件是如何实现级联执行的。
在 koa 中,要应用一个中间件,我们使用 app.use()
:
app .use(logger()) .use(bodyParser()) .use(helmet())
先来看看use()
是什么,它的源码如下:
use(fn) { if (typeof fn !== 'function') throw new TypeError('middleware must be a function!'); if (isGeneratorFunction(fn)) { deprecate('Support for generators will be removed in v3. ' + 'See the documentation for examples of how to convert old middleware ' + 'https://github.com/koajs/koa/blob/master/docs/migration.md'); fn = convert(fn); } debug('use %s', fn._name || fn.name || '-'); this.middleware.push(fn); return this; }
这个函数的作用在于将调用 use(fn)
方法中的参数(不管是普通的函数或者是中间件)都添加到 this.middlware
这个数组中。
在 Koa2
中,还对 Generator
语法的中间件做了兼容,使用 isGeneratorFunction(fn)
这个方法来判断是否为 Generator
语法,并通过 convert(fn)
这个方法进行了转换,转换成 async/await
语法。然后把所有的中间件都添加到了 this.middleware
,最后通过 callback()
这个方法执行。callback() 源码如下:
/** * Return a request handler callback * for node's native http server. * * @return {Function} * @api public */ callback() { const fn = compose(this.middleware); if (!this.listeners('error').length) this.on('error', this.onerror); const handleRequest = (req, res) => { const ctx = this.createContext(req, res); return this.handleRequest(ctx, fn); }; return handleRequest; }
源码中,通过 compose()
这个方法,就能将我们传入的中间件数组转换并级联执行,最后 callback()
返回this.handleRequest()
的执行结果。返回的是什么内容我们暂且不关心,我们先来看看 compose()
这个方法做了什么事情,能使得传入的中间件能够级联执行,并返回 Promise
。
compose()
是 koa2 实现中间件级联调用的一个库,叫做 koa-compose。源码很简单,只有一个函数,如下:
/** * Compose `middleware` returning * a fully valid middleware comprised * of all those which are passed. * * @param {Array} middleware * @return {Function} * @api public */ function compose (middleware) { if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!') for (const fn of middleware) { if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!') } /** * @param {Object} context * @return {Promise} * @api public */ return function (context, next) { // 记录上一次执行中间件的位置 # let index = -1 return dispatch(0) function dispatch (i) { // 理论上 i 会大于 index,因为每次执行一次都会把 i递增, // 如果相等或者小于,则说明next()执行了多次 if (i <= index) return Promise.reject(new Error('next() called multiple times')) index = i // 取到当前的中间件 let fn = middleware[i] if (i === middleware.length) fn = next if (!fn) return Promise.resolve() try { return Promise.resolve(fn(context, function next () { return dispatch(i + 1) })) } catch (err) { return Promise.reject(err) } } } }
可以看到 compose()
返回一个匿名函数的结果,该匿名函数自执行了 dispatch()
这个函数,并传入了0作为参数。
来看看 dispatch(i)
这个函数都做了什么事?i
作为该函数的参数,用于获取到当前下标的中间件。在上面的 dispatch(0)
传入了0,用于获取 middleware[0]
中间件。
首先显示判断 i<==index
,如果 true
的话,则说明 next()
方法调用多次。为什么可以这么判断呢?等我们解释了所有的逻辑后再来回答这个问题。
接下来将当前的 i
赋值给 index
,记录当前执行中间件的下标,并对 fn
进行赋值,获得中间件。
index = i; let fn = middleware[i]
获得中间件后,怎么使用?
try { return Promise.resolve(fn(context, function next () { return dispatch(i + 1) })) } catch (err) { return Promise.reject(err) }
上面的代码执行了中间件 fn(context, next)
,并传递了 context
和 next
函数两个参数。context
就是 koa
中的上下文对象 context
。至于 next
函数则是返回一个 dispatch(i+1)
的执行结果。值得一提的是 i+1
这个参数,传递这个参数就相当于执行了下一个中间件,从而形成递归调用。
这也就是为什么我们在自己写中间件的时候,需要手动执行
await next()
只有执行了 next
函数,才能正确得执行下一个中间件。
因此每个中间件只能执行一次 next
,如果在一个中间件内多次执行 next
,就会出现问题。回到前面说的那个问题,为什么说通过 i<=index
就可以判断 next
执行多次?
因为正常情况下 index
必定会小于等于 i
。如果在一个中间件中调用多次 next
,会导致多次执行 dispatch(i+1)
。从代码上来看,每个中间件都有属于自己的一个闭包作用域,同一个中间件的 i
是不变的,而 index
是在闭包作用域外面的。
当第一个中间件即 dispatch(0)
的 next()
调用时,此时应该是执行 dispatch(1)
,在执行到下面这个判断的时候,
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
此时的 index
的值是0,而 i
的值是1,不满足 i<=index
这个条件,继续执行下面的 index=i
的赋值,此时 index
的值为1。但是如果第一个中间件内部又多执行了一次 next()
的话,此时又会执行 dispatch(2)
。上面说到,同一个中间件内的 i
的值是不变的,所以此时 i
的值依然是1,所以导致了 i <= index
的情况。
可能会有人有疑问?既然 async
本身返回的就是 Promise
,为什么还要在使用 Promise.resolve()
包一层呢。这是为了兼容普通函数,使得普通函数也能正常使用。
再回到中间件的执行机制,来看看具体是怎么回事。
我们知道 async
的执行机制是:只有当所有的 await
异步都执行完之后才能返回一个 Promise
。所以当我们用 async
的语法写中间件的时候,执行流程大致如下:
- 先执行第一个中间件(因为
compose
会默认执行dispatch(0)
),该中间件返回Promise
,然后被koa
监听,执行对应的逻辑(成功或失败) - 在执行第一个中间件的逻辑时,遇到
await next()
时,会继续执行dispatch(i+1)
,也就是执行dispatch(1)
,会手动触发执行第二个中间件。这时候,第一个中间件await next()
后面的代码就会被 pending,等待await next()
返回Promise
,才会继续执行第一个中间件await next()
后面的代码。 - 同样的在执行第二个中间件的时候,遇到
await next()
的时候,会手动执行第三个中间件,await next()
后面的代码依然被 pending,等待await
下一个中间件的Promise.resolve
。只有在接收到第三个中间件的resolve
后才会执行后面的代码,然后第二个中间会返回Promise
,被第一个中间件的await
捕获,这时候才会执行第一个中间件的后续代码,然后再返回Promise
- 以此类推,如果有多个中间件的时候,会依照上面的逻辑不断执行,先执行第一个中间件,在
await next()
出 pending,继续执行第二个中间件,继续在await next()
出 pending,继续执行第三个中间,直到最后一个中间件执行完,然后返回 Promise,然后倒数第二个中间件才执行后续的代码并返回Promise,然后是倒数第三个中间件,接着一直以这种方式执行直到第一个中间件执行完,并返回Promise
,从而实现文章开头那张图的执行顺序。
通过上面的分析之后,如果你要写一个 koa2 的中间件,那么基本格式应该就长下面这样:
async function koaMiddleware(ctx, next){ try{ // do something await next() // do something } .catch(err){ // handle err } }
最近正在使用 koa2 + React 写一个博客,有兴趣的同学可以前往 GitHub 地址查看:koa-blog-api