用装饰器的语法写koa路由

殳俊晤
2023-12-01

koa-route-decors

目录结构

├── src
│   ├── constants.ts // 常量
│   ├── controller.ts // @Controller 装饰器
│   ├── index.ts // 入口文件
│   ├── injectable.ts // @Injectable 装饰器,声明可被注入的类
│   ├── injector.ts // 注射器
│   ├── interface.ts
│   ├── request.ts // http请求方法的装饰器, @Get @Post
│   ├── router.ts // 加载路由
│   └── utils.ts 
├── package-lock.json
├── package.json
├── tsconfig.json
└── tslint.json

正文

1. 定义http请求方法的装饰器

// 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');

// 装饰器工厂构造器
function createDecorator(method: RequestMethod) {
  return function (path = '') {
    return function (target: {[key: string]: any}, key: string, descriptor: PropertyDescriptor) {
      // 附加元数据
      Reflect.defineMetadata(PATH_METADATA, path, descriptor.value); // 附加路由路径
      Reflect.defineMetadata(METHOD_METADATA, method, descriptor.value); // 附加请求方法
    };
  };
}

2. 定义Controller装饰器,声明该类是一个控制器,用于后面加载路由

/// 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); // 附加路由前缀路径
  };
}

3. 定义一个注射器,依赖注入的容器

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

// 一个单例注射器类
class Injector {

  static injector: Injector;
  private readonly providerMap = new Map(); // 储存可被注入的依赖

  private constructor() {}

  // 获取注射器实例
  static getInstance() { 
    if (!Injector.injector) {
      Injector.injector = new Injector();
    }
    return Injector.injector;
  }

  // 添加依赖到注入容器
  inject(target: Constructor) {
    // 通过反射获取构造函数参数类型
    const paramTypes = Reflect.getMetadata('design:paramtypes', target) || [];
    if (this.providerMap.has(target)) return; // 已注册
    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); // 依赖放入到容器中
  }
  
  // 创建一个实例化工厂,真正的为给定的类注入依赖
  factory(target: Constructor) {
    const paramTypes = Reflect.getMetadata('design:paramtypes', target) || [];
    // 获取需要注入的依赖
    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); // 如果依赖还有依赖,递归调用
      } else {
        return new item(); // 没有依赖了,直接获取依赖的实例
      }
    });
    return new target(...dependencies); // 把所有的依赖注入的给定的类
  }
}
// 获取注射器实例并导出
const rootInjector = Injector.getInstance();
export {rootInjector};

4. 定义可被注入类的装饰器

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

export function Injectable() {
  return function (target: any) {
    rootInjector.inject(target); // 调用注射器的inject方法,把该类放入注入容器
  };
}

5. 构建路由,实例化控制器

// 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';

// 初始化路由
export function initRouter(controller: Constructor) {
  const router = new Router(); 
  const routes = mapRoute(controller); // 所有路由的映射
  // 绑定路由到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;
}

// 自动加载给定路径下面,*.controller.ts 文件下的控制器,绑定路由
export async function autoRouter(rootDir: string) {
  const router = new Router();
  const reg = /.+controller.ts$/;
  const files = await Utils.getFile(rootDir); // 获取给定路径下的所有文件路径
  const controllers = files.filter(item => reg.test(item)).map(item => require(item)); // 导入所有的控制器
  for (const controller of controllers) {
    const keys = Object.keys(controller);
    // 拿到所有符合要求的控制器
    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);
    // 遍历控制器,绑定合并路由
    controllerClass.forEach(item => {
      const subRouter = initRouter(item);
      router.use(subRouter.routes());
    });
  }
  return router;

}

// 获取controller的路由映射
function mapRoute(controller: Constructor) {
  const instance = rootInjector.factory(controller); // 实例化了控制器类
  const prototype = Object.getPrototypeOf(instance); // 获取控制器原型对象,类实例方法全部都定义在该对象中
  // 获取该类的所有方法名称,排除不是函数和构造函数的所有方法名
  const methodNames = Object.getOwnPropertyNames(prototype).filter(item => !(prototype[item] === prototype.constructor) && Utils.isFunction(prototype[item]));
  return methodNames.map(methodName => {
    const handle = prototype[methodName]; // 路由实际执行的方法
    const prefix = Reflect.getMetadata(PATH_METADATA, controller); // 获取通过@Controller装饰器定义的路由前缀
    const method = Reflect.getMetadata(METHOD_METADATA, handle); // 获取路由的http请求方法@Post, @Get 等装饰器定义
    let url = Reflect.getMetadata(PATH_METADATA, handle); // 获取路由方法的路径
    url = url ? url : `/${methodName}`; // 如果没有传递路径,默认使用方法名
    url = prefix + url; // 合并路由
    return {url, method, handle: handle.bind(instance), methodName}; // 返回一个路由映射,bind重新绑定this
  });
}

到这里,所有的逻辑全部实现完毕。

用伪代码模拟一下用法

├── src
│   ├── user.controller.ts
│   ├── user.service.ts
│   ├── user.model.ts
│   └── utils.ts 
├── package-lock.json
├── package.json
├── tsconfig.json
└── tslint.json

// user.controller.ts
@Contriller()
export class UserController {
  constructor(private userService: UserService) {} // 注入服务
  @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);
	 return info;
  }
}

// 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}); 
	return user;
  }
}

代码已上传至github koa-route-decors, 完整的koa项目实现例子huzz-koa-template,也可以看我的另外一篇文章nodejs项目的正确打开方式,typescript + koa

 类似资料: