在前文我们介绍过什么是Koa2的基础简单回顾下什么是koa2NodeJS的web开发框架Koa可被视为nodejs的HTTP模块的抽象源码重点 中间件机制 洋葱模型 compose源码结构 Koa2的源码地址:https:github。comkoajskoa 其中lib为其源码 koa2源码 可以看出,只有四个文件:application。js、context。js、request。js、response。jsapplication 为入口文件,它继承了Emitter模块,Emitter模块是NodeJS原生的模块,简单来说,Emitter模块能实现事件监听和事件触发能力 application1 删掉注释,从整理看Application构造函数 Application构造函数 Application在其原型上提供了listen、toJSON、inspect、use、callback、handleRequest、createContext、onerror等八个方法,其中listen:提供HTTP服务use:中间件挂载callback:获取httpserver所需要的callback函数handleRequest:处理请求体createContext:构造ctx,合并node的req、res,构造Koa的参数ctxonerror:错误处理 其他的先不要在意,我们再来看看构造器constructor Application的构造器 晕,这都啥和啥,我们启动一个最简单的服务,看看实例constKoarequire(Koa)constappnewKoa()app。use((ctx){ctx。bodyhelloworld})app。listen(3000,(){console。log(3000请求成功)})console。dir(app) 实例 能看出来,我们的实例和构造器一一对应, 打断点看原型 断点 哦了,除去非关键字段,我们只关注重点 Koa的构造器上的this。middleware、this。context、this。request、this。response 原型上有:listen、use、callback、handleRequest、createContext、onerror 注:以下代码都是删除异常和非关键代码先看listen。。。listen(。。。args){constserverhttp。createServer(this。callback())returnserver。listen(。。。args)}。。。 可以看出listen就是用http模块封装了一个http服务,重点是传入的this。callback()。好,我们现在就去看callback方法callbackcallback(){constfncompose(this。middleware)consthandleRequest(req,res){constctxthis。createContext(req,res)returnthis。handleRequest(ctx,fn)}returnhandleRequest} 它包含了中间件的合并,上下文的处理,以及res的特殊处理中间件的合并 使用了koacompose来合并中间件,这也是洋葱模型的关键,koacompose的源码地址:https:github。comkoajscompose。这代码已经三年没动了,稳的一逼functioncompose(middleware){returnfunction(context,next){letindex1returndispatch(0)functiondispatch(i){if(iindex)returnPromise。reject(newError(next()calledmultipletimes))indexiletfnmiddleware〔i〕if(imiddleware。length)fnnextif(!fn)returnPromise。resolve()try{returnPromise。resolve(fn(context,dispatch。bind(null,i1)))}catch(err){returnPromise。reject(err)}}}} 一晃眼是看不明白的,我们需要先明白middleware是什么,即中间件数组,那它是怎么来的呢,构造器中有this。middleware,谁使用到了use方法 我们先跳出去先看use方法useuse(fn){this。middleware。push(fn)returnthis} 除去异常处理,关键是这两步,this。middleware是一个数组,第一步往this。middleware中push中间件;第二步返回this让其可以链式调用,当初本人被面试如何做promise的链式调用,懵逼脸,没想到在这里看到了 回过头来看koacompose源码,设想一下这种场景。。。app。use(async(ctx,next){console。log(1);awaitnext();console。log(6);});app。use(async(ctx,next){console。log(2);awaitnext();console。log(5);});app。use(async(ctx,next){console。log(3);ctx。console。log(4);});。。。 我们知道它的运行是123456 它的this。middleware的构成是this。middleware〔async(ctx,next){console。log(1)awaitnext()console。log(6)},async(ctx,next){console。log(2)awaitnext()console。log(5)},async(ctx,next){console。log(3)ctx。bodyhelloworldconsole。log(4)},〕 不要感到奇怪,函数也是对象之一,是对象就可以传值 constfncompose(this。middleware) 我们将其JavaScript化,其他不用改,只需要把最后一个函数改成async(ctx,next){console。log(3);ctx。console。log(helloworld);console。log(4);} 测试compose 测试compose2逐行解析koacompose 这一段很重要,面试的时候常考,让你手写一个compose,淦它1。async(ctx,next){console。log(1);awaitnext();console。log(6);}中间件2。constfncompose(this。middleware)合并中间件3。fn()执行中间件functioncompose(middleware){returnfunction(context,next){letindex1;returndispatch(0);functiondispatch(i){if(iindex)returnPromise。reject(newError(next()calledmultipletimes),);letfnmiddleware〔i〕;if(imiddleware。length)if(!fn)returnPromise。resolve();try{returnPromise。resolve(fn(context,dispatch。bind(null,i1)));}catch(err){returnPromise。reject(err);}}};} 执行constfncompose(this。middleware),即如下代码constfnfunction(context,next){letindex1returndispatch(0)functiondispatch(i){if(iindex)returnPromise。reject(newError(next()calledmultipletimes))indexiletfnmiddleware〔i〕if(imiddleware。length)fnnextif(!fn)returnPromise。resolve()try{returnPromise。resolve(fn(context,dispatch。bind(null,i1)))}catch(err){returnPromise。reject(err)}}}} 执行fn(),即如下代码:constfnfunction(context,next){letindex1returndispatch(0)functiondispatch(i){if(iindex)returnPromise。reject(newError(next()calledmultipletimes))indexiindex0letfnmiddleware〔i〕fn为第一个中间件if(imiddleware。length)fnnext当弄到最后一个中间件时,最后一个中间件赋值为fnif(!fn)returnPromise。resolve()try{returnPromise。resolve(fn(context,dispatch。bind(null,i1)))返回一个Promise实例,执行递归执行dispatch(1)}catch(err){returnPromise。reject(err)}}}} 也就是第一个中间件,要先等第二个中间件执行完才返回,第二个要等第三个执行完才返回,直到中间件执行执行完毕 Promise。resolve就是个Promise实例,之所以使用Promise。resolve是为了解决异步,之所以使用Promise。resolve是为了解决异步 抛去Promise。resolve,我们先看一下递归的使用,执行以下代码constfnfunction(){returndispatch(0);functiondispatch(i){if(i3)i;console。log(i);returndispatch(i);}};fn();1,2,3,4 回过头来再看一次compose,代码类似于假设this。middleware〔fn1,fn2,fn3〕functionfn(context,next){if(imiddleware。length)fnnextfn3没有nextif(!fn)returnPromise。resolve()因为fn为空,执行这一行functiondispatch(0){returnPromise。resolve(fn(context,functiondispatch(1){returnPromise。resolve(fn(context,functiondispatch(2){returnPromise。resolve()}))}))}}} 这种递归的方式类似执行栈,先进先出 执行栈 这里要多思考一下,递归的使用,对Promise。resolve不用太在意上下文的处理 上下文的处理即调用了createContextcreateContext(req,res){constcontextObject。create(this。context)constrequest(context。requestObject。create(this。request))constresponse(context。responseObject。create(this。response))context。apprequest。appresponse。appthiscontext。reqrequest。reqresponse。reqreqcontext。resrequest。resresponse。resresrequest。ctxresponse。ctxcontextrequest。responseresponseresponse。requestrequestcontext。originalUrlrequest。originalUrlreq。urlcontext。state{}returncontext} 传入原生的request和response,返回一个上下文context,代码很清晰,不解释res的特殊处理 callback中是先执行this。createContext,拿到上下文后,再去执行handleRequest,先看代码:handleRequest(ctx,fnMiddleware){constresctx。resres。statusCode404constonerror(err)ctx。onerror(err)consthandleResponse()respond(ctx)onFinished(res,onerror)returnfnMiddleware(ctx)。then(handleResponse)。catch(onerror)} 一切都清晰了constKoarequire(Koa);constappnewKoa();console。log(app,app);app。use((ctx,next){ctx。});app。listen(3000,(){console。log(3000请求成功);}); 这样一段代码,实例化后,获得了this。middleware、this。context、this。request、this。response四大将,你使用app。use()时,将其中的函数推到this。middleware。再使用app。listen()时,相当于起了一个HTTP服务,它合并了中间件,获取了上下文,并对res进行了特殊处理错误处理onerror(err){if(!(errinstanceofError))thrownewTypeError(util。format(nonerrorthrown:j,err))if(404err。statuserr。expose)returnif(this。silent)returnconstmsgerr。stackerr。toString()console。error()console。error(msg。replace(gm,))console。error()}context。js 引入我眼帘的是两个东西1。constprotomodule。exports{inspect(){。。。},toJSON(){。。。},。。。}2。delegate(proto,response)。method(attachment)。access(status)。。。 第一个可以理解为,constproto{inspect(){。。。}。。。},并且module。exports导出这个对象 第二个可以这么看,delegate就是代理,这是为了方便开发者而设计的将内部对象response的属性,委托至暴露在外的proto上delegate(proto,response)。method(redirect)。method(vary)。access(status)。access(body)。getter(headerSent)。getter(writable);。。。 而使用delegate(proto,response)。access(status)。。。,就是在context。js导出的文件,把proto。response上的各个参数都代理到proto上,那proto。response是什么?就是context。response,context。response哪来的? 回顾一下,在createContext中createContext(req,res){constcontextObject。create(this。context)constrequest(context。requestObject。create(this。request))constresponse(context。responseObject。create(this。response))。。。} context。response有了,就明了了,context。responsethis。response,因为delegate,所以context。response上的参数代理到了context上了,举个例子ctx。header是ctx。request。header上代理的ctx。body是ctx。response。body上代理的request。js和response。js 一个处理请求对象,一个处理返回对象,基本上是对原生req、res的简化处理,大量使用了ES6中的get和post语法 大概就是这样,了解了这么多,怎么手写一个Koa2呢,请看下一篇手写Koa2参考资料KOA2框架原理解析和实现可能是目前最全的koa源码解析指南