背景知识
由于Typescript天然支持decorator,只需要再tsconfig.js里增加配置
{
"experimentalDecorators": true
}
AOP 面向切面编程
使用装饰器对于工程最大的两个好处就是
- 不需要去抽象出一个基类,写一些基础函数
- 函数内部结构完全没有被改变,逻辑依旧干净整洁
这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。
如何实现
这里主要讲下一会要用的Metadata 和 Reflect
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
只需要两步:
- 在 router.ts 初始化
// app/router.ts
import { Application } from 'egg';
import { initRouter } from '../lib/RouterEnhancer';
export default (app: Application) => {
initRouter(app);
};
- 在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!!!
这里面的东西还挺多我会专门拿篇文章来写如何完成整个功能的。