egg框架封装 - Controller增加路由装饰器

2,883 阅读4分钟

背景知识

由于Typescript天然支持decorator,只需要再tsconfig.js里增加配置

{
	"experimentalDecorators": true
}

AOP 面向切面编程

使用装饰器对于工程最大的两个好处就是

  • 不需要去抽象出一个基类,写一些基础函数
  • 函数内部结构完全没有被改变,逻辑依旧干净整洁

这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。

如何实现

这里主要讲下一会要用的MetadataReflect

Metadata - 元数据

metadata - 用于修饰数据的数据,比如 描述一个人,身高,体重等这些都是元数据

任何一个对象的描述都是通过众多的元数据构成的

tips:这里插入一个知识点

装饰器(Decorator)和后端java中的注解(Annotation)有什么不一样,我开始以为一样的? 几乎一样,出来一下一点区别

区别:

  • 注解 仅提供附加元数据支持,并不能实现任何操作。
  • 装饰器 仅提供定义劫持,能够对类及其方法的定义但是没有提供任何附加元数据的功能。

结论:

对于Decorator来言,是无法直接进行元数据的操作的,想要对元数据进行操作,还需要借助于比如Object或者Reflect-metadata来实现

Reflect

是ES6引入的一个内置对象,个人感觉像一个Object的工具方法集

MDN定义如下

Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与处理器对象的方法相同。Reflect不是一个函数对象,因此它是不可构造的。你不能将其与一个new运算符一起使用,或者将Reflect对象作为一个函数来调用。Reflect的所有属性和方法都是静态的(就像Math对象)。

Reflect的设计目的

  • 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上

  • 修改某些Object方法的返回结果,让其变得更合理。

  • 让Object操作都变成函数行为。某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)让它们变成了函数行为。

  • Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。

但ES6提供的Reflect并不能修改元数据(Metadata),那么装饰器自然也不行了。

那么Reflect-metadata是啥?

Reflect-metadata是ES7的一个提案,它主要用来在声明的时候添加和读取元数据

一码胜千言,举个栗子。

function classDecorator(): ClassDecorator {
	return target => {
		Reflect.defineMetadata('classMetaData', 'a', target);
	}
}

@classDecorator
class SomeoneClass {
	
}

Reflect.getMetadata('classMetaData',oneClass); // output: a

如果你还不知道装饰器,这里猛戳这里

进入主题

先看看最原始的代码

// router/beeTea/router.ts
const BeeTeaRouter = (app: Application) => {
  const { controller, router } = app;
  const subRouter = router.namespace('/hammer/beetea');
  subRouter.get('/category/query/v1', controller.beeTea.categoryQuery);
};

// controller/beeTea.ts
public async categoryQuery() {
  const { ctx } = this;
  const { shopCode } = ctx.query;
  ctx.body = await ctx.service.beeTea.category.getCategoryListByShopCode(shopCode);
}

// service/beeTea/category.ts
public async getCategoryListByShopCode(shopCode: string) {
  const { ctx } = this;
  const categoryParams = this.buildCategoryParams(shopCode);
  const result = await ctx.curl('http://xxx.com/product-api/product/base/get_category_business/v4', {
    method: 'POST',
    contentType: 'json',
    data: categoryParams,
    dataType: 'json',
  });
  return result.res;
}

上面的代码有几个问题:

  • BeeTeaRouter代码冗余,能否直接在Controller层上直接定义

期望这样编写

// controller/beeTea.ts

@RequestMapping("/hammer/beetea")
class SomeoneController extends Controller {
	@RequestMapping(value = "/category/query/v1", method = RequestMethod.GET)
	categoryQuery() {
		const { ctx } = this;
	  const { shopCode } = ctx.query;
	  ctx.body = await ctx.service.beeTea.category.getCategoryListByShopCode(shopCode);
	}
}
  • getCategoryListByShopCode代码中url不好维护,易错

期望这样编写

// old
const result = await ctx.curl('http://xxx.com/product-api/product/base/get_category_business/v4', {
  method: 'POST',
  contentType: 'json',
  data: categoryParams,
  dataType: 'json',
});

// new
@prefix(value = 'product-api')
const result = await ctx.curl('/product/base/get_category_business/v4', {
  method: 'POST',
  contentType: 'json',
  data: categoryParams,
  dataType: 'json',
});

Coding

这里贴出了关键代码

class RouteEnhancer {
  // 缓存 Controller 原型
  private static __prototype = {};
  /**
   * 路由映射 - 简化 router和Controller 代码
   * @param path 路径
   * @param method 请求方法 支持RESTFUL API
   * @param middleware 路由中间件
   */
  public RouteMapping(
    path = '',
    method: HTTP_METHOD = 'GET',
    ...middleware: Middleware[]
  ) {
    // 判断是class 的装饰器工厂还是 method 的装饰器工厂
    return function(target: any, propName?: string | symbol, descriptor?: PropertyDescriptor) {
      if (typeof target === 'function' && propName === undefined && descriptor === undefined) {
        RouteEnhancer.handleClassDecorator(target, path, middleware);
        return;
      } else if (typeof propName === 'string' && typeof descriptor === 'object') {
        RouteEnhancer.handleMethodDecorator(target, path, method, propName, middleware);
        return;
      }
    };
  }
  /**
   * 处理 Class 上路由的装饰器
   * @param target class 构造函数
   * @param path 路由路径
   * @param middleware 路由中间件
   */
  private static handleClassDecorator(
    target: Function,
    path: string,
    middleware: Middleware[],
  ) {
    if (!path) {
      throw new Error('path must be non-empty string');
    }
    if (isEmptyObject(RouteEnhancer.__prototype)) {
      RouteEnhancer.__prototype = target.prototype;
    }
    const allClassRouteData = Reflect.getMetadata('CLASS_ROUTE_PATH', target.prototype) || {};
    if (allClassRouteData[target.name]) {
      throw new Error(`you have defined a same path[${path}] by class decorator`);
    }
    const classRouteData: ClassPathData = { path, middleware };
    allClassRouteData[target.name] = classRouteData;
    Reflect.defineMetadata('CLASS_ROUTE_PATH', allClassRouteData, target.prototype);
  }
  /**
   * 处理 Method 上的路由的装饰器
   * @param target 方法装饰器所在类的原型 或者 类的构造函数
   * @param path 路由路径
   * @param method 路由方法
   * @param propName 方法名
   * @param middleware 方法的descriptor
   */
  private static handleMethodDecorator(
    target: Function | object,
    path: string,
    method: HTTP_METHOD,
    propName: string,
    middleware: Middleware[],
  ) {
    const proto = typeof target === 'function' ? target.prototype : target;
    if (isEmptyObject(RouteEnhancer.__prototype)) {
      RouteEnhancer.__prototype = proto;
    }
    const methodRouteData = Reflect.getMetadata('METHOD_ROUTE_PATH', proto) || {};
    methodRouteData[path] = methodRouteData[path] || [];
    methodRouteData[path].push({
      method,
      middleware,
      handlerName: propName,
      constructorFunction: proto.constructor,
      className: proto.constructor.name,
    } as Route);
    Reflect.defineMetadata('METHOD_ROUTE_PATH', methodRouteData, proto);
  }
  /**
   * 初始化路由
   * @param app Application 实例
   * @param options 所有路由的统一前缀
   */
  initRouter(app: Application, options = { prefix: '' }) {
    const { router } = app;
    // 所有 method 装饰器,存在 Controller 原型上的数据
    const allMethodRouteData = Reflect.getMetadata('METHOD_ROUTE_PATH', RouteEnhancer.__prototype);
    // 所有class 装饰器,存在 Controller 原型上的数据
    const allClassRouteData = Reflect.getMetadata('CLASS_ROUTE_PATH', RouteEnhancer.__prototype);
    Object.keys(allMethodRouteData).forEach((methodPath: string) => {
      allMethodRouteData[methodPath].forEach((routeData: Route) => {
        const classRouteData = allClassRouteData[routeData.className];
        const fullPath = `${options.prefix}${classRouteData.path}${methodPath}`;
        const methodLowerCase = routeData.method.toLowerCase();
        console.log(`register URL * ${routeData.method} ${fullPath} * ${routeData.className}.${routeData.handlerName}`);
        router[methodLowerCase](fullPath, ...classRouteData.middleware, ...routeData.middleware, async ctx => {
          const controllerIns = new routeData.constructorFunction(ctx);
          await controllerIns[routeData.handlerName](ctx);
        });
      });
    });
  }
}

应用到Controller

只需要两步:

  1. 在 router.ts 初始化
// app/router.ts
import { Application } from 'egg';
import { initRouter } from '../lib/RouterEnhancer';
export default (app: Application) => {
  initRouter(app);
};
  1. 在Controller文件里尽情用吧,不需要在编写router 文件了。
// controller/xx.ts
import { Controller } from 'egg';
import { RouteMapping } from '../../lib/RouterEnhancer';
@RouteMapping('/hammer/beetea')
export default class BeeTeaController extends Controller {
  @RouteMapping('/category/query/v1', 'POST')
  public async categoryQuery() {
    const { ctx } = this;
    const { shopCode } = ctx.query;
    ctx.body = await ctx.service.beeTea.category.getCategoryListByShopCode(shopCode);
  }
}

完成,亲测OK!!!

这里面的东西还挺多我会专门拿篇文章来写如何完成整个功能的。