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

手撕KOA2源码

时间:2023-04-03 18:38:29 Node.js

我在顺丰平台潜水很久了。这也是我的第一篇文章,之前一直在重点学习。希望以后可以通过写技术文章和回答问题来做一些输出^^以前没有这个习惯,所以语言组织可能有点混乱。最近两次面试都被问到KOA的洋葱圈模型(因为学校我用KOA2写了好几个项目),但是我没有深挖洋葱圈的原理,感觉回答的不是很满意。所以这次看了一下KOA的源码。目录结构├──lib│├──application.js│├──context.js│├──request.js│└──response.js└──package.json目前在我们下载的node_modules/koa包中这就是源文件结构。koa处理请求的核心就是以上四个文件,其中application.js是整个koa2的入口。最重要的中间件逻辑也在这里处理。本研究是档案。context.js负责处理应用上下文request.js是处理http请求response.js是处理http响应);选项=选项||{};this.proxy=options.proxy||false;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||'发展';//环境变量if(options.keys)this.keys=options.keys;this.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;}}listenlisten(...args){debug('listen');constserver=http.createServer(this.callback());returnserver.listen(...args);}KOA的监听函数是对原来的createServer的简单封装,传入的参数会直接传给原来的server.listen。只是生成一个配置对象,通过KOA类中的回调方法传递给服务端。从这个角度来看,KOA的实际执行逻辑其实是通过回调函数暴露出来的。callbackcallback(){constfn=compose(this.middleware);如果(!this.listenerCount('error'))this.on('error',this.onerror);consthandleRequest=(req,res)=>{constctx=this.createContext(req,res);返回this.handleRequest(ctx,fn);};returnhandleRequest;}先看第一句constfn=compose(this.middleware);this.midlleware显然在实例中间件队列中。众所周知,compose就是合成的意思。我们之前学过的一个短语是becomposedof->composedof。代码中的表达式大概是组合函数g()+h()=>g(h())这里的compose变量来自于koa-compose包,其作用是合并执行所有的koa中间件。可以这样理解,之前的中间件数组只是一些零散的洋葱圈层,经过koa-compose处理后成为一个完整的洋葱(文末会附上koa-compose的原理)。回到上面的方法,得到中间部分组合后的组合函数fn,声明一个最终输出的handleRequest函数。函数中首先通过this.createContext初始化消息,可以理解为生成完整的请求消息和响应消息,初始化消息的逻辑写在lib/request.js和lib/response.js中.最后,他们通过this.handleRequest方法处理消息。handleRequest(ctx,fnMiddleware){constres=ctx.res;res.statusCode=404;constonerror=err=>ctx.onerror(err);consthandleResponse=()=>respond(ctx);onFinished(res,onerror);返回fnMiddleware(ctx)。处理完逻辑后,做一些后续处理(返回客户端/错误处理)。众所周知,洋葱圈模型的每一层都是一个Promise对象。只有当上游(官方文档)洋葱圈进入resolved状态,线程的使用权才会传递给下游。(请注意,此时上一层洋葱圈还没有执行完毕。)当内层Promise变为resolved后,JS线程的使用权会继续向上冒泡处理外层之后的逻辑洋葱圈的问题得到解决。洋葱圈函数接收到的ctx和next这两个参数中的next方法是一个异步方法,也就是说强制解析当前层的洋葱圈,把控制权交给下游。当前函数将被阻塞,直到处理完所有下游逻辑。继续执行。看到这里,我们已经可以把目前看到的部分整理出来了。KOA实例收集一些需要的中间件(use方法),在this.callback函数中通过koa-compose组合中间件,生成一个洋葱圈处理器fn(要处理的对象,当然是处理消息对象)引用了消息格式化方法将消息格式化方法和洋葱圈处理器封装成一个工厂函数handleRequest。这个工厂函数只负责接收消息和返回结果。上面说的use方法其实就是洋葱圈模型的核心,也就是字面量注册中间件的方法。useuse(fn){if(typeoffn!=='function')thrownewTypeError('middlewaremustbeafunction!');if(isGeneratorFunction(fn)){deprecate('对生成器的支持将在v3中删除。'+'请参阅文档以获取有关如何转换旧中间件的示例'+'https://github.com/koajs/koa/blob/master/docs/migration.md');fn=转换(fn);}debug('use%s',fn._name||fn.name||'-');这个.middleware.push(fn);returnthis;}使用必须接受一个函数(接受两个参数,一个是ctx上下文,一个是next函数),如果这个函数是生成器(*generator),(KOA1中的中间件只接受iterator方法,在KOA2中都是用asyncawait实现的),那么就需要进行优雅的降级处理,通过koa-convert函数进行转换。(这部分我还没看==)最后还是很简单的。将此方法推送到KOA实例的中间件队列中。看到这里,申请文件的内容就结束了。看来KOA的源码还是比较少的。是的,但是因为有中间件的存在,扩展性变得非常强,这应该也是现在Koa如此流行的原因。PS:koa-composekoa-compose的代码量非常少,不到50行(49行)。但设计非常微妙。functioncompose(middleware){if(!Array.isArray(middleware))thrownewTypeError('Middlewarestackmustbeanarray!')for(constfnofmiddleware){if(typeoffn!=='function')thrownewTypeError('中间件必须由函数组成!')}returnfunction(context,next){//最后调用的中间件下标letindex=-1returndispatch(0)functiondispatch(i){//i<=index表示中间件被重复调用,抛出错误if(i<=index)returnPromise.reject(newError('next()calledmultipletimes'))index=iletfn=middleware[i]//fn取对应层的中间件,如果传入next函数,则next函数优先级高if(i===middleware.length)fn=next//这里fn指向第i层中间件函数//如果没有了,那就直接resolveif(!fn)returnPromise.resolve()try{//真正的执行步骤在这里,执行fn方法,同时执行下一层中间件的功能还传递了returnPromise.resolve(fn(context,dispatch.bind(null,i+1)));}catch(err){returnPromise.reject(err)}}}}这种语法是每个中间件执行等待nex的概括当t()语句执行时,会调用下一层的中间件。也可以把代码理解为//前半部分处理逻辑awaitnext()//后半部分处理逻辑/*=================等价于=================*///前半部分处理逻辑awaitnewPromise([下一层中间件的逻辑])//后半部分处理逻辑和这样的插入逻辑递归地发生在下一层中间件的逻辑中。洋葱圈的执行逻辑其实是存放在数组koa.middleware中的,只执行到第n层洋葱圈才会使用下标n+1去掉下一层的处理逻辑,一个Promise就会被生成并插入到上层洋葱圈的函数体中,形成一个不断重叠的函数范围。结尾