当前位置: 首页 > Web前端 > HTML5

你知道koa中间件执行原理吗

时间:2023-04-05 17:01:36 HTML5

你知道koa中间件的执行原理吗?,我完全不知道,所以,再看一遍,我好像明白了什么,一遍又一遍地看,我去,太神奇了,泪流满面,太疯狂了!!!在下面前面的例子中使用,会在控制台打印一些信息(具体打印什么?你能猜到吗?),然后返回helloworld。letkoa=require('koa')letapp=koa()app.use(function*(next){console.log('generate1----start')yieldnextconsole.log('generate1----end')})app.use(function*(next){console.log('generate2----start')yieldnextconsole.log('generate2----end')this.body='helloworld'})app.listen(3000)用过koa的同学都知道,添加中间件的方式是使用koa实例的use方法,传入一个生成器函数。这个generator函数可以接受一个next(这个next是什么?这里不说清楚,后面会详细解释)。你为什么执行use?这是koa的构造函数。为了不干扰其他信息,我去掉了一些暂时不用的代码。这里我们主要关注中间件数组。functionApplication(){//xxxthis.middleware=[];//这个数组是用来一个一个安装中间件的//xxx}下面我们看use方法,也去掉一些暂时不用的代码,可以看到每次执行use方法,push传入的generator函数中间件数组app.use=function(fn){//xxxthis.middleware.push(fn);//xxx};好的!你已经知道在koa中,use方法是预先传递的,请求可能经过的中间件是安装在一个数组中的。接下来,我们就开始本文的重点。当一个请求来了,它是如何经过中间件的,又是如何运行的呢?首先我们只需要知道下面的回调函数是请求到来时执行的回调即可(也尽量去掉我们不用的代码)app.callback=function(){//xxxvarfn=这个.实验性的?compose_es7(this.middleware):co.wrap(compose(this.middleware));//xxxreturnfunction(req,res){//xxxfn.call(ctx).then(function(){respond.call(ctx);}).catch(ctx.onerror);//xxx}};这段代码可以分为两个Middleware在一些请求之前的初始化处理。一些请求到达时运行的中间件。让我们分部分来谈谈。varfn=this.experimental?compose_es7(this.middleware):co.wrap(compose(this.middleware));代码部分对实验进行了判断。如果设置为true,那么koa会支持传入async函数,否则会执行co.wrap(compose(this.middleware))。只需要一行来初始化中间件就可以了?我知道koa很cool,但是你也别那么差好吗,所以评价一个好程序员不是用代码量来决定的。让我们来看看这段代码有什么神奇之处。compose(this.middleware)把中间件数组作为参数传给了compose方法,那么compose是做什么的呢?其实就是把本来没有任何关系的中间件串起来,这样就形成了千丝万??缕的联系。functioncompose(middleware){returnfunction*(next){//第一次拿到next是*noop生成的generator对象if(!next)next=noop();vari=middleware.length;//从后面往前走,开始执行中间件中的生成器函数while(i--){//将后面中间件得到的生成器对象作为第一个参数传给前面的。next=middleware[i].call(this,next);}返回收益*下一个;}}function*noop(){}用文字解释一下,compose从最后一个中间件开始处理,一直到第一个中间件。关键是将后一个中间件得到的generator对象作为参数(这个参数就是文章开头提到的next,也就是说next其实是一个generator对象)传递给前一个中间件。当然,最后一个中间件的参数next是一个空生成器函数生成的对象。下面写一个简单的例子来说明compose如何串联多个生成器函数function*gen1(next){yield'gen1'yield*next//开始执行下一个中间件yield'gen1-end'//next中间件完成,继续执行gen1中间件的逻辑}function*gen2(next){yield'gen2'yield*next//开始执行下一个中间件yield'gen2-end'//执行下一个中间件继续完成后执行gen2中间件的逻辑}function*gen3(next){yield'gen3'yield*next//开始执行下一个中间件yield'gen3-end'//执行完下一个中间件后继续执行gen3Middlewarelogic}function*noop(){}varmiddleware=[gen1,gen2,gen3]varlen=middleware.lengthvarnext=noop()//提供给最后一个中间件的参数while(len--){next=middleware[len.call(null,next)}function*letGo(next){yield*next}varg=letGo(next)g.next()//{value:"gen1",done:false}g.next()//{value:"gen2",done:false}g.next()//{value:"gen3",done:false}g.next()//{value:"gen3-end",done:false}g.next()//{value:"gen2-end",done:false}g.next()//{value:"gen1-end",done:false}g.next()//{value:undefined,done:true}看到了吗?中间件上链后的执行顺序为gen1->gen2->gen3->noop->gen3->gen2->gen1首尾相连,然后有关系?co.wrap被compose处理后返回一个生成器函数。co.wrap(compose(this.middleware))以上所有代码都可以理解为co.wrap(function*gen())好了,我们看看co.wrap做了什么,慢慢接近co。wrap=function(fn){createPromise.__generatorFunction__=fn;返回创建承诺;functioncreatePromise(){returnco.call(this,fn.apply(this,arguments));可以看到co.wrap返回了一个正常的函数createPromise,这个函数就是文章开头的fn。varfn=this.experimental?compose_es7(this.middleware):co.wrap(compose(this.middleware));中间件开始运行。前面已经说了中间件是怎么初始化的,也就是如果跟关系不近,接下来再说当请求来的时候初始化的中间件是怎么运行的。fn.call(ctx).then(function(){respond.call(ctx);}).catch(ctx.onerror);这一段是请求会经过的中间件的执行部分,fn执行后返回的是一个Promise,而koa通过注册成功和失败回调函数来分别处理请求。让我们回到co.wrap=function(fn){//xxxfunctioncreatePromise(){returnco.call(this,fn.apply(this,arguments));}}createPromise中的fn是中间件经过compose处理后返回一个生成器函数,那么执行后得到的是一个生成器对象,通过经典的co传递这个对象。如果你需要了解co的源码,请看我昨天写的,一步一步揭开co的神秘面纱。好了,接下来就是看co中compose处理的generator对象如何处理,再复习一下cofunctionco(gen){varctx=this;varargs=slice.call(arguments,1)//我们将所有内容包装在一个promise中以避免promise链,//这会导致内存泄漏错误。//参见https://github.com/tj/co/issues/180returnnewPromise(function(resolve,reject){if(typeofgen==='function')gen=gen.apply(ctx,args);if(!gen||typeofgen.next!=='function')returnresolve(gen);onFulfilled();/***@param{Mixed}res*@return{Promise}*@apiprivate*/functiononFulfilled(res){varret;try{ret=gen.next(res);}catch(e){returnreject(e);}next(ret);}/***@param{Error}err*@return{Promise}*@apiprivate*/functiononRejected(err){varret;try{ret=gen.throw(err);}catch(e){returnreject(e);下一步(退役);}/***获取生成器中的下一个值,*返回一个承诺。**@param{Object}ret*@return{Promise}*@apiprivate*/functionnext(ret){if(ret.done)returnresolve(ret.value);varvalue=toPromise.call(ctx,ret.value);if(value&&isPromise(value))returnvalue.then(onFulfilled,onRejected);returnonRejected(newTypeError('你只能产生一个函数、promise、生成器、数组或对象,'+'但传递了以下对象:"'+String(ret.value)+'"'));}});}我们直接看一下onFulfilled。这时候我们第一次进入co时,因为它已经是generator对象了,所以直接执行onFulfilled()functiononFulfilled(res){varret;试试{ret=gen.next(res);}catch(e){返回拒绝(e);}next(ret);}和gen.next用于执行中间件的业务逻辑。当遇到yield语句时,返回后面的结果赋值给ret,通常这里的ret就是我们文中提到的next,也就是当前中间件的下一个中间件拿到next中间件后交给next处理functionnext(ret){if(ret.done)returnresolve(ret.value);varvalue=toPromise.call(ctx,ret.value);if(value&&isPromise(value))returnvalue.then(onFulfilled,onRejected);returnonRejected(newTypeError('你只能产生一个函数、promise、生成器、数组或对象,'+'但传递了以下对象:"'+String(ret.value)+'"'));}当中间件执行完成后,将Promise的状态设置为成功。否则ret(即下一个中间件)会再次用co打包。主要看toPromise这几行。functiontoPromise(obj){//xxxif(isGeneratorFunction(obj)||isGenerator(obj))returnco.call(this,obj);//xxx}注意这个toPromise这个时候的返回值是一个Promise,非常关键。下一个中间件执行完毕后回到上一个中间件中断执行是关键。functionnext(ret){//xxxvarvalue=toPromise.call(ctx,ret.value);//是通过前面的toPromise返回的Promise实现的。当后一个中间件执行完成后,会回退到前一个中间件中断的地方继续执行if(value&&isPromise(value))returnvalue.then(onFulfilled,onRejected);//xxx}看到这里,我们可以得出结论,几乎所有的koa中间件都会被co打包一次,并且每个中间件都可以使用Promisethen监听下一个中间件是否结束,后一个中间件结束后,会执行该操作之前的中间件使用then来监控。这个操作就是接下来执行中间件yield后面的代码。例如:在koa中接收到一个请求时,请求会经过两个中间件,分别是中间件1和中间件2,中间件1//中间件1之前的代码yield中间件2yield中间件2//继续执行中间件1的代码aftermiddleware2isexecutedMiddleware2//yieldnoopmiddleware之前中间件2的代码yieldnoopmiddleware//noop中间件执行完成后继续执行中间件2的代码,那么处理过程就是co会立即调用onFulfilled执行中间件1前半段代码,遇到yield中间件2,获取中间件2的生成器对象,然后,将这个对象放入co继续执行,以此类推知道最后一个中间件(我们这里指的是一个)emptynoop中间件)执行结束,然后马上调用promise的resolve方法表示结束,ok,这时候中间件2监听到noop执行结束,马上执行onFulfilled执行yieldnoop中间件的后半段代码,嗯,这个时候中间件2也执行了,会马上调用promise的resolve方法表示结束,ok,这时候中间件1监听结束中间件2执行完,马上返回我去执行onFulfilled执行yield中间件2的后半部分代码,最后中间件全部执行完,再执行respond.call(ctx);啊啊啊,好绕,但是慢慢看,仔细想想,还是可以想出来的,用代码表达这个过程有点类似于newPromise((resolve,reject)=>{//我是中间件1yieldnewPromise((resolve,reject)=>{//我是中间件2yieldnewPromise((resolve,reject)=>{//我是body})//我是中间件2})//我是中间件1});到最后Rory讲了很多,不知道有没有把原则执行清楚。如果对你对koa的了解有一点帮助,不介意的话点源码地址,点个小星星如果对你对koa的了解有一点帮助,不介意的话,点源码地址和点个小星星如果对你稍微了解koa有帮助,不介意的话点个源码地址点个小星星。源码地址