01。Koa简介——基于Node.js的下一代Web开发框架。开发世界中更小、更具表现力、更强大的基石。与其对应的Express相比,Koa更小也更强大。本文将带你从零开始实现Koa的源码,从根源上解决你对Koa的困惑。本文中的Koa版本为2.7.0,源码可能会根据版本不同而有所变化。02.源码目录介绍通过源码目录可以知道Koa源码目录的截图。Koa主要分为4个部分,分别是:application:Koa的主要模块,对应app应用对象context:对应ctx对象request:对应Koa中的request对象response:response对象对应的四个文件Koa中是Koa的全部内容,application是核心文件。我们将从这个文件开始,一步步实现Koa框架03,实现一个基本的服务端代码目录my-applicationconst{createServer}=require('http');module.exports=classApplication{constructor(){//初始化middleware数组,所有的中间件函数都会添加到当前数组中this.middleware=[];}//使用中间件方法use(fn){//将所有中间件函数添加到中间件数组中this.middleware.push(fn);}//监听端口号methodlisten(...args){//使用nodejs的http模块监听端口号constserver=createServer((req,res)=>{/*处理请求的回调函数,in所有的中间件函数都在这里执行req是节点的原生请求对象res是节点的原生响应对象*/this.middleware.forEach((fn)=>fn(req,res));})server.listen(...参数);}}index.js//引入自定义模块constMyKoa=require('./js/my-application');//创建实例对象constapp=newMyKoa();//使用中间件app.use((req,res)=>{console.log('中间件函数执行~~~111');})app.use((req,res)=>{console.log('中间件函数执行~~~222');res.end('hellomyKoa');})//监听端口号app.listen(3000,err=>{if(!err)console.log('服务器启动成功');elseconsole.log(err);})运行入口文件索引。js,通过浏览器输入网址访问http://localhost:3000/,就可以看到结果了~~厉害了!构建了一个最简单的服务器模型当然,我们的极简服务器还存在很多问题。接下来,让我们一一解决。04.实现中间件函数的next方法提取createServer的回调函数封装成回调方法(可复用)//监听端口号methodlisten(...args){//使用nodejs的http模块来监听端口号constserver=createServer(this.callback());server.listen(...args);}callback(){consthandleRequest=(req,res)=>{this.middleware.forEach((fn)=>fn(req,res));}returnhandleRequest;}封装compose函数实现next方法//负责执行中间件函数的函数functioncompose(middleware){//compose方法的返回值是一个函数,这个函数的返回值是一个promise对象//当前函数是dispatchreturn(req,res)=>{//默认调用一次,为了执行第一个中间件函数returndispatch(0);functiondispatch(i){//提取中间件数组的函数fnletfn=middleware[i];//如果最后一个中间件也调用了next方法,直接返回一个状态成功的promise对象if(!fn)returnPromise.resolve();/*dispatch.bind(null,i+1))作为中间件函数调用的第三个参数,其实就是对应的next。例如:ifi=0,thendispatch.bind(null,1))-->即如果调用next方法,则实际执行dispatch(1)-->它使用递归重新进入取out下一个中间件函数然后执行fn(req,res,dispatch.bind(null,i+1))-->这就是为什么中间件函数可以有三个参数,我们在调用时传入*/returnPromise.resolve(fn(req,res,dispatch.bind(null,i+1)));}}}使用compose函数回调(){//执行compose方法返回一个函数constfn=compose(this.middleware);consthandleRequest=(req,res)=>{//调用函数,返回值是一个promise对象//then方法被触发,表示所有中间件函数都被调用完成fn(req,res).then(()=>{//这是所有处理函数的最后阶段,可以Responsesareallowed~});}returnhandleRequest;}修改入口文件index.js代码//引入自定义模块constMyKoa=require('./js/my-application');//创建实例对象constapp=newMyKoa();//使用中间件app.use((req,res,next)=>{console.log('执行的中间件函数~~~111');//调用next方法,也就是调用栈next中间件函数next();})app.use((req,res,next)=>{console.log('中间件函数执行~~~222');res.end('hellomyKoa');//最后一个next方法做不调用下一个中间件函数,直接返回Promise.resolve()next();})//监听端口号app.listen(3000,err=>{if(!err)console.log('服务器启动成功');elseconsole.log(err);})至此我们已经实现了next方法,其核心是compose函数。极简的代码实现了功能,不可思议!05.处理响应应该定义返回响应函数respondfunctionrespond(req,res){//获取设置的主体数据letbody=res.body;if(typeofbody==='object'){//如果是对象,则转成json数据返回body=JSON.stringify(body);res.end(正文);}else{//默认其他数据直接返回给res.end(body);}}在callback{constfn=compose(this.middleware)中调用callback();consthandleRequest=(req,res)=>{//当所有中间件函数执行完毕后,会触发then方法执行respond方法返回responseconsthandleResponse=()=>respond(req,res);fn(req,res).then(handleResponse);}returnhandleRequest;}修改入口文件index.js代码//引入自定义模块constMyKoa=require('./js/my-application');//创建实例Objectconstapp=newMyKoa();//使用中间件app.use((req,res,next)=>{console.log('中间件函数执行~~~111');next();})app.use((req,res,next)=>{console.log('中间件函数执行完毕~~~222');//设置响应内容,框架负责返回响应~res.body='hellomyKoa';})//监听端口号app.listen(3000,err=>{if(!err)console.log('服务器启动成功');elseconsole.log(err);})此时我们可以按照不同的响应内容~当然比较简单,可以继续扩展~06.定义请求模块//该模块需要npm下载constparse=require('parseurl');constqs=require('querystring');module.exports={/***获取请求头信息*/getheaders(){返回this.req.headers;},/***设置请求头信息*/setheaders(val){this.req.headers=val;},/***获取查询字符串*/getquery(){//解析查询字符串参数-->key1=value1&key2=value2constquerystring=parse(this.req).query;//将其作为对象解析return-->{key1:value1,key2:value2}returnqs.parse(querystring);}}07。定义响应模块module.exports={/***设置响应头信息*/set(key,value){this.res.setHeader(key,value);},/***获取响应状态码*/getstatus(){returnthis.res.statusCode;},/***设置响应状态码*/setstatus(code){this.res.statusCode=code;},/***获取响应主体信息*/getbody(){returnthis._body;},/***设置响应体信息*/setbody(val){//设置响应体内容this._body=val;//设置响应状态码this.status=200;//jsonif(typeofval==='object'){this.set('Content-Type','application/json');}},}08.定义Context模块//这个模块需要从npm下载constdelegate=require('delegates');constproto=module.exports={};//将响应对象上的属性/方法克隆到protodelegate(proto,'response').method('set')//克隆普通方法。access('status')//Clonewithgetandthemethodofsetdescriptor.access('body')//clonerequest对象上的属性/方法到protodelegate(proto,'request').access('query').getter('headers')//Clonemethod09withgetdescriptor,demystifydelegatesmodulemodule.exports=Delegator;/***初始化一个委托。*/functionDelegator(proto,target){//this必须指向Delegator的实例对象if(!(thisinstanceofDelegator))returnnewDelegator(proto,target);//要克隆的对象this.proto=proto;//要克隆的目标对象this.target=target;//所有常用方法的数组this.methods=[];//包含获取描述符的所有方法的数组this.getters=[];//带有集合描述符的所有方法的数组this.setters=[];}/***克隆普通方法*/Delegator.prototype.method=function(name){//要克隆的对象varproto=this.proto;//要克隆的目标对象vartarget=this.target;//方法添加到方法数组中间this.methods.push(name);//将克隆的属性添加到protoproto[name]=function(){/*this指向proto,即ctx例如:ctx.response.set.apply(ctx.response,arguments)arguments对应于实参列表,这与应用方法完全相同。执行ctx.set('key','value')实际上等同于执行response.set('key','value')*/returnthis[target][name].apply(this[target],arguments);};//方便的链式调用returnthis;};/***使用get和set描述符克隆方法。*/委托人。prototype.access=function(name){returnthis.getter(name).setter(name);};/***使用get描述符克隆方法。*/Delegator.prototype.getter=function(name){varproto=this.proto;vartarget=this.target;this.getters.push(名字);//方法可以为现有对象设置获取描述符属性proto.__defineGetter__(name,function(){returnthis[target][name];});returnthis;};/***带有设置描述符的Clone方法。*/Delegator.prototype.setter=function(name){varproto=this.proto;vartarget=this.target;this.setters.push(名字);//方法可以为现有对象设置描述符属性proto.__defineSetter__(name,function(val){returnthis[target][name]=val;});返回此;};10。使用ctx而不是req和res来修改my-applicationconst{createServer}=require('http');constcontext=require('./my-context');constrequest=require('./my-request');constresponse=require('./my-response');module.exports=classApplication{构造函数(){this.middleware=[];//Object.create(target)以目标对象为原型创建新对象。新对象原型具有目标对象的属性和方法。this.context=Object.create(context);this.request=Object.create(请求);this.response=Object.create(response);}使用(fn){this.middleware.push(fn);}listen(...args){//使用nodejs的http模块监听端口号constserver=createServer(this.callback());服务器.listen(...args);}callback(){constfn=compose(this.middleware);consthandleRequest=(req,res)=>{//创建上下文constctx=this.createContext(req,res);consthandleResponse=()=>respond(ctx);fn(ctx).然后(ha空闲响应);}返回句柄请求;}//创建context对象的方法createContext(req,res){/*所有的req/res都是nodenative对象,所有的request/response都是自定义对象,这是为了实现相互挂载引用,这样其他对象的方法就可以在任何对象上获得*/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=请求;返回上下文;}}//使用req和res而不是ctxfunctioncompose(middleware){return(ctx)=>{returndispatch(0);functiondispatch(i){letfn=middleware[i];如果(!fn)返回Promise.resolve();返回Promise.resolve(fn(ctx,dispatch.bind(null,i+1)));}}}函数响应nd(ctx){让body=ctx.body;constres=ctx.res;if(typeofbody==='object'){body=JSON.stringify(body);res.end(正文);}else{res.end(正文);}}修改入口文件index.js代码//导入自定义模块constMyKoa=require('./js/my-application');//创建实例对象constapp=newMyKoa();//使用中间件app.use((ctx,next)=>{console.log('中间件函数执行~~~111');next();})app.use((ctx,next)=>{console.log('中间件functionexecuted~~~222');//获取请求头参数console.log(ctx.headers);//获取查询字符串参数console.log(ctx.query);//设置响应头信息ctx.set('content-type','text/html;charset=utf-8');//设置响应内容,框架负责返回响应~ctx.body='
