NodeJS仿WebApi路由示例
用过WebApi或Asp.netMVC的都知道微软的路由设计得非常好,十分方便,也十分灵活。虽然个人看来是有的太灵活了,team内的不同开发很容易使用不同的路由方式而显得有点混乱。不过这不是重点,我在做Node项目的时候就觉得不停的用use(...)来指定路由路径很烦人,所以用Typescript写了这个基于Koa和Koa-router的路由插件,可以简单实现一些类似WebApi的路由功能。
目标是和WebApi一样:
1.加入的controller会自动加入路由。
2.也可以通过path()手动指定路由。
3.可以定义httpmethod,如GET或POST等。
4.Api的参数可以指定url里的queryparam、pathparam以及body等。
包已经上传到npm中,npminstallwebapi-router安装,可以先看看效果:
第一步,先设置controllers的目录和url的固定前缀
所有的controller都在这目录下,这样会根据物理路径自动算出路由。url的固定前缀就是host和路由之间的,比如localhost/api/v2/user/name,api/v2就是这个固定前缀。
import{WebApiRouter}from'webapi-router'; app.use(newWebApiRouter().router('sample/controllers','api'));
第二步是controller都继承自BaseController
exportclassTestControllerextendsBaseController { }
第三步给controller的方法加上装饰器
@POST('/user/:name') postWithPathParam(@PathParam('name')name:string,@QueryParam('id')id:string,@BodyParambody:any){ console.info(`TestController-postwithname:${name},body:${JSON.stringify(body)}`); return'ok'; }
@POST里的参数是可选的,空的话会用这个controller的物理路径做为路由地址。
:name是路径里的变量,比如/user/brook,:name就是brook,可以在方法的参数里用@PathParam得到
@QueryParam可以得到url里?后的参数
@BodyParam可以得到Post上来的body
是不是有点WebApi的意思了。
现在具体看看是怎么实现的
实现过程其实很简单,从上面的目标入手,首先得到controllers的物理路径,然后还要得到被装饰器装饰的方法以及它的参数。
装饰器的目的在于要得到是Get还是Post等,还有就是指定的Path,最后就是把noderequest里的数据赋值给方法的参数。
核心代码:
得到物理路径
initRouterForControllers(){ //找出指定目录下的所有继承自BaseController的.js文件 letfiles=FileUtil.getFiles(this.controllerFolder); files.forEach(file=>{ letexportClass=require(file).default; if(this.isAvalidController(exportClass)){ this.setRouterForClass(exportClass,file); } }); }
从物理路径转成路由
privatebuildControllerRouter(file:string){ letrelativeFile=Path.relative(Path.join(FileUtil.getApiDir(),this.controllerFolder),file); letcontrollerPath='/'+relativeFile.replace(/\\/g,'/').replace('.js','').toLowerCase(); if(controllerPath.endsWith('controller')) controllerPath=controllerPath.substring(0,controllerPath.length-10); returncontrollerPath; }
装饰器的实现
装饰器需要引入reflect-metadata库
先看看方法的装饰器,@GET,@POST之类的,实现方法是给装饰的方法加一个属性Router,Router是个Symbol,确保唯一。然后分析装饰的功能存到这个属性中,比如Method,Path等。
exportfunctionGET(path?:string){ return(target:BaseController,name:string)=>setMethodDecorator(target,name,'GET',path); } functionsetMethodDecorator(target:BaseController,name:string,method:string,path?:string){ target[Router]=target[Router]||{}; target[Router][name]=target[Router][name]||{}; target[Router][name].method=method; target[Router][name].path=path; }
另外还有参数装饰器,用来给参数赋上request里的值,如body,param等。
exportfunctionBodyParam(target:BaseController,name:string,index:number){ setParamDecorator(target,name,index,{name:"",type:ParamType.Body}); } functionsetParamDecorator(target:BaseController,name:string,index:number,value:{name:string,type:ParamType}){ letparamTypes=Reflect.getMetadata("design:paramtypes",target,name); target[Router]=target[Router]||{}; target[Router][name]=target[Router][name]||{}; target[Router][name].params=target[Router][name].params||[]; target[Router][name].params[index]={type:paramTypes[index],name:value.name,paramType:value.type}; }
这样装饰的数据就存到对象的Router属性上,后面构建路由时就可以用了。
绑定路由到Koa-router上
上面从物理路径得到了路由,但是是以装饰里的参数路径优先,所以先看看刚在存在原型里的Router属性里有没有Path,有的话就用这个作为路由,没有Path就用物理路由。
privatesetRouterForClass(exportClass:any,file:string){ letcontrollerRouterPath=this.buildControllerRouter(file); letcontroller=newexportClass(); for(letfuncNameinexportClass.prototype[Router]){ letmethod=exportClass.prototype[Router][funcName].method.toLowerCase(); letpath=exportClass.prototype[Router][funcName].path; this.setRouterForFunction(method,controller,funcName,path?`/${this.urlPrefix}${path}`:`/${this.urlPrefix}${controllerRouterPath}/${funcName}`); } }
给controller里的方法参数赋上值并绑定路由到KoaRouter
privatesetRouterForFunction(method:string,controller:any,funcName:string,routerPath:string){ this.koaRouter[method](routerPath,async(ctx,next)=>{awaitthis.execApi(ctx,next,controller,funcName)}); } privateasyncexecApi(ctx:Koa.Context,next:Function,controller:any,funcName:string):Promise<void>{//这里就是执行controller的api方法了 try { ctx.body=awaitcontroller[funcName](...this.buildFuncParams(ctx,controller,controller[funcName])); } catch(err) { console.error(err); next(); } } privatebuildFuncParams(ctx:any,controller:any,func:Function){//把参数具体的值收集起来 letparamsInfo=controller[Router][func.name].params; letparams=[]; if(paramsInfo) { for(leti=0;i<paramsInfo.length;i++){ if(paramsInfo[i]){ params.push(paramsInfo[i].type(this.getParam(ctx,paramsInfo[i].paramType,paramsInfo[i].name))); }else{ params.push(ctx); } } } returnparams; } privategetParam(ctx:any,paramType:ParamType,name:string){//从ctx里把需要的参数拿出来 switch(paramType){ caseParamType.Query: returnctx.query[name]; caseParamType.Path: returnctx.params[name]; caseParamType.Body: returnctx.request.body; default: console.error('doesnotsupportthisparamtype'); } }
这样就完成了简单版的类似WebApi的路由.
源码下载
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。