NodeJS Imitation WebApi Routing Example

  • 2021-07-24 09:59:27
  • OfStack

Anyone who has used WebApi or Asp. net MVC knows that Microsoft's routing design is very good, with 10 points convenient and 10 points flexible. Although it seems that some individuals are too flexible, it is easy for different developers in team to use different routing methods, which is a bit confusing. But this is not the point. When I was doing Node project, I felt that I kept using it use(...) To specify the routing path is annoying, so use the Typescript Wrote this based on Koa And Koa-router Routing plug-in, you can simply achieve a number of similar WebApi routing functions.

The target is like WebApi1:

1. Added controller will automatically join the route.

2. You can also specify the route manually through path ().

3. http method can be defined as GET Or POST Wait.

4. The parameters of Api can specify query param, path param and body in url.

Package has been uploaded to npm, npm install webapi-router installation, you can see the effect first:

Step 1: Set the directory of controllers and the fixed prefix of url

All controller are in this directory, which will automatically calculate the route according to the physical path. The fixed prefix of url is between host and the route, such as localhost/api/v2/user/name , api/v2 This is the fixed prefix.


import { WebApiRouter } from 'webapi-router';

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

Step 2 is that controller inherits from BaseController


export class TestController extends BaseController
{

}

Step 3 Add decorators to the controller method


@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 The parameter in is optional. If it is empty, the physical path of this controller will be used as the routing address.

Typescript0 Is a variable in the path, such as /user/brook , Typescript0 Is brook Which can be obtained with @ PathParam in the parameters of the method

@QueryParam Available url Li ? Parameters after

@BodyParam Available Post Coming up body

Isn't it a bit of WebApi?

Now let's take a concrete look at how it is realized

The implementation process is actually very simple. Starting from the above goal, we first get the physical path of controllers, and then get the method decorated by the decorator and its parameters.
The purpose of the decorator is to get is Get Or Post Wait, and it is specified Path Finally, the data in node request is assigned to the parameters of the method.

Core code:

Get the physical path


initRouterForControllers() {
  // Find out all inherited from the specified directory BaseController Adj. .js Documents 
  let files = FileUtil.getFiles(this.controllerFolder);

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

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

Turn from physical path to route


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;
}

Realization of decorator

Decorators need to be introduced reflect-metadata库

Look at the decorator of the method first. @GET , @POST And so on, the implementation method is to add 1 attribute to the decorated method Router , Router It's a Symbol , ensure only 1. Then the function of analyzing decoration is stored in this attribute, such as Method , Path Wait.


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;
}

There is also a parameter decorator, which is used to assign parameters to Koa-router1 Values in, such as body , param Wait.


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 };
}

This decorated data is stored on the Router property of the object, which can be used later when building the route.

Bind routing to Koa-router Upper

The route is obtained from the physical path above, but the parameter path in the decoration takes precedence, so look at the one just existing in the prototype first Router Is there any in the attribute Path If there is, use this as a route, and there is no Path Use physical routing.


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}`);
  }
}

Assign values to method parameters in controller and bind routes to 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> { // This is the execution controller Adj. api Method 
  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) { // Collect the specific values of the parameters 
  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){ //  From ctx Take out the required parameters in 
  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');
  }
}

This completes a simple version of WebApi-like routing.

Source download: webapi-router_jb51. rar


Related articles: