相信用过Koa、Redux或者Express的小伙伴都对中间件不陌生,尤其是在学习Koa的过程中,也会接触到“洋葱模型”。本篇包哥就和大家一起学习Koa的中间件,不过这里包哥一开始并不打算展示大家熟知的“洋葱模型图”,而是先介绍一下Koa中的中间件是什么?,koa中间件在@types/koa-compose包下的index.d.ts头文件中,我们找到了中间件类型的定义://@types/koa-compose/index.d.tsdeclarenamespacecompose{typeMiddleware=(context:T,next:Koa.Next)=>any;typeComposedMiddleware=(context:T,next?:Koa.Next)=>Promise;}//@types/koa/index.d.ts=>Koa.NexttypeNext=()=>Promise;通过观察Middleware类型的定义,我们可以知道,在Koa中,middleware是一个普通的函数,它接收两个参数:context和next。其中context代表上下文对象,next代表一个函数对象,调用后返回一个Promise对象。了解了Koa的中间件是什么之后,我们来介绍一下Koa中间件的核心,即compose函数:functionwait(ms){returnnewPromise((resolve)=>setTimeout(resolve,ms||1));}constarr=[];conststack=[];//typeMiddleware=(context:T,next:Koa.Next)=>any;stack.push(async(context,next)=>{arr.push(1);awaitwait(1);awaitnext();awaitwait(1);arr.push(6);});stack.push(async(context,next)=>{arr.push(2);awaitwait(1);awaitnext();awaitwait(1);arr.push(5);});stack.push(async(context,next)=>{arr.push(3);awaitwait(1);awaitnext();awaitwait(1);arr。推(4);});awaitcompose(堆栈)({});对于上面的代码,我们希望compose(stack)({})语句执行后,数组arr的值为[1,2,3,4,5,6]。这里我们不关心compose函数是如何实现的。我们来分析一下。如果要求数组arr输出想要的结果,以上三个中间件的执行过程:1.开始执行第一个中间件,将1压入arr数组,arr数组的值为[1],以及然后等待1毫秒。为了保证arr数组的第一项是2,我们需要在调用next函数后开始执行第二个中间件。2、开始执行第二个中间件,将2压入arr数组。此时arr数组的值为[1,2],继续等待1毫秒。为了保证arr数组的第二项为3,我们还需要在调用next函数后开始执行第三个中间件。3、开始执行第三个中间件,将3压入arr数组。此时arr数组的值为[1,2,3],继续等待1毫秒。为了保证arr数组的第三项是4,我们要求在调用第三个中间next函数后,必须继续执行。4、第三个中间件执行完成后,arr数组的值为[1,2,3,4]。因此,为了保证arr数组的第4项为5,我们需要在第三个中间件执行完毕后返回到第二个中间件的next函数后执行语句。5、第二个中间件执行完成后,arr数组的值为[1,2,3,4,5]。同样,为了保证arr数组的第5项为6,我们需要在第二个中间件执行完毕后,再执行返回到第一个中间件的next函数后的语句。6、第一个中间件执行时,arr数组的值为[1,2,3,4,5,6]。为了更直观的理解上面的执行过程,我们可以把每个中间件看成一个大任务,然后以next函数为分界点,将每个大任务拆解成3个beforeNext、next和afterNext小任务。上图中,我们开始执行中间件1的beforeNext任务,然后按照紫色箭头的执行步骤完成中间件的任务调度。77.9K的axios项目能学到什么本文阿宝哥从任务注册、任务编排、任务调度三个方面分析了axios拦截器的实现。同样,阿宝哥将从以上三个方面分析Koa中间件机制。1.1任务注册在Koa中,我们创建了Koa应用对象后,可以通过调用该对象的use方法来注册中间件:constKoa=require('koa');constapp=newKoa();app.use(async(ctx,next)=>{conststart=Date.now();awaitnext();constms=Date.now()-start;console.log(`${ctx.method}${ctx.url}-${ms}女士`);});其实use方法的实现很简单。在lib/application.js文件中,我们找到了它的定义://lib/application.jsmodule.exports=classApplicationextendsEmitter{constructor(options){super();//省略部分代码this.middleware=[];}use(fn){if(typeoffn!=='function')thrownewTypeError('middlewaremustbeafunction!');//省略部分代码this.middleware.push(fn);returnthis;}}从上面可以看出代码中,fn参数的类型校验会在use方法内部进行。当验证通过时,fn指向的中间件会保存在中间件数组中,同时返回this对象,从而支持链式调用。1.2从77.9KAxios项目中的任务安排可以学到什么这篇文章中阿宝哥参考了Axios拦截器的设计模型,提炼出了如下通用的任务处理模型:在这个通用模型中,阿宝哥使用Putthepre-processor和后处理器分别在CoreWork核心任务前后完成任务安排。对于Koa的中间件机制,它通过在awaitnext()语句前后分别放置预处理器和后处理器来完成任务安排。//中间件app.use(async(ctx,next)=>{conststart=Date.now();awaitnext();constms=Date.now()-start;console.log(`${ctx.method}${ctx.url}-${ms}ms`);});1.3任务调度通过前面的分析,我们已经知道通过app.use方法注册的中间件,会保存在内部的中间件数组中。要完成任务调度,我们需要不断的从中间件数组中取出中间件执行。中间件调度算法封装在koa-compose包下的compose函数中。该函数的具体实现如下://省略一些代码"));index=i;letfn=middleware[i];if(i===middleware.length)fn=next;if(!fn)returnPromise.resolve();try{returnPromise.resolve(fn(context,dispatch.bind(null,i+1)));}catch(err){returnPromise.reject(err);}}};}compose函数接收一个参数,该参数的类型为数组,调用后这个函数,将返回一个新的函数。下面我们就以前面的例子为例,分析一下awaitcompose(stack)({});的执行过程。陈述。1.3.1dispatch(0)从上图可以看出,第一个中间件内部调用next函数的时候,其实是继续调用dispatch函数,参数i的值为1。1.3.2dispatch(1)如上图所示可以看出,在第二个中间件内部调用next函数时,仍然调用了dispatch函数。此时参数i的值为2。1.3.3dispatch(2)从上图可以看出,在第三个中间件内部调用next函数时,仍然调用了dispatch函数,参数i的值此时是3。1.3.4dispatch(3)从上图可以看出,当middleware数组中的中间件开始执行时,如果调度时没有显式设置next参数的值,那么next函数之后的语句就会开始执行返回并继续执行。当第三个中间件执行完成后,将返回执行第二个中间件下一个函数之后的语句,直到执行完中间件中定义的所有语句。分析完compose函数的实现代码,我们来看看Koa是如何使用compose函数来处理注册中间件的。constKoa=require('koa');constapp=newKoa();//响应app.use(ctx=>{ctx.body='大家好,我是阿宝哥';});app.listen(3000);使用上面的代码,我可以快速启动一个服务器。use方法之前已经分析过了,那我们来分析一下listen方法。该方法的实现如下://lib/application.jsmodule.exports=classApplicationextendsEmitter{listen(...args){debug('listen');constserver=http.createServer(this.callback());returnserver.listen(...args);}}显然在listen方法内部,会先调用Node.js内置HTTP模块的createServer方法创建服务器,然后开始监听指定端口,即,开始等待客户端的连接。另外,在调用http.createServer方法创建HTTP服务器时,我们传入的参数是this.callback()。该方法的具体实现如下://lib/application.jsconstcompose=require('koa-compose');module.exports=classApplicationextendsEmitter{callback(){constfn=compose(this.middleware);if(!this.listenerCount('error'))this.on('error',this.onerror);constandleRequest=(req,res)=>{constctx=this.createContext(req,res);returnthis.handleRequest(ctx,fn);};returnhandleRequest;}在回调方法里面,我们终于看到了久违的compose方法。调用回调方法后,将返回handleRequest函数对象来处理HTTP请求。每当Koa服务器收到客户端请求时,会调用handleRequest方法,该方法会先创建一个新的Context对象,然后执行注册的中间件来处理收到的HTTP请求:module.exports=classApplicationextendsEmitter{handleRequest(ctx,fnMiddleware){constres=ctx.res;res.statusCode=404;constoneerror=err=>ctx.onerror(err);constandleResponse=()=>respond(ctx);onFinished(res,onerror);returnfnMiddleware(ctx).then(handleResponse).catch(onerror);}}好了,Koa中间件的内容基本介绍完了。对Koa内核感兴趣的可以自行研究。接下来我们介绍洋葱模型及其应用。2.洋葱模型2.1洋葱模型简介(来源:https://eggjs.org/en/intro/egg-and-koa.html)上图中,洋葱中的每一层代表一个独立的中间件,用于实现不同的功能,比如异常处理,缓存处理等。每个请求都会从左边开始逐层经过中间件,进入最内层中间件后,再从最内层中间件逐层返回。因此,对于每一层中间件,在一个请求和响应周期中,有两个时间点加入不同的处理逻辑。2.2onion模型的应用除了onion模型在Koa中的应用,该模型在Github上的一些不错的项目中也有广泛的应用,比如koa-router和阿里巴巴的midway,umi-request等项目。在介绍了Koa的中间件和洋葱模型之后,阿宝哥根据自己的理解提炼出了如下通用的任务处理模型:://x-response-timeasyncfunctionresponseTime(ctx,next){conststart=newDate();awaitnext();constms=newDate()-start;ctx.set("X-Response-Time",ms+"ms");实际上,对于每个中间件,预处理器和后处理器都是可选的。例如下面的中间件用于设置统一的响应内容://responseasyncfunctionrespond(ctx,next){awaitnext();if("/"!=ctx.url)return;ctx.body="HelloWorld";}尽管上面介绍的两个中间件都比较简单,但是你也可以根据自己的需要实现复杂的逻辑。Koa的核心非常轻巧,小而内脏。通过提供优雅的中间件机制,开发者可以灵活地扩展Web服务器的功能。这种设计思路值得学习和借鉴。好了,这次就先介绍到这里吧。后面有机会阿宝哥会单独介绍Redux或者Express的中间件机制。