前言上一篇《nodejs之express中间件》已经详细讲解了express中间件的实现,也对实现该中间件的框架Connect的源码做了简单的分析。里面也提到express的中间件是线性的,koa的中间件是onion。这篇文章会讲一下koa的中间件。koa简介Koa和express是同一个团队开发的。很像express,也是一个功能极简的框架,所以项目中需要的大部分东西都是以中间件的形式引入的。目前koa有1.x和2.x版本,1.x版本基于generator,2.x版本基于generator/async,await。由于generator的语法明显不如async,所以后续版本会去掉generator的使用,改用async的方式。但是,2.x过渡版本仍然兼容生成器。需要注意的是,在使用生成器或依赖生成器的第三方库时,会报警告,大致意思是“生成器在当前版本中仍然可以正常使用,但会在以后的版本中移除”。注:本文所有koa均采用koa2版本编写。并且兼容koa3。koa中间件相对于express的线性中间件,koa的中间件没有那么直观。先看一张图,把洋葱圈当成一个中间件。直线型是从第一个中间件到最后一个,但是洋葱形状很特别。最早使用的中间件是洋葱的最外层,一开始会按顺序走到所有中间件,然后倒序走到所有中间件,相当于每个中间件都进入两次。这给了我们更多的操作空间。看下面这段代码constkoa=require('koa');letserver=newkoa();server.use(async(ctx,next)=>{console.log('a-1');next();console.log('a-2');})server.use(async(ctx,next)=>{console.log('b-1');next();console.log('b-2');})server.use(async(ctx,next)=>{console.log('c');})server.listen(3000);代码执行后,命令行输出的顺序在koa官方文档上是外层的中间件称为“上游”,内层的中间件称为“下游”。一般中间件会执行两次,第一次在调用next之前,调用next时控制权按顺序传递给下游中间件。当下游没有中间件或者中间件没有执行next函数时,会依次恢复上游中间件的行为,由上游中间件执行next之后的代码。如果某个中间件中有异步代码,最好使用async/await来处理异步。我们已经弄清楚了如何使用它。我们先看一下内部实现原理。源码解析在github上找到koa的源码。核心功能都在application.js中。学习了上一篇的源码,这部分源码阅读起来应该压力不大。整个代码200多行,这里我只截取比较重要的部分。'usestrict';module.exports=classApplicationextendsEmitter{constructor(){super();this.proxy=false;这个.middleware=[];this.subdomainOffset=2;this.env=process.env.NODE_ENV||'发展';this.context=Object.create(context);this.request=Object.create(请求);this.response=Object.create(response);如果(util.inspect.custom){这个[util.inspect.custom]=this.inspect;}}listen(...args){debug('listen');constserver=http.createServer(this.callback());返回server.listen(...args);}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);归还这个;}callback(){constfn=compose(this.middleware);如果(!this.listenerCount('error'))this.on('error',this.onerror);consthandleRequest=(req,res)=>{constctx=this.createContext(req,res);返回这个.handleRequest(ctx,fn);};返回句柄请求;}handleRequest(ctx,fnMiddleware){constres=ctx.res;res.statusCode=404;constonerror=err=>ctx.onerror(err);consthandleResponse=()=>respond(ctx);onFinished(res,onerror);返回fnMiddleware(ctx).then(handleResponse).catch(onerror);}};为了验证,第二个if用于兼容生成器函数的使用。最后执行this.middleware.push(fn);即中间件放在middleware数组中,类似于connect中的stack数组。2、再看回调函数。回调函数的返回值是listen函数中执行createServer函数的回调,即handleRequest函数,也可以理解为用来响应request事件的函数。3.注意当this.handleRequest(ctx,fn);在回调中执行,这个fn是中间件数组经过compose函数处理后返回的值。然后看handleRequest函数,在最后调用这个函数,那么在这之前,你需要知道compose函数对中间件数组做了什么?找到koa-compose包,拦截里面的compose函数。functioncompose(middleware){if(!Array.isArray(middleware))thrownewTypeError('Middlewarestackmustbeanarray!')for(constfnofmiddleware){if(typeoffn!=='function')thrownewTypeError('中间件必须由函数组成!')}returnfunction(context,next){//最后调用的中间件#letindex=-1returndispatch(0)functiondispatch(i){if(i<=index)returnPromise.reject(newError('next()calledmultipletimes'))index=iletfn=middleware[i]if(i===middleware.length)fn=nextif(!fn)返回Promise。resolve()try{returnPromise.resolve(fn(context,dispatch.bind(null,i+1)));}}catch(err){returnPromise.reject(err)}}}}前两段显然是在验证数组和函数的类型时。然后返回一个函数,就是回调函数中的fn,也是handleRequest函数中最后调用的函数。先总结一下上面的内容:首先在use函数中将中间件添加到this.middleware数组中。在回调函数中,首先通过compose函数操作this.middleware数组,将数组转化为函数赋值给fn(注意这个函数比较特殊,下面会详细说明),然后调用fn通过handleRequest函数实现,很明显该函数必须返回一个Promise对象。如果这个过程清楚了,我们再来看看这个compose做了什么。其实compose返回一个函数,使用闭包遍历中间件数组,返回一个Promise对象,resolve回调执行当前索引对应的中间件函数,并赋值给内部闭包函数next调用时,同时index值会+1,这样在中间件中执行next()就相当于执行了dispatch(i+1),自然就找到了下一个中间件。如果中间件没有执行next或者遍历完数组中的所有中间件之后,自然会开始一步步执行每个中间件next之后的代码。这就是洋葱圈的实现。这里的逻辑有点绕,不懂也没关系,我把代码流程简化一下,通过图片说一下整个实现过程。整个核心还是compose函数。如果把这个函数简化到极致,就是下面的代码functiona(){console.log('a-1');下一个(b);console.log('a-2');}functionb(){console.log('b-1');下一个(c);console.log('b-2');}functionc(){console.log('c');}functionnext(fn){returnPromise.resolve(fn());}//输出为//a-1//b-1//c//b-2//a-2至此我们分析了express中间件和koa中间件的实现原理。两者的源代码都非常简洁,设计巧妙,可读性强。很多细节值得借鉴。希望自己在源码阅读的道路上越走越远。
