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

koa-router源码分析

时间:2023-04-03 17:43:30 Node.js

代码结构执行流程上面两张图主要画出了koa-router的整体代码结构和大概的执行流程,但是画得不够具体。然后说一下koa-router中几个关键代码的解释。阅读代码首先要找到入口文件,几乎所有node模块的入口文件都会在package.json文件中的main属性中指定。koa-router的入口文件是lib/router.js。第三方模块首先说几个第三方节点模块,因为在后面的代码讲解中会用到。具体实现不用看,只要知道它们的作用即可:koa-compose:给它提供一个中间件数组,返回一个execute函数,依次执行所有中间件。methods:node中支持的http动词是http.METHODS,可以在终端输出中查看。path-to-regexp:将路径字符串转换为强大的正则表达式,还可以输出路径参数。Router&LayerRouter和Layer是两个构造函数。它们分别在router.js和layer.js中。koa-router的所有代码也在这两个文件中。可以知道它的代码量并不是很大。Router:创建一个管理整个路由模块的实例functionRouter(opts){if(!(thisinstanceofRouter)){returnnewRouter(opts);}this.opts=opts||{};this.methods=this.opts.methods||['HEAD','OPTIONS','GET','PUT','PATCH','POST','DELETE'];this.params={};this.stack=[];};首先是if(!(thisinstanceofRouter)){returnnewRouter(opts);}这是一个常用的去new的方式,所以我们可以引入koa-router:constrouter=require('koa-router')()insteadof:constrouter=newrequire('koa-router')()//这个没问题this.methods:用在后面要讲的allowedMethods方法中,目的是响应optionsrequests的错误处理。this.params:全局路由参数处理的中间件组成的对象。this.stack:其实就是一个路由(Layer)实例的数组。每次处理请求时,都需要循环这个数组来寻找匹配的路由。Layer:创建每个路由实例functionLayer(path,methods,middleware,opts){...this.stack=Array.isArray(middleware)?中间件:[中间件];//处理methods.forEach(function(method){varl=this.methods.push(method.toUpperCase());if(this.methods[l-1]==='GET'){//如果是get请求,支持head请求this.methods.unshift('HEAD');}},this);//确保路由的每个中间件都是函数this.stack.forEach(function(fn){vartype=(typeoffn);if(type!=='function'){thrownewError(methods.toString()+"`"+(this.opts.name||path)+"`:`middleware`"+"必须是函数,不能是`"+type+"`");}},this);this.path=路径;//由path-to-rege模块生成的路径的正则表达式this.regexp=pathToRegExp(path,this.paramNames,this.opts);...};这里的this.stack和Router中的不一样,这里是路由所有中间件的数组。(一个路由可以有多个中间件)router.register()功能:注册路由从上一篇代码结构图中可以看出,Router的几个实例方法直接或简单调用了register方法。可以看出,应该是一个比较核心的功能,代码不长,我们一行一行来看:Router.prototype.register=function(path,methods,middleware,opts){opts=opts||{};变种路由器=这个;//所有路由varstack=this.stack;//表示路由的路径支持数组//如果是数组,需要递归调用register来注册路由//因为一条路径对应一条路由if(Array.isArray(path)){path.forEach(function(p){router.register.call(router,p,methods,middleware,opts);});归还这个;}//创建路由,路由是Layer的一个实例//mthods是路由处理的http方法//最后一个参数对象最终传递给Layer模块中的path-to-regexp模块接口调用。route=newLayer(path,methods,middleware,{end:opts.end===false?opts.end:true,name:opts.name,sensitive:opts.sensitive||this.opts.sensitive||false,严格:opts.strict||this.opts.strict||false,前缀:opts.prefix||this.opts.prefix||“”,ignoreCaptures:opts.ignoreCaptures});//处理路由前缀if(this.opts.prefix){route.setPrefix(this.opts.prefix);}//为每个路由添加全局路由参数Object.keys(this.params).forEach(function(param){route.param(param,this.params[param]);},this);//将新创建的路由添加到路由数组中stack.push(route);returnroute;};router.verb()verb=>get|put|post|patch|delete功能:注册路由这是koa-router提供的路由,直接注册对应的http方法,但是最终注册方法会是调用如:router.get('/user',function(ctx,next){...})和下面使用的register方法是等价的:router.register('/user',['get'],[function(ctx,next){...}])可以看到直接用router.verb代码注册路由方便多了:你会发现router里面没有Router.prototype.get代码。js代码,因为它也依赖于上面提到的methods模块来实现。//这里的方法就是上面methods模块提供的数组methods.forEach(function(method){Router.prototype[method]=function(name,path,middleware){varmiddleware;//这段代码做了两件事://1.name参数是可选的,所以做一些参数替换处理//2.将所有路由中间件合并到一个数组中if(typeofpath==='string'||pathinstanceofRegExp){middleware=Array.prototype.slice.call(arguments,2);}else{middleware=Array.prototype.slice.call(arguments,1);path=name;name=null;}//调用注册方法this.register(path,[method],中间件,{name:name});返回这个;};});router.routes()功能:开始路由这是koa中配置路由的重要一步:varrouter=require('koa-router')();...app.use(router.routes())这样,koa-router启动了,那么我们肯定很好奇这个routes函数是做什么的,但是我们可以确定router.routes()返回的是一个中间件函数。函数体有点长,我们简化一下,看一下整体轮廓:Router.prototype.routes=Router.prototype.middleware=function(){varrouter=this;vardispatch=functiondispatch(ctx,next){...}dispatch.router=this;返回调度;};这里形成一个闭包,在routes函数内部返回一个dispatch函数作为中间件。接下来看dispatch函数的实现:vardispatch=functiondispatch(ctx,next){varpath=router.opts.routerPath||ctx.routerPath||ctx.路径;//router.match内部会遍历所有的路由(this.stach),//根据路径和请求方式找到对应的路由//返回的匹配对象为:/*varmatched={path:[],//保存路由数组pathAndMethod:[],//保存路径和方法都匹配的路由数组route:false//是否有对应的路由};*/varmatched=router.match(path,ctx.method);变种层链,层,我;if(ctx.matched){ctx.matched.push.apply(ctx.matched,matched.path);}else{ctx.matched=matched.path;}//如果没有对应的路由,直接跳转到下一个中??间件if(!matched.route)returnnext();//找到正确的路由路径varmostSpecificPath=matched.pathAndMethod[matched.pathAndMethod.length-1].path;ctx._matchedRoute=mostSpecificPath;//使用reduce方法转换所有路由中间件形成一个链layerChain=matched.pathAndMethod.reduce(function(memo,layer){//在每个路由的中间件执行之前,根据不同的参数设置ctx.captures和ctx.params//这就是为什么我们可以直接在中间件函数中使用ctx.params来读取路由参数信息memo.push(function(ctx,next){//返回路由参数的keyctx.captures=layer.captures(path,ctx.captures);//返回由参数的key和对应的value组成的对象ctx.params=layer.params(path,ctx.captures,ctx.params);//执行下一个中间件return下一个();});//将上面的附加中间件与现有的路由中间件合并//所以最终的layerChain将是一个中间件数组returnmemo.concat(layer.stack);},[]);//最后调用上面提到的compose模块提供的方法,returnlayerChain(arrayofmiddleware)//依次执行所有的中间件执行函数,执行returncompose(layerChain)(ctx,next);};router.allowMethods()功能:请求出错时的处理逻辑,也是koa中配置路由的一个步骤:varrouter=require('koa-router')();...app.use(router.routes())app.use(router.allowMethods())可以看出,这个方法在闭包中也返回了一个中间件函数。让我们稍微简化一下代码:Router.prototype.allowedMethods=function(options){options=options||{};varimplemented=this.methods;returnfunctionallowedMethods(ctx,next){returnnext().then(function(){varallowed={};if(!ctx.status||ctx.status===404){...if(!~implemented.indexOf(ctx.method)){if(options.throw){...}else{ctx.status=501;ctx.set('Allow',allowedArr);}}elseif(allowedArr.length){if(ctx.method==='OPTIONS'){ctx.status=204;ctx.set('Allow',allowedArr);}elseif(!allowed[ctx.method]){if(options.throw){...}else{ctx.status=405;ctx.set('Allow',allowedArr);}}}}});};};眼尖的同学可能会看到一些http代码:404,501,204,405那么这个函数其实就是当所有的中间件函数都执行完,请求失败的时候进行相应的处理:如果请求的方法是koa-r如果外部不支持且没有设置throw选项,则返回501(未实现)。如果是options请求,会返回204(无内容)。如果请求的方法支持但没有设置throw选项,则返回405(不允许该方法)。总结的比较粗分析了这么多,大概可以知道koa-router的工作原理了。作者能力有限,如有错误请指出。