当前位置: 首页 > 后端技术 > Node.js

由浅入深了解Koa2的源码

时间:2023-04-03 18:01:19 Node.js

在上一篇文章中,我们介绍了Koa2的基础是什么。先简单回顾一下什么是koa2。NodeJS的web开发框架Koa可以看作是nodejs的HTTP模块的抽象源码关键中间件机制Onion模型compose源码结构Koa2的源码地址:https://github.com/koajs/koa其中lib是它的源代码。可以看到只有四个文件:application.js、context.js、request.js、response.jsapplication是入口文件。它继承了Emitter模块。Emitter模块是NodeJS的原生模块。简单的说,Emitter模块可以实现事件监听和事件触发的能力。删除评论。从整理的角度来看,Application构造器Application在其原型上提供了listen、toJSON、inspect、use、callback、handleRequest、createContext、onerror等八个方法,其中listen:提供HTTP服务use:中间件mountcallback:获取回调http服务器需要的函数handleRequest:处理请求体createContext:构造ctx,合并节点req,res,构造koa的参数——ctxonerror:错误处理等不用担心。让我们看一下构造函数。都是关于它的。让我们开始最简单的服务,看看例子constKoa=require('Koa')constapp=newKoa()app.use((ctx)=>{ctx.body='helloworld'})app.listen(3000,()=>{console.log('3000requestSuccess')})console.dir(app)可以看到我们的实例和构造函数一一对应,断点看原型,除了非key字段,我们只关注this在key的构造函数上Koa.middleware、this.context、this.request、this.response的原型有:listen、use、callback、handleRequest,createContext,onerror注:以下代码是删除异常和非关键代码先看listen...listen(...args){constserver=http.createServer(this.callback())returnserver.listen(...args)}...可以看出listen使用http模块封装了一个http服务。关键点是传入的this.callback()。好,我们来看回调方法callbackcallback(){constfn=compose(this.middleware)consthandleRequest=(req,res)=>{constctx=this.createContext(req,res)returnthis.handleRequest(ctx,fn)}returnhandleRequest}里面包含中间件的组合,context处理,res特殊处理中间件的合并使用koa-compose合并中间件,这也是onion模型的关键。koa-compose的源码地址:https://github.com/koajs/compose。这段代码三年没动过,很稳定。functioncompose(middleware){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)))}catch(err){returnPromise.reject(err)}}}}一看就看不懂,需要了解什么是中间件,也就是一个中间件数组,怎么来的,构造函数里有个this.middleware,谁用过-use方法先跳出来看use方法useuse(fn){this.middleware.push(fn)returnthis}去掉异常处理,关键是这两步,this.middleware是一个数组,第一步是把this.middleware中的中间件push进去;第二步是returnthissosoitcanbechaining一开始面试howtomakepromisechaincalls,我就懵了。没想到会在这里看到。回头看koa-compose源码,想象一下这种场景...app.use(async(ctx,next)=>{console.log(1);awaitnext();console.log(6);});app.use(async(ctx,next)=>{控制台日志(2);等待下一个();console.log(5);});app.use(async(ctx,next)=>{console.log(3);ctx.body="helloworld";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.body='helloworld'控制台。log(4)},]别奇怪,函数也是对象之一,如果是对象就可以传值。constfn=compose(this.middleware)我们将它JavaScript化,其他不用改,只需要将最后一个函数改成async(ctx,next)=>{console.log(3);-ctx.body='你好世界';+console.log('你好世界');console.log(4);}逐行分析koa-compose很重要。它经常在面试中进行测试。让你手写一篇文章。//1。async(ctx,next)=>{console.log(1);等待下一个();控制台日志(6);}中间件//2。constfn=compose(this.middleware)合并中间件//3.fn()执行中间件functioncompose(middleware){returnfunction(context,n分机){让指数=-1;返回调度(0);functiondispatch(i){if(i<=index)returnPromise.reject(newError('next()被多次调用'),);索引=我;让fn=中间件[i];if(i===middleware.length)fn=next;如果(!fn)返回Promise.resolve();尝试{returnPromise.resolve(fn(context,dispatch.bind(null,i+1)));}catch(err){returnPromise.reject(err);}}};}执行constfn=compose(this.middleware),即下代码constfn=function(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()尝试{returnPromise.resolve(fn(context,dispatch.bind(null,i+1)))}catch(err){returnPromise.reject(err)}}}}执行fn(),即如下代码:constfn=function(context,next){letindex=-1returndispatch(0)functiondispatch(i){if(i<=index)returnPromise.reject(newError('next()calledmultipletimes'))index=i//index=0letfn=middleware[i]//fn是第一个中间件if(i===middleware.length)fn=next//当找到最后一个中间件时,将最后一个中间件分配给fnif(!fn)returnPromise.resolve()try{returnPromise.resolve(fn(context,dispatch.bind(null,i+1)))//返回一个Promise实例,执行递归dispatch(1)}catch(err){returnPromise.reject(err)}}}}是第一个中间件,它必须等待第二个中间件执行完才返回,而第二个一个会等第三个执行完再返回Return,直到中间件执行完成。Promise.resolve是一个Promise实例。之所以使用Promise.resolve是为了解决异步。之所以使用Promise.resolve是为了解决异步。,执行以下代码constfn=function(){returndispatch(0);函数调度(i){如果(i>3)返回;我++;控制台日志(一);返回调度(i++);}};fn();//1,2,3,4回顾一次compose,代码类似于//assumethis.middleware=[fn1,fn2,fn3]functionfn(context,next){if(i===middleware.length)fn=next//fn3没有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){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=thiscontext.req=request.req=response.req=reqcontext.res=request.res=response.res=resrequest.ctx=response.ctx=contextrequest.response=response.request=requestcontext.originalUrl=request.originalUrl=req.url上下文。state={}returncontext}传入原始的request和response,返回一个context—context,代码很清楚,没有说明res的特殊处理。回调中先执行this.createContext,拿到上下文后执行handleRequest,先看代码:handleRequest(ctx,fnMiddleware){constres=ctx.resres.statusCode=404constonerror=(err)=>ctx.onerror(err)consthandleResponse=()=>respond(ctx)onFinished(res,onerror)returnfnMiddleware(ctx).then(handleResponse).catch(onerror)}一切都清楚了constKoa=require('Koa');constapp=newKoa();console.log('app',app);app.use((ctx,next)=>{ctx.body='helloworld';});app.listen(3000,()=>{console.log('3000请求成功');});这么一段代码,实例化后,得到this.middleware、this.context、this.request、this.response这四将。使用app.use()时,将里面的函数push到this.mi中间件再次使用app.listen()时,相当于一个HTTP服务,加入中间件,获取上下文,对res错误处理做特殊处理onerror(err){if(!(errinstanceofError))thrownewTypeError(util.format('非错误抛出:%j',err))if(404==err.status||err.expose)returnif(this.silent)returnconstmsg=err.stack||err.toString()console.error()console.error(msg.replace(/^/gm,''))console.error()}context.js给我带来了两件事//1.constproto=module.exports={inspect(){...},toJSON(){...},...}//2.delegate(proto,'response').method('attachment').access('status')...第一个可以理解为,constproto={inspect(){...}...},module.exportsexportsthisobject第二个可以这样看,delegate就是代理,这个是为了方便开发者//将内部对象response的属性委托给暴露的protodelegate(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从哪里来?回顾一下,在createContextcreateContext(req,res){constcontext=Object.create(this.context)constrequest=(context.request=Object.create(this.request))constresponse=(context.response=Object。create(this.response))...}context.response很明确,context.response=this.response,因为有delegate,context.response上的参数被委托给了context,比如ctx.header就是ctx.body代理在ctx.request.header上,request.js和response.js代理在ctx.response.body上,一个处理请求对象,一个处理返回对象,基本上是对原来req和res处理的简化,很多ES6中get和post语法的使用大概就是这样的。了解了这么多,如何手写一个Koa2,请看下一篇-手写Koa2参考资料。对KOA2框架原理的分析和实现可能是目前最完整的。koa源码分析指南