前言koa是一个被广泛使用的node框架,它的源码非常简洁,看完之后越发佩服TJ了,它能用so实现这么强大又好用的框架小代码。下面结合源码详细分析核心原理。整体结构首先看一下koa框架的结构。koa的源码由四部分组成,分别是application.js(入口文件)、context.js(context,即koa的ctx)、request.js(请求对象,基于req封装)、response.js(response对象,基于res包),核心代码主要位于application.js中。下面将从这四个文件开始具体分析:applicationapplication.js是koa的入口文件,也是核心。在这个文件中,引入了另外三个文件,在构造函数中定义了一些核心属性,主要是中间件:这是注册中间件的集合context:context模块,继承自context.js创建的对象request:request模块,继承fromtheobjectcreatedbyrequest.jsresponse:响应模块,继承自response.js创建的对象constcontext=require('./context');constrequest=require('./request');constresponse=require('./response');....构造函数(选项){super();选项=选项||{};this.proxy=options.proxy||错误的;this.subdomainOffset=options.subdomainOffset||2;this.proxyIpHeader=options.proxyIpHeader||'X-Forwarded-For';this.maxIpsCount=options.maxIpsCount||0;this.env=options.env||process.env.NODE_ENV||'发展';如果(options.keys)this.keys=options.keys;这个.middleware=[];//中间件数组this.context=Object.create(context);//上下文对象this.request=Object.create(request);//请求对象this.response=Object.create(response);//响应对象if(util.inspect.custom){this[util.inspect.custom]=this.inspect;接下来看koa使用方法listen,listen方法是对http.createServer做了简单的封装,提取出单独的回调函数,http服务监听传入的端口。Node原生的http.createServer方法需要传入一个回调函数进行处理,但是在实际复杂的业务逻辑中,代码难免难以管理,所以koa在这里单独处理了回调函数。listen(...args){debug('listen');constserver=http.createServer(this.callback());返回server.listen(...args);}继续看单独提取的回调方法,这个方法主要做了三件事:统一集成注册的中间件处理监控框架运行错误,设置错误处理函数返回请求处理函数。下面让我们详细看看这三个步骤:callback(){constfn=compose(this.middleware);如果(!this.listenerCount('error'))this.on('error',this.onerror);consthandleRequest=(req,res)=>{constctx=this.创建上下文(请求,资源);返回this.handleRequest(ctx,fn);};返回句柄请求;}compose该方法第一步调用compose方法集成中间件,compose方法单独编写koa-compose模块是koa中间件处理的核心。详细看处理逻辑很有意思:functioncompose(middleware){//入参必须是数组,数组中的元素必须是函数(每个中间件每一项都是一个函数)if(!Array.isArray(middleware))thrownewTypeError('Middlewarestackmustbeanarray!')for(constfnofmiddleware){if(typeoffn!=='function')thrownewTypeError('Middlewaremustbecomposedof函数!')}/***@param{Object}context*@return{Promise}*@apipublic*/returnfunction(context,next){//最后调用的中间件#letindex=-1returndispatch(0)functiondispatch(i){if(i<=index)returnPromise.reject(newError('next()被多次调用'))index=iletfn=middleware[i]if(i===middleware.length)fn=nextif(!fn)returnPromise.resolve()try{returnPromise.resolve(fn(context,dispatch.bind(null,i+1)));}//返回方法中的第二个参数递归调用下一个中间件方法,这就是为什么在中间件执行next()会调用下一个中间件函数}catch(err){returnPromise.reject(err)}}}}该方法中首先对传入的中间件集合的入参进行判断,只能是数组,且数组中的每个元素必须是函数,这就要求注册的中间件必须是函数。接下来是核心。该方法返回一个函数。第一个参数context为请求上下文,第二个参数next为所有中间件执行。完成后最终执行的回调函数。我们重点看下这个方法中最核心的dispatch函数的逻辑:dispatchdispatch函数会遍历middleware中间件集合,一个一个的取出中间件执行,直到所有的中间件都执行完。fn(context,dispatch.bind(null,i+1))这条语句是最关键的一条。执行当前中间件函数,将context上下文作为第一个参数传入,下一个要执行的中间件方法作为第二个参数传入。这就是为什么当我们执行koa中间件中的next()方法(对应这里的第二个参数)时,会执行下一个中间件函数。如果不调用next(),那么后面的中间件功能将无法实现。监听error第二步是通过this.on('error',this.onerror)监听框架中的error事件,相应的onerror处理函数进行相应的错误处理onerror(err){if(!(errinstanceofError))thrownewTypeError(util.format('非错误抛出:%j',err));如果(404==err.status||err.expose)返回;if(this.silent)返回;constmsg=err.stack||err.toString();控制台.错误();console.error(msg.replace(/^/gm,''));控制台.错误();}returnhandleRequestcreateContext首先执行constctx=this.createContext(req,res)创建当前请求的上下文对象。createContext方法的作用是创建一个context对象,并将当前的this、req、res挂载到该对象上。这也是为什么我们在使用koa时可以在请求的ctx上获取到app、req、res上各种请求相关的属性。createContext(req,res){constcontext=Object.create(this.context);constrequest=context.request=Object.create(this.request);constresponse=context.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.state={};返回上下文;}handleRequest后面跟着returnthis.handleRequest(ctx,fn)其中ctx是上一步创建的请求上下文对象,fn是compose返回的闭包函数。handleRequest方法最终返回fnMiddleware(ctx).then(handleResponse).catch(onerror);即响应处理函数是在所有中间件执行完毕后执行的。handleRequest(ctx,fnMiddleware){constres=ctx.res;res.statusCode=404;constonerror=err=>ctx.onerror(err);consthandleResponse=()=>respond(ctx);onFinished(res,onerror);//监听请求结束的第三方库。当出错时,执行默认的错误回调函数returnfnMiddleware(ctx).then(handleResponse).catch(onerror);//所有中间件执行完毕后执行response处理函数,如果抛出异常,则执行默认的错误回调函数}respond接下来重点关注上一步对应处理函数中用到的respond函数,又是一个核心函数在koa中。主要是针对不同的响应体和状态进行不同的处理,主要分为以下几种情况:没有响应体时的处理if(statuses.empty[code]){//返回的状态码表示没有相应的主体//剥离标题ctx.body=null;返回res.end();}HEAD请求方法,响应头已经发送,但是没有内容长度时的处理if('HEAD'===ctx.method){//HEAD请求方法if(!res.headersSent&&!ctx.response.has('Content-Length')){//响应头已经发送,但是没有内容长度,设置它const{length}=ctx.response;if(Number.isInteger(length))ctx.length=length;}返回res.end();}有对应的body,但是是空的if(null==body){//有对应的body,但是是空的if(ctx.response._explicitNullBody){ctx.response.remove('Content-Type');ctx.response.remove('传输编码');返回res.end();}...有对应的body,不同的格式Processing//responses,处理不同的responsebodyif(Buffer.isBuffer(body))returnres.end(body);if('string'==typeofbody)returnres.end(body);if(bodyinstanceofStream)returnbody.pipe(res);//主体:jsonbody=JSON.stringify(body);if(!res.headersSent){ctx.length=Buffer.byteLength(正文);}res.end(body);至此,整个koa入口文件的主要流程使用方法分析完毕。除了这个主流程之外,还有一个中间件使用方法use(),需要单独看useuse方法,就是koa中注册中间件的方法。原理其实很简单。当调用use方法注册中间件时,实质上就是将中间件功能推送到框架中间件集合this.middleware中,从而提前执行中间件。先出。并且函数最终返回this,保证了中间件的注册可以实现链式调用。具体代码和注释如下:use(fn){if(typeoffn!=='function')thrownewTypeError('middlewaremustbeafunction!');//koa1.x版本使用GeneratorFunction来写中间,而koa2则使用E??S6async/await代替。所以在use()函数中会判断是否是老式中间件,转换老式中间件.'+'请参阅文档以获取有关如何转换旧中间件的示例'+'https://github.com/koajs/koa/blob/master/docs/migration.md');fn=转换(fn);}debug('使用%s',fn._name||fn.name||'-');这个.middleware.push(fn);归还这个;//returnthis,这样就可以链式调用了.使用的委托方法是第三方库,用于代理对象的属性和方法。委托(原型,'request').method('acceptsLanguages').method('acceptsEncodings').method('acceptsCharsets').method('accepts').method('get').method('is')...委托(原型,'响应').method('附件').method('redirect').method('remove').method('vary').method('has').method('set')...2.定义onerror错误处理函数,在前面application.js中的handleRequest中用到了。这个函数主要是根据不同的情况做不同的处理:onerror(err){if(null==err)return;if(!(errinstanceofError))err=newError(util.format('非错误抛出:%j',err));...if('ENOENT'==err.code)err.status=404;//默认为500if('number'!=typeoferr.status||!statuses[err.status])err.status=500;//响应constcode=statuses[err.status];constmsg=err.expose?错误消息:代码;this.status=err.status;this.length=Buffer.byteLength(msg);res.end(msg);}request&&response最后的request和response模块,没有什么特别需要分析的,只是封装了request和response的相关属性和方法,并使用set和get函数的形式对属性进行读写操作。一般封装代码如下:getsearch(){if(!this.querystring)return'';返回`?${this.querystring}`;},设置搜索(str){this.querystring=str;},...onionringmodel最后重点说一下koa中间件请求的onionringmodel,这是koa区别于express的一大特点。用网上的一张经典图片和一个简单的例子来说明:constKoa=require('koa');constapp=newKoa();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.body="lastmiddleware";console.log(4);});app.listen(3000,()=>{~~~~console.log('listenningon3000');});//依次输出1,2,3,4,5,6为什么可以达到这样的效果呢?其实我们引入koa-compose的源码中就已经可以找到答案了。koa中间件机制returnPromise.resolve(fn(context,dispatch.bind(null,i+1)))每次都返回一个promise,在中间件方法中调用next()时,对应执行下一个中间件。所以,当awaitnext()会等待下一个中间件执行完毕,然后回到当前中间件继续执行后面的代码。这就是洋葱圈模型的实现原理。
