阅读原文序言Koa是目前主流的NodeJS框架,以轻量着称,其中间件机制与比较传统的Express支持异步,所以编码时经常用到async/await提高了可读性并使代码更加优雅。上一篇NodeJS进阶——Koa源码分析,也分析了“洋葱模型”和实现它的compose。个人觉得compose编程思想更重要,应用更广泛,所以本文以“洋葱模型”为题,通过四种方式实现compose。洋葱模型案例如果你用过Koa,你一定对“洋葱模型”这个词不陌生。是Koa中间件的一种串行机制,支持异步。下面是表达“洋葱模型”的一个经典案例。constKoa=require("koa");constapp=newKoa();应用程序。use(asycn(ctx,next)=>{console.log(1);awaitnext();console.log(2);});app.use(asycn(ctx,next)=>{console.log(3);awaitnext();console.log(4);});app.use(asycn(ctx,next)=>{console.log(5);awaitnext();console.log(6);});app.listen(3000);//1//3//5//6//4//上面的2个我们按照官方的推荐使用了async/await,但是同步代码不使用也没关系。这里简单分析一下执行机制。如果在第一个中间件函数中执行了next,那么下一个中间件就会执行Execution,以此类推,我们就有了上面的结果,而在Koa源码中,这个功能是通过一个compose方法来实现的。在这篇文章中,我们在Compose的四种实现方式中实现同步和异步,并附上相应的案例。核实。准备工作在真正创建compose方法之前,应该做一些准备工作,比如创建一个app对象来替换Koa创建的实例对象,添加一个use方法和一个管理中间件的中间件数组。//File:app.js//模拟Koa创建的实例constapp={middlewares:[]};//创建use方法app.use=function(fn){app.middlewares.push(fn);};//app.compose.....module.exports=app;上面模块中导出了app对象,创建了存放中间件功能的middlewares和添加中间件的use方法,因为不管用哪个方法实现compose这些都是需要的,只是compose逻辑不同,所以下面的代码块中只会写compose方法。Compose在Koa中的实现首先介绍一下Koa源码中的实现。在Koa源码中,其实是通过koa-compose中间件实现的。这里我们把这个模块的核心逻辑抽取出来,用自己的方法实现,由于重点是分析compose的原理,所以把ctx这个参数去掉,因为我们不会用到,重点放在下一个参数上。1.同步的实现//文件:app.jsapp.compose=function(){//递归函数functiondispatch(index){//如果所有中间件都执行完,则跳出if(index===app.middlewares.length)返回;//取出index中间件,执行constroute=app.middlewares[index];返回路线(()=>派遣(索引+1));}//取出第一个中间件函数,执行dispatch(0);};以上就是同步的实现。通过递归函数dispatch的执行,取出数组中的第一个中间件函数执行。执行时传入一个函数,递归执行dispatch。参数+1,使得执行下一个中间件函数,以此类推,直到所有中间件都执行完,当不满足中间件执行条件时,就会跳出,所以遵循135642的情况在上面的caseExecution中,测试示例如下(同步,异步)。//文件:sync-test.jsconstapp=require("./app");app.use(next=>{console.log(1);next();console.log(2);});app.use(next=>{console.log(3);next();console.log(4);});app.use(next=>{console.log(5);next();console.log(6);});app.compose();//1//3//5//6//4//2//文件:async-test.jsconstapp=require("./app");//异步函数functionfn(){returnnewPromise((resolve,reject)=>{setTimeout(()=>{resolve();console.log("hello");},3000);});}app.use(asyncnext=>{console.log(1);awaitnext();console.log(2);});app.use(asyncnext=>{console.log(3);awaitfn();//调用异步函数awaitnext();console.log(4);});app.use(asyncnext=>{console.log(5);awaitnext();console.log(6);});app.compose();我们发现如果按照Koa的推荐来写case,即使使用了async函数也会pass,但是在传递参数使用的时候,可能会传入普通函数或者async函数,我们要传递所有的返回值中间件的部分被打包成一个Promise来兼容这两种情况。事实上,在Koa中,compose最后返回一个Promise,即为了写后续的逻辑,现在不支持了,先解决这两个问题吧。注意:在后面compose的其他实现中,使用了sync-test.js和async-test.js进行校验,所以后面不会再重复。2.升级支持异步//File:app.jsapp.compose=function(){//递归函数functiondispatch(index){//如果中间件全部执行完,跳出返回一个Promiseif(index===app.middlewares.length)返回Promise.resolve();//取出index中间件并执行constroute=app.middlewares[index];//执行后返回成功状态的PromisereturnPromise.resolve(route(()=>dispatch(index+1)));}//取出第一个中间件函数,执行dispatch(0);};我们知道async函数中await之后执行的异步代码需要实现等待,异步执行之后继续往下执行,需要等待Promise,所以我们在调用的时候每个中间件函数的最后都会返回一个成功的Promise,使用async-test.js进行测试,发现结果为13hello(after3s)5642。老版本Redux中compose的实现1.同步的实现//File:app.jsapp.compose=function(){returnapp.middlewares.reduceRight((a,b)=>()=>b(a),()=>{})();};上面的代码看起来不太好理解,我们不妨根据案例拆解这段代码,假设middlewares中存放的三个中间件函数分别是fn1,fn2,fn3,因为使用了reduceRight方法,所以反向合并命令。第一次,a代表初始值(空函数),b代表fn3,fn3的执行返回一个函数。这个函数作为下一次合并的a,fn2作为b,以此类推,过程如下。//第一个reduceRight的返回值将用作a()=>fn3(()=>{});//第二个reduceRight的返回值将用作a()=>fn2(()=>fn3(()=>{}));//第三个reduceRight的返回值将是a()=>fn1(()=>fn2(()=>fn3(())=>{})));从上面的反汇编过程可以看出,如果我们调用这个函数,首先执行fn1,如果调用next,则执行fn2,如果next也被调用,则执行fn3,而fn3已经是最后一个了一个中间件函数,再次调用next会执行我们最初传入的空函数,这就是为什么将reduceRight的初始值设置为空函数的原因,防止最后一个中间件调用next报错。经过测试,上面的代码不会出现乱序,但是在compose执行之后,我们想进行一些后续的操作,所以我们希望返回Promise,我们希望传递给use的中间件函数可以是普通的Functions也可以是异步函数,这就需要我们的compose完全支持异步。2.升级支持异步//file:app.jsapp.compose=function(){returnPromise.resolve(app.middlewares.reduceRight((a,b)=>()=>Promise.resolve(b(a)),()=>Promise.resolve();)());};参考同步分析流程,因为最后一个中间件执行完后执行的空函数里面肯定没有逻辑,但是遇到异步代码可以继续执行(比如执行next后调用then)处理成Promise,这样就保证了reduceRight返回的函数在每次合并的时候都返回一个Promise,这样就完全兼容了async和普通函数。当所有的中间件执行完后,也会返回一个Promise,这样compose就可以调用then方法执行后续逻辑了。新版Redux中compose的实现1.同步的实现//File:app.jsapp.compose=function(){returnapp.middlewares.reduce((a,b)=>arg=>a(()=>b(arg)))(()=>{});};在新版本的Redux中,对compose的逻辑进行了一些改动,将原来的reduceRight替换成了reduce。必须和Redux的源码一模一样,也是基于同样的思路来实现串口中间件的需求。个人觉得改成正序合并后更难理解,所以还是结合案例拆分了上面的代码。中间件仍然是fn1、fn2和fn3。由于reduce没有传入初值,所以此时a为fn1,b为fn2。//第一个reduce的返回值将用作aarg=>fn1(()=>fn2(arg));//第二个reduce的返回值将用作aarg=>(arg=>fn1(()=>fn2(arg)))(()=>fn3(arg));//相当于...arg=>fn1(()=>fn2(()=>fn3(arg)));//执行最后一个返回的函数连接中间件,返回值相当于...fn1(()=>fn2(()=>fn3(()=>{})));所以在调用reduce函数最后返回的时候,传入一个空函数作为参数。其实这个参数最终还是传给了fn3,也就是第三个中间件,这样就保证了最后一个中间件next调用的时候不会报错。2.升级支持异步。接下来还有一个比较难的任务,就是把上面的代码改成支持异步的。实现如下。//文件:app.jsapp.compose=function(){returnPromise.resolve(app.middlewares.reduce((a,b)=>arg=>Promise.resolve(a(()=>b(arg))))(()=>Promise.resolve()));};实现异步其实就是一个逆序合并的套路,就是把每个中间件函数的返回值都做成Promise,让compose也返回Promise。使用async函数实现这个版本是我在研究Koa源码的时候看到一篇大佬分析Koa原理的文章(找了半天没找到链接),以及这里分享给大家分享一下,因为是async函数实现的,默认支持异步,因为async函数会返回一个Promise。//File:app.jsapp.compose=function(){//自执行异步函数返回Promisereturn(asyncfunction(){//定义默认next,next在最后一个中间件中执行letnext=async()=>Promise.resolve();//中间件是针对每个中间件函数的,oldNext是每个中间件函数中的next//函数返回一个async作为新的next,异步执行返回Promise解决异步问题functioncreateNext(middleware,oldNext){returnasync()=>{awaitmiddleware(oldNext);}}//反向遍历中间件数组,先传next到最后一个中间件函数//把新的中间件函数存入next变量//调用下一个中间件函数并将新生成的next传递给for(leti=app.middlewares.length-1;i>=0;i--){next=createNext(app.middlewares[i],next);}awaitnext();})();};上面代码中的next是一个只返回成功的Promise的函数,在其他实现中可以理解为最后一个中间件调用的next,而数组middlewares恰好是反向遍历的,第一个得到的值就是最后一个中间件,而调用createNext的作用是返回一个新的async函数,可以执行数组中最后一个中间件,并传入初始next,这个返回的async函数作为新的next,然后是second-to-last中间件被拉取,调用createNext,返回一个async函数,函数还是倒数第二个对于一个中间件的执行,传入的next是最后一个新生成的next,依此类推第一个中间件,所以执行第一个中间件返回的next会执行传入的最后生成的next函数,第二个中间件会是执行,第二个中间件中的下一个将被执行。这样一直到原来定义的next被执行,通过案例的验证,执行结果和洋葱模型完全一样。至于异步的问题,每次执行的next都是一个async函数,执行完返回一个Promise,最外层的自执行async函数也返回一个Promise,也就是说compose最后返回一个Promise,所以是完全支持的异步。这个方法放在最后是因为我个人觉得比较难懂。我按照这些方法的理解难度从上到下对这些方法进行了排序。总结或许看完这些方法,你会觉得Koa对compose的实现是最容易理解的。你可能还会觉得Redux的两个实现和async函数的实现是如此巧妙,就像JavaScript一样。在被别人批评为“弱型”和“不严谨”的同时,它的灵活和创造性让我们无法判断这是优势还是劣势(见仁见智,见仁见智),但有一件事可以肯定的是,学习JavaScript并没有被强类型语言的“从众”所束缚(个人观点,强类型语言的开发者不要抱怨),要吸收如此巧妙的编程思想任重而道远像composeXi一样写出优雅优质的代码,愿你在技术的道路上“一往无前”。
