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

koa2源码及实现分析

时间:2023-04-03 15:35:44 Node.js

koa2源码分析及实现源码分析koa2源码地址:https://github.com/koajs/koa目录公司内部系统,目录如下:在package.json中的依赖库中找到版本号如下:不难看出koa源码是由application.js、context.js、request组成的。js和response.js。application.jsapplication.js是koa的入口文件,它继承事件创建类实例,并导出实例,赋予框架事件监听和事件触发的能力。应用也暴露了一些常用的API,比如listen、toJSON、inspect、use等。listen的实现原理其实就是对http.createServer的封装。重点是这个函数中传入的回调,包括中间件的合并,context的处理,res的特殊处理。使用是收集中间件,将多个中间件放入缓存队列,然后通过koa-compose插件递归组合调用这一系列的中间件。context.js这部分是koa的应用上下文ctx。其实就是简单的对象曝光。里面的关键点就是delegate,也就是代理。这是为了方便开发人员而设计的。比如我们想访问ctx.repsponse.status但是通过delegate我们可以直接访问ctx.status来访问。request.js和response.js这两部分是对原来的res和req的一些操作,使用es6的get和set的一些语法来读取headers、body、url等。简易版koa2的实现本文实现的简易版koa2源码地址:https://github.com/TheWalkingFat/simple_koa2根据网上资料,实现简易版koa2框架,需要理解和实现四大模块,分别是:封装nodehttpserver,创建koa类构造函数,构造request,response,context对象中间件机制,实现洋葱皮模型错误捕获和错误处理封装nodehttpserver,创建Koa类构造函数从koa2源码可以看出,实现koa的server应用和端口监听,其实是基于node的native代码封装lethttp=require('http');letserver=http。createServer((req,res)=>{res.writeHead(200);res.end('成功');});server.listen(3001,()=>{console.log('监听3001');});那么我们需要将上面的node.js原生代码封装成koa方式:consthttp=require('http');constKoa=require('koa');constapp=newKoa();app.listen(3001);实现koa的第一步就是将上面这个过程进行封装,所以我们需要创建application.js来实现一个Application类的构造函数lethttp=require('http');classApplication{constructor(){this.callbackFunc;}listen(...args){让server=http.createServer(this.callback());服务器.listen(...args);}使用(fn){this.callbackFunc=fn;}打回来(){return(req,res)=>{this.callbackFunc(req,res);};}}module.exports=应用程序;然后创建app.js,导入application.js,运行服务器实例开始监听代码:letKoa=require('./application');letapp=newKoa();app.use((req,res)=>{res.writeHead(200);res.end('成功');});app.listen(3001,()=>{console.log('监听3001');});现在在命令行中输入nodeapp.js启动服务,然后在浏览器中访问localhost:3001,可以看到浏览器输出成功,至此第一步就完成了。我们简单的封装了http服务器,创建了一个可以生成koa实例的class类。该类还实现了app.use来注册中间件和注册回调函数。app.listen用于开启服务器实例,传入callback回调函数构造请求、响应和上下文对象并查看源代码。request.js、response.js、context.js这三个文件分别是request、response、context三个模块的代码文件。context:context就是我们平时写的koa代码ctx,相当于一个全局的koa实例上下文this,连接了request和response两个功能模块,暴露给koa实例和中间件等回调函数的参数,起到承上启下的作用。request和response:分别对node原生的request和response进行功能封装,使用getter和setter属性,基于node的对象req/res对象封装koa的request/response对象来简单实现request.js:module.exports={getheader(){返回this.req.headers;},设置标头(val){this.req.headers=val;},getheaders(){返回this.req.headers;},设置标头(val){this.req.headers=val;}}在getters和setters的基础上,header、url、origin、path等方法也封装在request.js中,这些方法都是在原始请求上用getters和setters封装的,这里就不一一进行了一个已经实现,有兴趣的同学可以自行查看源码。response.js:和request原理一样,也是基于getter和setter封装了原来的response。然后我们将两个常用的ctx.body和ctx.status语句作为示例简单描述module.exports={getbody(){returnthis._body;},设置正文(数据){this._body=data;},getstatus(){返回this.res.statusCode;},设置状态(statusCode){this.res.statusCode=statusCode;}}这里需要注意一点,对于statusCode的读写操作,我们直接根据原生响应对象的statusCode进行操作,而对于body,我们存储了一个私有变量,而不是直接对属性进行操作的原因原始对象是我们在写koa代码的时候,body会被多次读取和修改,所以返回浏览器信息的实际操作是在application.js中进行封装和操作的。context.js:现在我们已经实现了request.js和response.js,获取了request和response对象及其封装方法,接下来开始实现context.js。context的作用是将request和response对象挂载到ctx之上,koa实例和代码可以方便的使用request和response对象中的方法。让proto={};functiondelegateSet(property,name){proto.__defineSetter__(name,function(val){this[property][name]=val;});}functiondelegateGet(property,name){proto.__defineGetter__(name,function(){returnthis[property][name];});}letrequestSet=[];letrequestGet=['query'];letresponseSet=['body','status'];letresponseGet=responseSet;requestSet.forEach(ele=>{delegateSet('request',ele);});requestGet.forEach(ele=>{delegateGet('request',ele);});responseSet.forEach(ele=>{delegateSet('response',ele);});responseGet.forEach(ele=>{delegateGet('response',ele);});module.exports=proto;context.js文件主要是对常用的request和response方法进行挂载和代理,context.request.query直接通过context.query代理,context.response.body和context.response.status通过context代理.body和context.status。而context.request,context.response会挂载到application.js中。至此,我们已经获取了request、response、context这三个模块对象。接下来就是将request和response的所有方法挂载到context中,让context实现承前启后的作用,修改application.js文件,添加如下代码:lethttp=require('http');letcontext=require('./context');letrequest=require('./request');letresponse=require('./response');createContext(req,res){让ctx=Object.create(this.context);ctx.request=Object.create(this.request);ctx.response=Object.create(this.response);ctx.req=ctx.请求。请求=请求;ctx.res=ctx.response.res=res;returnctx;}createContext这个方法是关键,它通过Object.create创建ctx,并将request和response挂载到ctx上,原来的req和res挂载到ctx的子属性上。如果回头看context/request/response.js文件,就可以知道当时使用的this.res或者this.response是从哪里来的。原来是在对应的实例上挂载了这个createContext方法。运行时上下文ctx构建完成后,我们app.use回调函数的参数都是以ctx为基础的。中间件机制和洋葱皮模型的实现我从网上找了两张图来分析koa的洋葱模型:koa的中间件机制是一个洋葱皮模型。该层开始执行,遇到next后进入队列中的下一个中间件。所有的中间件执行完后,开始返回帧,执行队列中前一个中间件未执行的代码部分。这是洋葱皮模型,koa中间件机制koa的洋葱皮模型在koa1中使用generator+co.js实现,而koa2使用async/await+Promise修改application.js文件,添加如下代码:use(middleware){this.middlewares.push(middleware);}compose(){//将中间件合并到接收ctx对象的函数中returnasyncctx=>{functioncreateNext(middleware,oldNext){returnasync()=>{await中间件(ctx,oldNext);}}让len=this.middlewares.length;让next=async()=>{returnPromise.resolve();};for(leti=len-1;i>=0;i--){让currentMiddleware=this.middlewares[i];next=createNext(currentMiddleware,next);等待下一个();};}callback(){return(req,res)=>{让ctx=this.createContext(req,res);让响应=()=>this.responseBody(ctx);让onerror=(err)=>this.onerror(err,ctx);让fn=this.compose();返回fn(ctx).then(响应)。赶上(错误);};}koa通过use函数将所有中间件推送到一个内部数组队列this.middlewares中,洋葱皮模型让所有中间件顺序执行。当到达next()时,控制权将传递给下一个中间件。接下来中间件的下一个参数,洋葱皮模型最关键的代码是compose。compose中createNext函数的作用是将next作为参数传递给下一个中间件,并将contextctx绑定到当前中间件。当中间件执行完毕,调用next(),其实就是执行下一个中间件。通过链式逆递归模型的实现,i从最大数开始循环,从最后一个开始封装中间件。每次都将自己的执行函数封装到next中,作为上一个中间件的next参数。这样,在循环到第一个中间件时,只需要执行一次next(),就可以递归调用一条链中的所有中间件。这就是koa剥洋葱的核心代码机制。错误捕获和错误处理错误分为两类:中间件执行错误和框架级错误。向application.js添加一些代码:callback(){return(req,res)=>{letctx=this.createContext(req,res);让respond=()=>this.responseBody(ctx);让onerror=(err)=>this.onerror(err,ctx);让fn=this.compose();返回fn(ctx).then(respond).catch(onerror);};}onerror(err,ctx){if(err.code==='ENOENT'){ctx.status=404;}else{ctx.status=500;}让msg=err.message||'内部错误';ctx.res.end(msg);this.emit('error',err);}第一种,中间件执行错误:我们之前说过,koa2只使用了async/await+promise来实现洋葱模型,所以.catch自然可以捕捉到中间件执行错误。在回调函数中,我们添加了这行代码returnfn(ctx).then(respond).catch(onerror);第二种是框架层面的错误:应用中的类继承了EventEmitter,在触发错误时主动调用emit('error'),那么外层引用只需要监听错误即可实现。onerror(err,ctx){if(err.code==='ENOENT'){ctx.status=404;}else{ctx.status=500;}让msg=err.message||'内部错误';ctx.res.end(msg);this.emit('error',err);}app.on('error',err=>{console.log('error:',err);});至此,我们已经实现了一个简单版本的koa2。koa2的原理主要由四部分组成。了解了这四个部分之后,再看koa2的源码就会容易很多。本文实现的简单版koa2源码地址:https://github.com/TheWalkingFat/simple_koa2