我们知道Koa中间件是以级联代码(Cascading)的形式执行的。类似于回形针,可以参考下图:今天的文章将分析Koa的中间件是如何实现级联执行的。在koa中,要应用一个中间件,我们使用app.use():app.use(logger()).use(bodyParser()).use(helmet())先来看看use()是什么,它的源码如下:use(fn){if(typeoffn!=='function')thrownewTypeError('middlewaremustbeafunction!');if(isGeneratorFunction(fn)){deprecate('对生成器的支持将在v3中删除。'+'请参阅文档以获取有关如何转换旧中间件的示例'+'https://github.com/koajs/koa/blob/master/docs/migration.md');fn=转换(fn);}debug('使用%s',fn._name||fn.name||'-');这个.middleware.push(fn);归还这个;}这个函数的作用是调用use(fn)方法(不管是普通函数还是中间件)添加到数组this.middlware中。在Koa2中,也兼容Generator语法的中间件。使用isGeneratorFunction(fn)方法判断是否为Generator语法,通过convert(fn)方法转换为async/await语法。然后将所有的中间件添加到this.middleware中,最后通过callback()方法执行。callback()的源代码如下:/***为节点的本地http服务器返回请求处理程序回调*。**@return{Function}*@apipublic*/callback(){constfn=compose(this.middleware);if(!this.listeners('error').length)this.on('error',this.onerror);consthandleRequest=(req,res)=>{constctx=this.createContext(req,res);返回this.handleRequest(ctx,fn);};返回句柄请求;}在源码中,通过compose()方法可以将我们传入的中间件数组进行转换并级联执行,最后callback()返回this.handleRequest()执行结果。我们暂时不关心返回的是什么。我们先来看看compose()方法做了什么来启用传入中间件的级联执行并返回一个Promise。compose()是koa2实现中间件级联调用的一个库,叫做koa-compose。源码很简单,只有一个功能,如下:**@param{Array}middleware*@return{Function}*@apipublic*/functioncompose(middleware){if(!Array.isArray(middleware))thrownewTypeError('中间件堆栈必须是一个数组!')for(constfnofmiddleware){if(typeoffn!=='function')thrownewTypeError('中间件必须由函数组成!')}/***@param{Object}context*@return{Promise}*@apipublic*/returnfunction(context,next){//记录上次执行中间件的位置#letindex=-1returndispatch(0)functiondispatch(i){//理论上我会大于index,因为每次执行i都会自增,//如果等于或小于,说明next()执行了多次if(i<=index)returnPromise.reject(newError('next()calledmultipletimes'))index=i//获取当前的中间件letfn=middleware[i]if(i===middleware.length)fn=nextif(!fn)returnPromise.解决()尝试{returnPromise.resolve(fn(context,functionnext(){returndispatch(i+1)}))}catch(err){returnPromise.reject(err)}}}}可以看到compose()返回a匿名函数的结果,匿名函数自己执行dispatch()函数,传入0作为参数,看看dispatch(i)函数做了什么。i作为这个函数的参数,用来获取当前下标的中间件。上面的dispatch(0)中,传入0得到middleware[0]中间件。首先显示判断i<==index。如果为真,则表示已多次调用next()方法。为什么能做出这样的判断呢?在我们解释完所有的逻辑之后回到这个问题。接下来将当前的i赋值给index,记录当前正在执行的中间件的下标,给fn赋值得到中间件。index=i;letfn=middleware[i]拿到中间件后,如何使用呢?try{returnPromise.resolve(fn(context,functionnext(){returndispatch(i+1)}))}catch(err){returnPromise.reject(err)}上面的代码执行了中间件fn(context,next),并传递了context和nextfunction两个参数。context是koa中的context对象context。至于next函数,返回dispatch(i+1)的执行结果。值得一提的是参数i+1,传递这个参数相当于执行下一个中间件,这样就形成了递归调用。这就是为什么我们自己写中间件的时候,需要手动执行awaitnext()。只有执行完next函数,才能正确执行下一个中间件。因此,每个中间件只能执行一次next。如果在一个中间件中多次执行next,就会出现问题。回到上面提到的问题,为什么我们可以通过i<=index来判断next可以执行多次呢?因为一般情况下index必须小于等于i。如果在中间件中多次调用next,dispatch(i+1)将被执行多次。从代码来看,每个中间件都有自己的闭包范围,同一个中间件的i保持不变,索引在闭包范围之外。当第一个中间件调用dispatch(0)的next()时,此时应该执行dispatch(1)。当执行下面的判断时,if(i<=index)returnPromise.reject(newError('next()calledmultipletimes'))此时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监听去执行对应的逻辑(成功或失败)在执行第一个中间件的逻辑时,遇到awaitnext(),会继续执行dispatch(i+1),即执行dispatch(1),手动触发执行第二个中间件。这时候第一个中间件awaitnext()后面的代码会pending,第一个中间件awaitnext()后面的代码会在awaitnext()返回Promise后继续执行。同样,在执行第二个中间件时,遇到awaitnext()时,会手动执行第三个中间件,awaitnext()之后的代码还在pending,等待下一个await中间件的Promise。解决。只有接收到第三个中间件的resolve后,才会执行下面的代码,然后第二个中间件返回Promise,被第一个中间件的await捕获,然后是第一个中间件的后续执行代码,然后返回到Promise,等等。如果有多个中间件,就会按照上面的逻辑继续执行。首先执行第一个中间件,pendingnext()之后继续执行第二个中间件,继续pendinginawaitnext(),继续执行第三个中间件,直到执行完最后一个中间件,然后返回Promise,然后倒数第二个中间件执行后面的代码并返回Promise,然后是倒数第二个中间件Middleware,然后继续这样执行,直到执行完第一个中间件,并返回Promise,从而实现图片开头的执行顺序文章。经过上面的分析,如果要写一个koa2中间件,基本格式应该是这样的:asyncfunctionkoaMiddleware(ctx,next){try{//dosomethingawaitnext()//dosomething}。catch(err){//handleerr}}最近在用koa2+React写博客。有兴趣的同学可以去GitHub地址查看:koa-blog-api
