koa-route-decors

The directory structure

└ ─ SRC │ ├─ Constants. Ts/ / constant│ ├ ─ ─ controller. Ts// @controller decorator│ ├ ─ ─ index. Ts// Import file│ ├ ─ ─ injectable. Ts// the @Injectable decorator declares classes that can be injected│ ├ ─ ─ injector. Ts/ / syringe│ ├─ ├─ Request// HTTP request method decorator, @get @post│ ├ ─ ─ the router. Ts// Load the route│ └ ─ ─ utils. Ts ├ ─ ─ package - lock. Json ├ ─ ─ package. The json ├ ─ ─ tsconfig. Json └ ─ ─ tslint. JsonCopy the code

The body of the

1. Define the decorator for HTTP request methods

// request.ts
import 'reflect-metadata';
import {RequestMethod} from './interface';
import {METHOD_METADATA, PATH_METADATA} from './constants';

export const Get = createDecorator('get');
export const Post = createDecorator('post');
export const Put = createDecorator('put');
export const Delete = createDecorator('delete');
export const Options = createDecorator('options');
export const Patch = createDecorator('patch');

// Decorator factory constructor
function createDecorator(method: RequestMethod) {
  return function (path = ' ') {
    return function (target: {[key: string]: any}, key: string, descriptor: PropertyDescriptor) {
      // Append metadata
      Reflect.defineMetadata(PATH_METADATA, path, descriptor.value); // Append the routing path
      Reflect.defineMetadata(METHOD_METADATA, method, descriptor.value); // Attach the request method
    };
  };
}
Copy the code

2. Define the Controller decorator and declare that this class is a Controller that will be used to load routes later

/// controller.ts
import 'reflect-metadata';
import {Constructor} from './interface';
import {PATH_METADATA} from './constants';

export function Controller(path = ' ') {
  return function (target: Constructor) {
    Reflect.defineMetadata(PATH_METADATA, path, target); // Append route prefix path
  };
}
Copy the code

3. Define a syringe, dependency injection container

// injector.ts
import 'reflect-metadata';
import {Constructor} from './interface';

// A single syringe class
class Injector {

  static injector: Injector;
  private readonly providerMap = new Map(a);// Store dependencies that can be injected

  private constructor() {}

  // Get an example of a syringe
  static getInstance() { 
    if(! Injector.injector) { Injector.injector =new Injector();
    }
    return Injector.injector;
  }

  // Add dependencies to the injection container
  inject(target: Constructor) {
    // Get the constructor parameter type by reflection
    const paramTypes = Reflect.getMetadata('design:paramtypes', target) || [];
    if (this.providerMap.has(target)) return; / / registered
    for (const p of paramTypes) {
      if (p === target) {
        throw new Error('can not depend self');
      } else if (!this.providerMap.has(p)) {
        throw new Error('dependency is not register'); }}this.providerMap.set(target, target); // The dependency is put into the container
  }
  
  // Create an instance factory to actually inject dependencies into a given class
  factory(target: Constructor) {
    const paramTypes = Reflect.getMetadata('design:paramtypes', target) || [];
    // Get the dependencies to inject
    const dependencies = paramTypes.map((item: Constructor) = > {
      if (!this.providerMap.has(item)) {
        throw new Error('dependency is not register');
      } else if (item.length) {
        return this.factory(item); // If there are dependencies, call recursively
      } else {
        return new item(); // There is no dependency}});return newtarget(... dependencies);// Inject all dependencies into the given class}}// Get the syringe instance and export it
const rootInjector = Injector.getInstance();
export {rootInjector};
Copy the code

4. Define decorators for classes that can be injected

// injectable.ts
import {rootInjector} from './injector';

export function Injectable() {
  return function (target: any) {
    rootInjector.inject(target); // Call the inject method of the syringe to put the class into the injection container
  };
}
Copy the code

5. Construct routes and instantiate controllers

// router.ts
import 'reflect-metadata';
import {METHOD_METADATA, PATH_METADATA} from './constants';
import * as Router from 'koa-router';
import {Constructor} from './interface';
import {Utils} from './utils';
import {rootInjector} from './injector';

// Initialize the route
export function initRouter(controller: Constructor) {
  const router = new Router(); 
  const routes = mapRoute(controller); // Map all routes
  // Bind the route to the koa-router
  for (const item of routes) {
    const {url, method, handle} = item;
    switch (method) {
      case 'get':
        router.get(url, handle);
        break;
      case 'post':
        router.post(url, handle);
        break;
      case 'put':
        router.put(url, handle);
        break;
      case 'delete':
        router.delete(url, handle);
        break;
      case 'options':
        router.options(url, handle);
        break;
      case 'patch':
        router.patch(url, handle);
        break; }}return router;
}

// Automatically load the controllers in the *.controller.ts file under the given path, bind the route
export async function autoRouter(rootDir: string) {
  const router = new Router();
  const reg = /.+controller.ts$/;
  const files = await Utils.getFile(rootDir); // Get all file paths under the given path
  const controllers = files.filter(item= > reg.test(item)).map(item= > require(item)); // Import all controllers
  for (const controller of controllers) {
    const keys = Object.keys(controller);
    // Get all the controllers that meet the requirements
    const controllerClass = keys.map(item= > {
      if (
        Utils.isFunction(controller[item])
        && controller[item] === controller[item].prototype.constructor
        && typeof Reflect.getMetadata(PATH_METADATA, controller[item]) === 'string'
      ) {
        return controller[item];
      } else {
        return false;
      }
    }).filter(item= > item);
    // Traverse the controller and bind the merged route
    controllerClass.forEach(item= > {
      const subRouter = initRouter(item);
      router.use(subRouter.routes());
    });
  }
  return router;

}

// Get the route mapping of the controller
function mapRoute(controller: Constructor) {
  const instance = rootInjector.factory(controller); // Instantiate the controller class
  const prototype = Object.getPrototypeOf(instance); // Get the controller prototype object in which the class instance methods are all defined
  // Get all method names of the class, excluding all method names that are not functions and constructors
  const methodNames = Object.getOwnPropertyNames(prototype).filter(item= >! (prototype[item] === prototype.constructor) && Utils.isFunction(prototype[item]));return methodNames.map(methodName= > {
    const handle = prototype[methodName]; // The route is actually executed
    const prefix = Reflect.getMetadata(PATH_METADATA, controller); // Get the route prefix defined by the @Controller decorator
    const method = Reflect.getMetadata(METHOD_METADATA, handle); // HTTP request methods @post, @get, etc
    let url = Reflect.getMetadata(PATH_METADATA, handle); // Get the path of the routing method
    url = url ? url : ` /${methodName}`; // If no path is passed, the method name is used by default
    url = prefix + url; // Merge routes
    return {url, method, handle: handle.bind(instance), methodName}; // Return a route map. Bind rebinds this
  });
}

Copy the code

At this point, all the logic has been implemented.

Use pseudocode to simulate usage

├ ─ ─ the SRC │ ├ ─ ─ the user. The controller. The ts │ ├ ─ ─ the user. The service. The ts │ ├ ─ ─ the user. The model. The ts │ └ ─ ─ utils. Ts ├ ─ ─ package - lock. Json ├ ─ ─ Package. json ├─ tsconfig.json ├─ tslint.json// user.controller.ts
@Contriller()
export class UserController {
  constructor(private userService: UserService) {} // Inject the service
  @Get()
  async userInfo(ctx: Context, next: () => Promise<any>) {
    const {id} = ctx.request.body
  	const info = await this.userServuce.getUserInfoById(id); ctx.body = info; }}// user.service.ts
@Injectable()
export class UserService {
  constructor(private userModel: UserModel) {}
  async getUserInfoById(id: number) {
	 const info = await this.userModel.findById(id);
	 returninfo; }}// user.model.ts
@Injectable()
export class UserModel {
  private repository: Repository<User>;
  private select: (keyof User)[] = ['id'.'username'.'nickname'];
  constructor() {
    this.repository = getRepository(User);
  }

  async findById(id: number) {
	const user = await this.repository.findOne(id, {select: this.select}); 
	returnuser; }}Copy the code

The code has been uploaded to Github koa-Route-Decors, a complete koA project implementation example, Huzz-Koa-template, or see my other article on how to open nodeJS projects correctly, typescript + KOA