简介最近在业余时间阅读了Koa2的源码;在阅读Koa2(2.2.0版本)源码的过程中,感觉代码简洁,思路清晰(不得不佩服大神的水平)。以下是我读后的一些感受。Koa的设计理念Koa是一个轻量级的、富有表现力的http框架。一个web请求会通过Koa的中间件栈动态完成响应处理。Koa2使用async和await的语法来增强中间件的表现力。Koa并没有在内核方法中绑定任何中间件,它只是提供了一个轻量级优雅的函数库。Koa的基本构成Koa的源代码非常精简,只有四个文件:application.js:框架入口;负责管理中间件和处理请求context.js:上下文对象的原型,代理请求和响应对象上的方法和属性request.js:请求对象的原型,提供请求相关的方法和属性response.js:响应对象的原型,提供响应相关的方法和属性application.js//application.jsmodule.exports=classApplicationextendsEmitter{constructor(){super();this.proxy=false;//是否信任代理头参数,默认为falsethis.middleware=[];//保存通过app.use(middleware)注册的中间件this.subdomainOffset=2;//子域域默认偏移量,默认为2this.env=process.env.NODE_ENV||'发展';//环境参数,默认为NODE_ENV或'development'this.context=Object.create(context);//context模块,通过context.js=Object.create(request)创建this.request;//request模块,通过request.js创建this.response=Object.create(response);//response模块,通过response.js创建}//...}application.js是koa的主要入口文件,暴露了application类。该类继承自EventEmitter。这里我们可以看出它和koa1.x的区别。koa1.x使用构造函数的方法。koa2广泛使用es6语法。调用时和koa1.x不同varkoa=require('koa');//koa1.xvarapp=koa();//koa2.x//使用类必须使用new来调用varapp=新考阿();除了上面的构造函数,application.js还暴露了一些公共的API,比如常用的两个,一个是listen,一个是use。使用函数//application.jsuse(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);returnthis;}use函数做的事情很简单:注册一个中间件fn,其实就是把fn放到中间件数组中。监听函数//application.jslisten(...args){debug('listen');constserver=http.createServer(this.callback());返回server.listen(...args);}listen方法首先会通过this.callback方法返回一个函数作为http.createServer的回调函数,然后进行监听。我们已经知道http.createServer的回调函数接收两个参数:req和res,我们来看this.callback的实现://application.jscallback(){constfn=compose(this.middleware);if(!this.listeners('error').length)this.on('error',this.onerror);consthandleRequest=(req,res)=>{res.statusCode=404;constctx=this.createContext(req,res);constonerror=err=>ctx.onerror(err);consthandleResponse=()=>respond(ctx);onFinished(res,onerror);返回fn(ctx).then(handleResponse).catch(onerror);};returnhandleRequest;}首先回调方法结合所有中间件,使用koa-compose。我们来看看koa-compose的代码://koa-composefunctioncompose(middleware){//传入的中间件must是一个数组if(!Array.isArray(middleware))thrownewTypeError('Middlewarestackmust是一个数组!')//传入的中间件的每个元素必须是一个函数for(constfnofmiddleware){if(typeoffn!=='function')thrownewTypeError('中间件必须由函数组成!')}返回函数(上下文,下一个){letindex=-1returndispatch(0)functiondispatch(i){if(i<=index)returnPromise.reject(newError('next()calledmultipletimes'))index=iletfn=middleware[i]//下面两行代码处理最后一个中间件还有next的情况。其实直接resolveif(i===middleware.length)fn=nextif(!fn)returnPromise.resolve()try{//这里就是传入next执行中间件代码returnPromise.resolve(fn(context,functionnext(){returndispatch(i+1)}))}catch(err){returnPromise.reject(err)}}}}你可以看到koa-compose基本上是一个递归调用一个调度函数,其中最重要的是下面的代码:returnPromise.resolve(fn(context,functionnext(){returndispatch(i+1)}))这段代码等同于:fn(context,functionnext(){returndispatch(i+1)})returnPromise.resolve()这里middlewareFunction的第二个参数(也就是next)动态传入Messenger,会调用dispatch(index)执行下一个中间件。最后,将返回一个状态为Resolved(已完成)的Promise对象。这个对象的作用我们后面会讲到。我们暂时回到回调方法。如前所述,它首先结合中间件生成一个函数fn。然后回调方法返回http.createServer需要的回调函数handleRequest。handleRequest函数首先默认设置http编码为404,然后使用createContext函数封装node返回的req和res创建context,然后通过onFinished(res,onerror)监听http响应,并在回调时执行请求结束。这里传入的回调是context.onerror(err),当出错时执行。最后返回fn(ctx).then(handleResponse).catch(onerror)的执行结果。这里的fn函数是所有中间件组合后生成的函数。调用它执行完所有中间件后,会返回Promise对象上面提到的Resolved(完成)状态,然后执行响应处理函数respond(ctx)(response函数也主要包括一些收尾工作,比如如何http代码为空时输出,http方法为head时如何输出,body为stream或json时如何输出;代码就不贴出来了,有兴趣的朋友可以自己看看),以及当抛出异常时,使用context.onerror(err)来处理它。我们可以看一下createContext函数//application.jscreateContext(req,res){constcontext=Object.create(this.context);constrequest=context.request=Object.create(this.request);常量响应=上下文。response=Object.create(this.response);context.app=request.app=response.app=this;context.req=request.req=response.req=req;context.res=request.res=response.res=res;request.ctx=response.ctx=上下文;请求.响应=响应;response.request=请求;context.originalUrl=request.originalUrl=req.url;context.cookies=newCookies(req,res,{keys:this.keys,secure:request.secure});request.ip=request.ips[0]||req.socket.remoteAddress||'';context.accept=request.accept=接受(请求);语境。状态={};returncontext;}createContext在创建context的时候,也会同时创建request和response。通过下图可以更直观的看出所有这些对象之间的关系。图中:最左边一列代表每个文件导出的对象,中间一列代表每个Koa应用及其维护的属性,右边两列代表每个请求对应维护的一些对象列。黑线代表实例化,红线代表原型。链条的蓝线表示属性。通过上面的分析,我们可以大致知道Koa处理请求的流程:当请求到来时,会通过req和res创建一个上下文(ctx),然后执行中间件。content.jscontent.js的主要功能是提供对请求和响应对象的方法和属性的方便访问。其中使用了node-delegates(有兴趣可以看源码),将context.request和context.response上的方法和属性委托给context。在源代码中,我们可以看到://context.jsdelegate(proto,'response').method('attachment')//....access('status')//....getter('writable');delegate(proto,'request').method('acceptsLanguages')//....access('querystring')//....getter('ip');request.jsrequest.js封装Request相关的属性和方法。通过application.js中的createContext方法代理对应的request对象。constrequest=context.request=Object.create(this.request);//...context.req=request.req=response.req=req;//...request.response=response;request.req是原始请求对象,通过ths.req(即request.req)获取request.js中的属性。response.jsresponse.js封装了响应相关的属性和方法。和request一样,通过createContext方法代理对应的response对象。constresponse=context.response=Object.create(this.response);//...context.res=request.res=response.res=res;//...response.request=request;结语关于Koa2源码先分析一下,希望对大家有所帮助。如有不同看法,欢迎交流!
