本文基于Koa@1.4.0。2014年开始接触Koa,看源码,写文章,后来也用Koa做一些项目,但一直没有系统性的学习总结。今天通过这篇文章,给大家介绍一下Koa中中间件从加载到执行的全过程解析。如果有不准确的地方忘记指出了。发展史来看看Koa的整个发展史,每个里程碑都发生了哪些变化2013.12FirstCommit2015.08发布Koav1版本2015.10发布Koav2-alpha.1版本,用ES6重写代码,更新中间软件支持async和await2017.02发布了Koav2版本,在v1版本放弃了generator中间件。如果想继续使用,必须使用koa-covert进行适配。明确表示在v3中会删除对生成器中间件的支持,纵观整个开发过程,我们似乎发现了一个规律,Koa每两年发布一个大版本。那么,我们可以期待2019年的v3吗?知识点回顾在正式分析Koa源码之前,我们还需要一些其他的知识储备。这里我们简单回顾一下Generator函数的使用,Co&Promise。如果想深入了解,可以上网查找相关资料进行学习。如果你已经掌握了它们的用法,可以跳过下面的内容,继续阅读下面的内容。Generator函数生成器是ES6中处理异步编程的一种解决方案。让我们通过一个简单的例子来回顾一下它的用法。function*gen1(){console.log('gen1start');产量1;变量值=产量2;控制台日志(值);产量*gen2();产量4;console.log('gen1end');}function*gen2(){console.log('gen2start');产量3;console.log('gen2end');}varg=gen1();g.next();//gen1开始{value:1,done:false}g.next();//{value:2,done:false}g.next('delegatingtogen2');//委托给gen2gen2start{value:3,done:false}g.next();//gen2结束{value:4,done:false}g.next();//gen1end{value:undefined,done:true}看完例子需要注意几点:直接调用generator函数并不会真正开始执行函数,只是通过next方法,才开始每次调用next方法时执行,暂停在yield右边,不执行yield左边的赋值操作,给next方法传递参数,就可以给左边的变量赋值通过yield*的yield,可以委托给gen2继续执行gen2中的内容。这称为委派收益。yield全部执行完后,再次调用next,返回值是undefined,done为trueCo&Promise的使用,从上面的例子我们可以看出,整个generator函数可以通过不断调用next方法来执行。那么有没有什么方案可以自动调用这些next方法呢?答案是合作。另外,Promise作为ES6中提供的异步编程解决方案,可以有效避免回调地狱的产生。这里也通过一个小例子来回顾一下co的实现原理和Promise的用法。varco=require('co');co(function*(){vara=Promise.resolve(1);varb=Promise.resolve(2);varc=Promise.resolve(3);varres=yield[a,b,c];console.log(res);//=>[1,2,3]}).赶上(错误);函数onerror(err){控制台。日志(错误。堆栈);}本例res直接输出[1,2,3]。那我们来看下面,co内部到底做了什么操作,直接上代码functionco(gen){//xxxreturnnewPromise(function(resolve,reject){if(typeofgen==='function')gen=gen.apply(ctx,args);if(!gen||typeofgen.next!=='function')returnresolve(gen);onFulfilled();functiononFulfilled(res){varret;try{ret=gen.next(res);}catch(e){returnreject(e);}next(ret);}functiononRejected(err){varret;try{ret=gen.throw(err);}catch(e){returnreject(e);}next(ret);}functionnext(ret){if(ret.done)returnresolve(ret.value);varvalue=toPromise.call(ctx,ret.value);if(value&&isPromise(value))returnvalue.then(onFulfilled,onRejected);returnonRejected(newTypeError('Youmayonlyyieldafunction,promise,generator,array,orobject,'+'但传递了以下对象:"'+String(ret.value)+'"'));}}//xxx}functiontoPromise(obj){if(!obj)returnobj;如果(isPromise(obj))返回obj;如果(isGeneratorFunction(obj)||isGenerator(obj))returnco.call(this,obj);if('function'==typeofobj)returnthunkToPromise.call(this,obj);如果(Array.isArray(obj))返回arrayToPromise.call(this,obj);如果(isObject(obj))返回objectToPromise.call(this,obj);returnobj;}functionarrayToPromise(obj){returnPromise.all(obj.map(toPromise,this));}可以看到co最后返回的是一个Promise对象,所以就有了例子中的catch方法,我们拿a先看Promise内部的具体实现,判断gen是否为函数,如果是,直接调用;然后通过判断是否有next方法来判断是否是generator实例,如果不是,直接resolve;onFulfilled函数内部第一次调用gen.next方法,ret值{value:array,done:false},然后将ret传递给内部的next方法;因为我们知道ret.value的值是一个数组,所以我们直接看arrayToPromise方法。我们先回顾一下Promise的用法。Promise.resolve()可以创建一个Promise实例,而Promise.all()用于将多个Promise实例包装成一个新的Promise实例。只有当数组中每个实例的状态都变为fulfilled时,Promise.all的状态才会被fulfill。此时数组中每个实例的返回值组成一个数组,传递给all的回调函数。因为我们所有的示例都使用Promise.resolve(),所以我们的Promise.all的状态必须得到满足。回头看next方法中的if(value&&isPromise(value))returnvalue.then(onFulfilled,onRejected);此时的value值是一个被Promise.all包裹的新实例,状态为fulfilled。第一次调用then的回调函数onFulfilled,参数是由各个子实例的返回值组成的数组,即[1,2,3]。所以第二次调用gen.next方法时,res的值为数组[1,2,3]。通过之前在generator中的回顾,我们知道传给gen.next的参数会被赋值给外层yield左边的变量,所以上面的例子中,res最终会输出[1,2,3].此时在我们内部的onFulfilled中,ret的值为{value:undefined,done:true},resolve直接在next方法中返回,结束操作。有了以上基础,我们终于可以进入正题了。和往常一样,我们先来看看如何在Koa中添加中间件。varkoa=require('koa');varapp=koa();app.use(function*(next){console.log('gen1start');yieldnext;console.log('gen1end');});app.use(function*(next){console.log('gen2start');yieldnext;console.log('gen2end');});app.listen(3000);这里我们添加了两个中间件,不用担心不知道代码的输出,当你第一次看到这段代码的时候,你会不会有疑惑?next到底是什么,不传入会怎样,yieldnext是做什么的,它们之间的执行顺序是什么?下面我们带着这些问题,看看Koa内部是如何实现的。为了方便理解,删掉GotosomecodefunctionApplication(){//xxxthis.middleware=[];//xxx}app.use=function(fn){//xxxthis.middleware.push(fn);返回这个;}应用程序。listen=function(){debug('listen');varserver=http.createServer(this.callback());returnserver.listen.apply(server,arguments);};app.callback=function(){//xxxvarfn=this.experimental?compose_es7(this.middleware):co.wrap(compose(this.middleware));if(!this.listeners('error').length)this.on('error',this.onerror);返回函数handleRequest(req,res){//xxxfn.call(ctx).then(functionhandleResponse(){respond.call(ctx);}).catch(ctx.onerror);}};从代码中可以看出,我们通过use方法添加的中间件被塞进了一个预先定义好的中间件数组中。app.listen入口方法创建一个http服务器服务。在callback回调中,重点关注这行代码co.wrap(compose(this.middleware))这是一个嵌套的两层方法,第一层通过中间件组件的middleware数组传入compose方法;第二层通过第一层返回的结果传递给co.wrap方法。然后再看第一层的compose方法functioncompose(middleware){returnfunction*(next){if(!next)next=noop();vari=middleware.length;while(i--){next=middleware[i].call(this,next);}返回收益*下一个;}}function*noop(){}返回一个生成器函数,函数内部,i为数组的长度,middleware[i]表示数组的结尾一个中间件,执行middleware[i].call(this,next)生成生成器实例并将其分配给下一个。请注意这里的下一个参数。next第一次是noop的空生成器函数。做了i--一次之后,next就变成了之前生成器函数的实例对象,以此类推。也就是说,这个while循环从最后一个中间件开始处理,一直往前,把后面的生成器函数的实例作为前面生成器函数的参数传入,然后执行整个循环,而我们的next的值就是数组的第一个生成器实例。看到这里,我们是不是可以回答上面的第一个问题了呢?next参数其实就是next中间件的生成器实例。揭开面纱compose的存在让整个中间件串连起来,但不允许中间件运行。要让整个流程运行起来,关键是看co。我们继续看下面的代码co.wrap=function(fn){createPromise.__generatorFunction__=fn;返回创建承诺;functioncreatePromise(){returnco.call(this,fn.apply(this,arguments));}};这里的fn就是上面compose返回的生成器函数。在wrap内部调用co,继续我们对co内部的分析,在onFulfilled函数内部第一次调用了gen.next方法,在compose内部执行到yield*next,next是我们的第一个中间件,根据委托yield的知识,它将代理到第一个中间件的内部,并在我们中间件的yieldnext处暂停,其中next是下一个中间件的生成器实例。ret的值为{value:generatorinstance,done:false},然后将ret传递给内部的next方法。如果(isGeneratorFunction(obj)||isGenerator(obj))returnco.call(this,obj);如果value是生成器函数或者生成器实例,继续调用co。在函数onFulfilled中,对gen.next方法的第二次调用在第二个中间yieldnext处暂停。以此类推,直到最后遇到空生成器函数noop,执行if(ret.done)returnresolve(ret.value),promise状态设置为fulfilled。varvalue=toPromise.call(ctx,ret.value);if(value&&isPromise(value))returnvalue.then(onFulfilled,onRejected);因为每次调用co都会返回一个promise实例,而ret.done为true时,状态设置为fulfilled,所以执行了回调中的onFulfilled函数。这样就从最后一个中间件往回执行,像纸签一样,整个流程串起来了。看完这部分,我们来回答剩下的两个问题。yieldnext用于执行下一个中间件的内容;中间件之间的执行顺序也是按照使用顺序执行的。是不是觉得整个过程很曲折,但是不得不佩服作者的巧妙设计。从下图我们可以更直观的理解整个执行过程但是当你看compose源码的时候,有没有疑惑,为什么compose里面是yield*next,而我们的中间件里面yieldnext呢?按理说这里下一个是生成器实例,有什么区别呢?Koa的维护者也讨论过这个问题,详见最后两个参考链接。在性能方面,yield*next略优于yieldnext。毕竟前者是原生的,后者需要合包处理,不过写成后者也没什么大的影响。Koa的作者TJ也非常反对两者的区别。转写,推荐后者。再者,yield*后面只能跟生成器函数或者可迭代对象,中间件中的yield后面可以跟函数、promise、generator、array或者object,因为co最终会帮我们处理成promise,所以建议你当用Koa开发,接下来可以写成yield。参考http://es6.ruanyifeng.com/#docs/generatorhttp://es6.ruanyifeng.com/#docs/promisehttps://github.com/qianlongo/resume-native/issues/1http://taobaofed.org/blog/2015/11/19/yield-and-delegating-yield/http://www.jongleberry.com/delegating-yield.htmlhttps://github.com/koajs/compose/issues/2
