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的路由.
源码下载
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。