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) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch (i) {
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)
}
}
}
}
运行流程
1.
new app()
首先koa中主要有
app
,context
,response
,request
四个基类,我们在实例化app的时候,实际上就是初始化了一些app中的属性。2.
app.listen()
在我们执行
app.listen()
时,主要做了createServer
,中间件用compose
串起来。这里面的
compose
是Koa洋葱圈模型的实现关键,后面会详细介绍3.handle request
当请求来临时,koa首先
Object.create(this.context)
,创建一个我们常用ctx对象,然后执行中间件,最后respond(res.end(body)
)洋葱圈的实现
我们知道,koa中的比较重要的部分在于其中间件的挂载和执行,当请求到来时,中间件先顺序执行,再逆序执行,那么这在koa2.x中是如何实现的呢?
1.app.use()
首先当我们执行app.use时,将该中间件push到middleware队列中。
2.app.listen()
当执行listen时,执行
const fn = compose(this.middleware);
,compose执行返回一个函数fn,fn执行时,按队列中的顺序依次执行,传入参数ctx及next方法。那么中间件为什么会从前向后执行,然后再从后向前执行呢?
首先,我们在写中间时会有
await next()
的用法(注意,await会等到后面的Promise resolve或reject后才厚向下继续执行),那么执行await next()
就会转而执行dispatch(i + 1)
,直到最后一个中间件;当执行到最后一个再执行dispatch(i + 1)
时,会触发if (!fn) return Promise.resolve()
,最后一个中间件开始执行await next()
后面的逻辑,完成后,执行倒数第二个,依次执行到第一个中间件。注意,当中间件中有两处
await next()
时,会触发if (i <= index) return Promise.reject(new Error('next() called multiple times'))
,抛出错误。context/request/response
三者的关系引用深入浅出koa中的一张图。
其中,我们在使用ctx.body等属性或方法时,实际上调用的this.request.body等属性或方法,实际实现就是调用了delegate库,将request和response中一些常用属性和方法挂载到context对象上。
中间件的书写
koa中间件实现起来比较简单,只要实现一个带有ctx和next参数的一个函数即可。以koa-body为例。随便看一个中间件就好了
Koa1.0中的洋葱圈实现
Koa1.0中的中间还没有await和async,而是用的yield来实现,
yeild next
如何做到上述的顺序执行然后逆序呢?我们下面简单回顾一下。1.compose middleware
2.co.wrap()
这里相当于调用了co()方法,把我们之前的compose()函数返回的结果函数作为参数传给了它。
3.co()——逆向执行关键
注意,我们在写每个中间件时,实际都有yield next;onFulfilled这个函数只在两种情况下被调用,一种是调用co的时候执行,还有一种是当前promise中的所有逻辑都执行完毕后执行
这里我们传入的fn是一个generator对象,根据上述转换函数,将会继续调用co()函数,执行next()时,我们传入的参数ret.val是下一个中间件的generator对象,所以继续调用co()函数,如此递归的执行下去;当到最后一个中间件时,执行完成后,ret.done==true,会再次调用resolve,返回到上一层中间件。
这个过程其实就是递归调用的过程。