NodeJS仿WebApi路由示例


Posted in NodeJs onFebruary 28, 2017

用过WebApi或Asp.net MVC的都知道微软的路由设计得非常好,十分方便,也十分灵活。虽然个人看来是有的太灵活了,team内的不同开发很容易使用不同的路由方式而显得有点混乱。 不过这不是重点,我在做Node项目的时候就觉得不停的用use(...)来指定路由路径很烦人,所以用Typescript写了这个基于KoaKoa-router的路由插件,可以简单实现一些类似WebApi的路由功能。

目标是和WebApi一样:

1.加入的controller会自动加入路由。

2.也可以通过path()手动指定路由。

3.可以定义http method, 如GETPOST等。

4.Api的参数可以指定url里的query param、path param以及body等。

包已经上传到npm中,npm install webapi-router 安装,可以先看看效果:

第一步,先设置controllers的目录和url的固定前缀

所有的controller都在这目录下,这样会根据物理路径自动算出路由。 url的固定前缀就是host和路由之间的,比如localhost/api/v2/user/nameapi/v2就是这个固定前缀。

import { WebApiRouter } from 'webapi-router';

app.use(new WebApiRouter().router('sample/controllers', 'api'));

第二步是controller都继承自BaseController

export class TestController extends BaseController
{

}

第三步给controller的方法加上装饰器

@POST('/user/:name')
postWithPathParam(@PathParam('name') name: string, @QueryParam('id') id: string, @BodyParam body: any) {
  console.info(`TestController - post with name: ${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,最后就是把node request里的数据赋值给方法的参数。

核心代码:

得到物理路径

initRouterForControllers() {
  //找出指定目录下的所有继承自BaseController的.js文件
  let files = FileUtil.getFiles(this.controllerFolder);

  files.forEach(file => {
    let exportClass = require(file).default;

    if(this.isAvalidController(exportClass)){
      this.setRouterForClass(exportClass, file);
    }
  });
}

从物理路径转成路由

private buildControllerRouter(file: string){

  let relativeFile = Path.relative(Path.join(FileUtil.getApiDir(), this.controllerFolder), file);
  let controllerPath = '/' + relativeFile.replace(/\\/g, '/').replace('.js','').toLowerCase();

  if(controllerPath.endsWith('controller'))
    controllerPath = controllerPath.substring(0, controllerPath.length - 10);

  return controllerPath;
}

装饰器的实现

装饰器需要引入reflect-metadata库

先看看方法的装饰器,@GET,@POST之类的,实现方法是给装饰的方法加一个属性RouterRouter是个Symbol,确保唯一。 然后分析装饰的功能存到这个属性中,比如MethodPath等。

export function GET(path?: string) {
  return (target: BaseController, name: string) => setMethodDecorator(target, name, 'GET', path);
} 

function setMethodDecorator(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等。

export function BodyParam(target: BaseController, name: string, index: number) {
  setParamDecorator(target, name, index, { name: "", type: ParamType.Body });
}

function setParamDecorator(target: BaseController, name: string, index: number, value: {name: string, type: ParamType}) {
  let paramTypes = 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就用物理路由。

private setRouterForClass(exportClass: any, file: string) { 

  let controllerRouterPath = this.buildControllerRouter(file);
  let controller = new exportClass();

  for(let funcName in exportClass.prototype[Router]){
    let method = exportClass.prototype[Router][funcName].method.toLowerCase();
    let path = exportClass.prototype[Router][funcName].path;

    this.setRouterForFunction(method, controller, funcName, path ? `/${this.urlPrefix}${path}` : `/${this.urlPrefix}${controllerRouterPath}/${funcName}`);
  }
}

给controller里的方法参数赋上值并绑定路由到KoaRouter

private setRouterForFunction(method: string, controller: any, funcName: string, routerPath: string){
  this.koaRouter[method](routerPath, async (ctx, next) => { await this.execApi(ctx, next, controller, funcName) });
}

private async execApi(ctx: Koa.Context, next: Function, controller: any, funcName: string) : Promise<void> { //这里就是执行controller的api方法了
  try
  {
    ctx.body = await controller[funcName](...this.buildFuncParams(ctx, controller, controller[funcName]));
  }
  catch(err)
  {
    console.error(err);
    next(); 
  }
}

private buildFuncParams(ctx: any, controller: any, func: Function) { //把参数具体的值收集起来
  let paramsInfo = controller[Router][func.name].params;
  let params = [];
  if(paramsInfo)
  {
    for(let i = 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);
      }
    }
  }
  return params;
}

private getParam(ctx: any, paramType: ParamType, name: string){ // 从ctx里把需要的参数拿出来
  switch(paramType){
    case ParamType.Query:
      return ctx.query[name];
    case ParamType.Path:
      return ctx.params[name];
    case ParamType.Body:
      return ctx.request.body;
    default:
      console.error('does not support this param type');
  }
}

这样就完成了简单版的类似WebApi的路由.

源码下载:webapi-router_3water.rar

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

NodeJs 相关文章推荐
Nodejs爬虫进阶教程之异步并发控制
Feb 15 NodeJs
Nodejs中的this详解
Mar 26 NodeJs
NodeJS整合银联网关支付(DEMO)
Nov 09 NodeJs
Nodejs下用submit提交表单提示cannot post错误的解决方法
Nov 21 NodeJs
用nodejs搭建websocket服务器
Jan 23 NodeJs
详解如何在NodeJS项目中优雅的使用ES6
Apr 22 NodeJs
nodejs利用ajax实现网页无刷新上传图片实例代码
Jun 06 NodeJs
Nodejs之http的表单提交
Jul 07 NodeJs
nodejs中art-template模板语法的引入及冲突解决方案
Nov 07 NodeJs
NodeJs 实现简单WebSocket即时通讯的示例代码
Aug 05 NodeJs
Nodejs实现WebSocket代码实例
May 19 NodeJs
在NodeJs中使用node-schedule增加定时器任务的方法
Jun 08 NodeJs
Nodejs多站点切换Htpps协议详解及简单实例
Feb 23 #NodeJs
NodeJs下的测试框架Mocha的简单介绍
Feb 22 #NodeJs
基于Nodejs利用socket.io实现多人聊天室
Feb 22 #NodeJs
NodeJS配置HTTPS服务实例分享
Feb 19 #NodeJs
解决nodejs中使用http请求返回值为html时乱码的问题
Feb 18 #NodeJs
利用nodejs监控文件变化并使用sftp上传到服务器
Feb 18 #NodeJs
详解nodejs中exports和module.exports的区别
Feb 17 #NodeJs
You might like
php str_pad 函数使用详解
2009/01/13 PHP
php提示Call-time pass-by-reference has been deprecated in的解决方法[已测]
2012/05/06 PHP
PHP准确取得服务器IP地址的方法
2015/06/02 PHP
给PHP开发者的编程指南 第一部分降低复杂程度
2016/01/18 PHP
PHP进程通信基础之信号量与共享内存通信
2017/02/19 PHP
Swoole源码中如何查询Websocket的连接问题详解
2020/08/30 PHP
优秀js开源框架-jQuery使用手册(1)
2007/03/10 Javascript
jQuery多媒体插件jQuery Media Plugin使用详解
2014/12/19 Javascript
js对象的复制继承实例
2015/01/10 Javascript
JS+CSS实现带关闭按钮DIV弹出窗口的方法
2015/02/27 Javascript
深入理解JavaScript系列(30):设计模式之外观模式详解
2015/03/03 Javascript
jQuery实现的手机发送验证码倒计时效果代码分享
2015/08/24 Javascript
jQuery插件之jQuery.Form.js用法实例分析(附demo示例源码)
2016/01/04 Javascript
基于JavaScript代码实现随机漂浮图片广告
2016/01/05 Javascript
概述jQuery中的ajax方法
2016/12/16 Javascript
详解Angularjs在控制器(controller.js)中使用过滤器($filter)格式化日期/时间实例
2017/02/17 Javascript
前端构建工具之gulp的语法教程
2017/06/12 Javascript
angularjs 缓存的使用详解
2018/03/19 Javascript
element-ui 实现响应式导航栏的示例代码
2020/05/08 Javascript
解决Vue keep-alive 调用 $destory() 页面不再被缓存的情况
2020/10/30 Javascript
[49:12]完美世界DOTA2联赛PWL S2 Magma vs GXR 第二场 11.29
2020/12/02 DOTA
python操作MySQL数据库的方法分享
2012/05/29 Python
Python探索之创建二叉树
2017/10/25 Python
Python日期时间模块datetime详解与Python 日期时间的比较,计算实例代码
2018/09/14 Python
python实现beta分布概率密度函数的方法
2019/07/08 Python
python提取xml里面的链接源码详解
2019/10/15 Python
Python使用py2neo操作图数据库neo4j的方法详解
2020/01/13 Python
Tensorflow tf.nn.depthwise_conv2d如何实现深度卷积的
2020/04/20 Python
浅谈Python中threading join和setDaemon用法及区别说明
2020/05/02 Python
Python CSS选择器爬取京东网商品信息过程解析
2020/06/01 Python
意大利在线高尔夫商店:Online Golf
2021/03/09 全球购物
测试时代收集的软件测试面试题
2013/09/25 面试题
村优秀党员事迹材料
2014/01/15 职场文书
出纳员岗位责任制
2014/02/11 职场文书
教师学期个人总结
2015/02/11 职场文书
2015年煤矿安全工作总结
2015/05/23 职场文书