大部分人用koa应该是用来实现后端服务的。后端服务最多见的就是实现接口了。但这些接口通常有一些相同的功能。例如日志打印请求时间,请求参数等。每写一个接口都写这些功能,不只浪费时间,代码也难看。所以将这些通用功能提取成中间件,请求来了以后,通过各个中间件进行处理。后端
首先中间件是由咱们,koa的用户定义的。因此koa要将咱们定义的中间件存储起来。咱们应该都写过中间件,知道每一个中间件其实就是一个方法。而后中间件的执行是有顺序的。因此要存储一些有顺序的方法,最简单的就是使用一个数组。添加一个中间件就是为数组push一个方法。
咱们使用过koa,都知道koa添加中间件的方式是app.use(fn)。app就是koa模块的实例。数组
class Koa { constructor(){ this.middlewares = [] } use(fn){ this.middlewares.push(fn) } } const app = new Koa() app.use((ctx, next) => {}) console.log(app.middlewares.length) console.log(app.middlewares[0].toString())
稍做思考,咱们就知道让数组middlewares做为app的一个属性,为app提供一个方法use,功能就是为middlewares数组push一个方法。上面的代码会在控制台上打印出1后打印出(ctx,next)=>{},这里就不贴图了。promise
如今咱们已经可以存储中间件了,那么如何让他们按照必定的顺序去执行呢?最简单的就是链式执行了。就是一个执行完以后去执行另外一个。代码也很简单。闭包
function sleep(time) { return new Promise((resolve)=> { setTimeout(()=>{ resolve() }, time) }) } let middleWare0 = async function (ctx, next) { let startTime = Date.now() if(next){ await next() } console.log(Date.now() - startTime) } let middleWare1 = async function (ctx, next) { if(next){ await next() } await sleep(1000) } async function compose(middlewares, ctx) { for(let middleware of middlewares){ await middleware(ctx) } } compose([middleWare0, middleWare1], {})
compose方法就是用来链式执行中间件的。sleep函数就是用来模拟异步操做的。一共有两个中间件。确实是先执行了中间件0,而后执行了中间件1。这时应该能看出上述代码有些问题:
首先,咱们在日常写代码的时候是没有if(next)这个判断的。这里去掉这个判断就会报错。由于在执行中间件的时候根本没有给他next这个参数。next是undefined,而不是一个函数,因此会报错。
其次,middleWare0的代码功能实际上是想等待后面的中间件执行完以后再console.log,但如今确是马上就输出了。时间差也是0毫秒左右。而咱们要等待middleWare1完成,应该是有1000ms左右。
**koa要实现的中间件不是简单的链式执行。而是前面的中间件可以控制后面的中间件该什么时候执行。**很高端吧。其实,不用惧怕。中间件只不过是一个函数罢了,而他想要控制另外一个函数该如何执行,最简单就是把这个函数告诉他,也就是做为一个参数传递进去。咱们把compose改一改 。app
let compose = async function (ctx) { await middlewares[0](ctx,middlewares[1]) }
首先咱们考虑只有两个中间件的状况。上述代码执行了中间件0,并将ctx和中间件1传给了它。也就是在中间件0中的next就是中间件1。这和咱们熟悉的不同,next在执行时并不须要咱们传参数。这时咱们能够推断出next这个函数应该是这种形式:koa
next[i] = async function(){ await middlewares[i](ctx,next[i+1]) }
next[i]应该返回的是一个异步函数,里面执行了当前第i个中间件,并将ctx,以及next[i+1]传递给第i个中间件。
不难发现,这是一个递归的结构。还有对比上面两段代码。其实结构是同样的。因此最终可以写出这段代码:异步
function compose(ctx, i) { return async function () { if(middlewares[i]){ await middlewares[i](ctx, compose(ctx, i+1)) } } }
执行compose(ctx,i)便能获得一个异步函数fn。fn的第一步就是判断中间件0是否存在,若是存在就调用中间件0,第一个参数为ctx,第二个参数为compose(ctx,i+1)这个函数的执行结果,也就是下一个中间件对应的异步函数。下面举例看下fn。async
let fn = compose(ctx, 0) fn = async function () { await middlewares[0](ctx, async function () { await middlewares[1](ctx, async function () { await middlewares[2](ctx, async function () { ... }) }) }) }
fn的格式就是上面那样(少了判断中间件是否存在)。
经过这种方式来执行中间件,每一个中间件都能控制何时来执行下一个中间件。
不过咱们仍是少了一步,就是如何把middlewares这个数组传递进来。个人实现方式不是很好,但看起来比较简单。函数
let middlewares = [] let setMiddleWare = function (middlewaresArg) { middlewares = middlewaresArg } function compose(ctx, i) { return async function () { if(middlewares[i]){ await middlewares[i](ctx, compose(ctx, i+1)) } } } exports.setMiddleWare = setMiddleWare exports.compose = compose
测试代码以下:测试
function sleep(time) { return new Promise((resolve)=> { setTimeout(()=>{ resolve() }, time) }) } let middleWare0 = async function (ctx, next) { let startTime = Date.now() await next() console.log(Date.now() - startTime) } let middleWare1 = async function (ctx, next) { next() await sleep(1000) console.log('中间件1') } let middleWare2 = async function (ctx, next) { await sleep(2000) await next() console.log('中间件2') } const compose = require('./compose') compose.setMiddleWare([middleWare0, middleWare1, middleWare2]) let fn = compose.compose({},0) fn() .catch(err => { console.log(err) })
compose即是上面的模块。若代码没有问题,则应该先执行中间件0的await next()前面的代码,而后执行中间件1,中间件1第一句就是next()也就是异步执行中间件2,这时进入中间件2,中间件2第一句会执行等待定时器2秒。这时回到中间件1,中间1会等待定时器1秒,约一秒后中间件1定时完成,输出“中间件1”。中间件0等待中间件1执行完成,输出时间差,约为1000。约1秒后,中间件2定时完成,输出“中间件2”。
下面是代码的输出结果。
具体的等待时间能够本身运行验证下。
到如今为止咱们基本完成了中间件的全部实现代码。那么让咱们看下koa是如何实现的吧。
//如下代码均去掉了部分无关代码 class Application extends Emitter { constructor() { super(); this.middleware = []; } use(fn) { if (typeof fn !== 'function') throw new TypeError('middleware must be a function!'); debug('use %s', fn._name || fn.name || '-'); this.middleware.push(fn); return this; } callback() { const fn = compose(this.middleware); const handleRequest = (req, res) => { const ctx = this.createContext(req, res); return this.handleRequest(ctx, fn); }; return handleRequest; } handleRequest(ctx, fnMiddleware) { return fnMiddleware(ctx).then(handleResponse).catch(onerror); } }
这时看use和callback方法是否是有一些豁然开朗了?
koa的use和咱们写的相比,主要就是多了一个对入参fn的校验。 compose中,middleware数组的传入方式不一样,他是直接传进了compose。让咱们看下koa的compose是如何保存middleware的。
function compose (middleware) { //去掉了校验代码 return function (context, next) { let index = -1 return dispatch(0) function dispatch (i) { 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) } } } }
koa保存middleware其实用了闭包。compose(middleware)返回了一个function,而这个function用到了middleware。因此只要外面引用这个function,middleware就会一直在这个做用域存在。 compose执行后,返回的function支持两个参数,一个是context,一个是next。next为middleware数组后面的中间件。看下代码中的fn,fn随着i的增长,而取值为middleware[i],当i为middleware的长度时,也就是没有这个中间件时,fn取值为传入的next,因此传入的next能够理解为next[middleware.length] 。 接着看,肯定好了fn后,执行了fn,传入了参数context,以及next[i+1],和咱们刚刚的代码是同样的。不过因为他用的是promise,因此外面包裹了一层Promise.resolve。方便后面的promise链调用。而咱们是用的async,功能都是差很少的。