装饰器实现路由的自动挂载

装饰器实现路由的自动挂载

  • 类装饰器工作记录contriller路径;
  • 方法装饰器记录方法路径;
  • 参数装饰器记录参数类型;
  • 组合路由

类装饰器工作记录controller路径

controller装饰器的实现

1
2
3
4
5
export const Control = (prefix: string = '') => {
return (target: any) => {
Reflect.defineMetadata(ROUTE_CONTROLLER_METADATA, prefix, target);
};
};

实现一个装饰个装饰器工厂函数,传入要保存的controller路径,工厂函数内部返回一个闭包函数(即是装饰器),在类装饰器函数中会传入一下参数,第一个是当前类构造函数的引用。然后使用reflect-metadata这个库的defineMetadata方法保存controller路径。这个方法一共需要传入三个参数:分别是metadata的key,第二个是要保存的数据,第三个也是一个键值,在这里使用构造函数的引用作为键值,这里的存储路径是:“构造函数”下保存以“metadate key”作为键值的数据“controller路径”。

在取出时只需要使用方法getMetadata(<metadata key>, <ref>)按defineMetadata方法的顺序传参即可在任何地方取出刚刚保存的数据。

方法装饰器记录方法路径

方法装饰器根据http的动词方法进行封装。

跟controller装饰器实现类型,同样封装一样的装饰工厂来保存路径。与controller装饰器的实现不同的是,还需要额外保存另外两个数据:1.http动词;2.保存方法名。需要http方法动词是因为挂载路由时需要制定不同的http方法;保存方法名,则是为了在后续可以访问到与路径对应的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const createHttpMethodDecorator = (methodtype) => {
return (path: string = '') => {
return (target: any, propertyKey: string) => {
const handlers = Reflect.getMetadata(ROUTE_HANDLE_METADATA, target) || [];
handlers.push(propertyKey);
Reflect.defineMetadata(ROUTE_HANDLE_METADATA, handlers, target);
// 设置方法
Reflect.defineMetadata(ROUTE_METHOD_METADATA, methodtype, target, propertyKey);
// 设置路由
Reflect.defineMetadata(ROUTE_URL_METADATA, path, target, propertyKey);
};
};
};

export const Get = createHttpMethodDecorator(HttpMethodEnum.GET);
export const Post = createHttpMethodDecorator(HttpMethodEnum.POST);
export const Put = createHttpMethodDecorator(HttpMethodEnum.PUT);
export const Delete = createHttpMethodDecorator(HttpMethodEnum.DELETE);
export const Patch = createHttpMethodDecorator(HttpMethodEnum.PATCH);
export const All = createHttpMethodDecorator(HttpMethodEnum.ALL);
export const Options = createHttpMethodDecorator(HttpMethodEnum.OPTIONS);
export const Head = createHttpMethodDecorator(HttpMethodEnum.HEAD);

注意:方法装饰器传入的第一个参数和类装饰器不同,类装饰器传入的是构造函数的引用,而方法装饰器传入的构造函数原型对象的引用(Ctor.prototype)。

参数装饰器记录参数类型

参数装饰器的封装比以上两种会稍微复杂一点,主要需要保存的数据有:

  1. 参数装饰的类型:包含Query对应get方法请求参数,Body对应post等方法参数,Param对应路径上的参数;
  2. 参数的索引:参数在方法参数列表上的索引;
  3. 参数名:参数的名字(字符串)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
const assignMetadata = (args, paramtype, index, data, ...pipes: PipeTransform[]) => {
return {...args,
[`${paramtype}:${index}`]: {
paramtype,
paramIndex: index,
propName: data,
pipes,
}};
};

const createRouteParamDecorator = (paramtype) => {
return (data?: any, ...pipes: PipeTransform[]) => (target, key, index) => {
const args = Reflect.getMetadata(ROUTE_ARGS_METADATA, target, key) || {};
Reflect.defineMetadata(
ROUTE_ARGS_METADATA,
assignMetadata(args, paramtype, index, data, ...pipes),
target,
key,
);
};
};

export const Request = createRouteParamDecorator(RouteParamtypesEnum.REQUEST);
export const Response = createRouteParamDecorator(RouteParamtypesEnum.RESPONSE);
export const UploadFileStream = createRouteParamDecorator(RouteParamtypesEnum.FILE_STREAM);
export const Headers = createRouteParamDecorator(RouteParamtypesEnum.HEADERS);

export const Query = (property?: string, ...pipes: PipeTransform[]) => {
return createRouteParamDecorator(RouteParamtypesEnum.QUERY)(property, ...pipes);
};

export const Body = (property?: string, ...pipes: PipeTransform[]) => {
return createRouteParamDecorator(RouteParamtypesEnum.BODY)(property, ...pipes);
};

export const Param = (property?: string, ...pipes: PipeTransform[]) => {
return createRouteParamDecorator(RouteParamtypesEnum.PARAM)(property, ...pipes);
};

组合路由

首先需要动态引入Controller,因为装饰器保存的数据都是保存在键名是构造函数引用下或构造函数原型对象引用下。

1
2
const controller = require(file).default;
const prototype = controller.prototype;

取出Controller装饰器保存的路径:

1
2
// 控制器前缀 类装饰器,传入的是构造函数
const ctrlPrefix = Reflect.getMetadata(ROUTE_CONTROLLER_METADATA, constructor) || '';

取出存储的方法名:

1
2
// 函数 方法、属性装饰器,传入原型
const methodNames = Reflect.getMetadata(ROUTE_HANDLE_METADATA, prototype) || [];

取出该方法下的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
methodNames.forEach((methodName) => {
// http方法:get 、post、put等
const httpMethod: string = Reflect.getMetadata(ROUTE_METHOD_METADATA, prototype, methodName);
// 方法装饰器声明的路由
const urlPath: string = Reflect.getMetadata(ROUTE_URL_METADATA, prototype, methodName) || '';
// 是否上传文件
const uploadFile = Reflect.getMetadata(UPLOAD_FILE_METADATA, prototype, methodName) || null;
// handler的参数
const handlerArgs: RouteParamMetadataInterface = Reflect.getMetadata(
ROUTE_ARGS_METADATA,
prototype,
methodName,
) || {};
// handlerArgs的参数类型
const handlerArgsTypes: any[] = Reflect.getMetadata(
ReflectDefaultMetadata.DESGIN_PARAMTYPES,
prototype,
methodName,
);
for (const key of Object.keys(handlerArgs)) {
const {paramIndex} = handlerArgs[key];
// 相应位置的参数类型。类型声明
handlerArgs[key].type = handlerArgsTypes[paramIndex];
}
handlers.push({
urlPath,
httpMethod,
methodName,
handlerArgs,
uploadFile,
});
});

从上面我们就拿到了:

  • Controller装饰器保存的路径;ctrlPrefix
  • 方法装饰器保存的路径:urlPath
  • 方法装饰器的类型:httpMethod

有以上数据就可以进行路由的挂载:

1
2
3
4
5
6
7
8
9
10
11
12
13
// url = [ctrlPrefix, urlPath].join('/');
app.router[httpMethod](url, ...middleware, async (ctx: Context) => {
const instance = new controller(ctx);
if (uploadFile) {
ctx.request.file = await this.getUploadFile(ctx, uploadFile);
}
const params: any[] = await this.getRouteParams(ctx, handlerArgs);
// instance[methodName]使用methodName保存的方法名调用与路由对应的方法
const result = await instance[methodName](...params);
if (ctx.body === undefined && result !== undefined) {
ctx.body = result;
}
});

总结

  1. 使用类装饰器和方法装饰器工厂函数保存路由的路径;
  2. 方法装饰器除了保存路径外,还需要保存以下数据:
    1. 方法装饰器根据http方法进行封装,所以还需要保存不同方法的类型;
    2. 方法的名字,用于后续挂载路由时,调用与路由对应的方法
  3. 使用reflect-metadata将路径保存在构造函数或构造函数原型对象的引用下,根据metadataKey来区分不同的数据;
  4. 在挂载路由时只需要引入对应的Controller就可取出所存储的数据,拼接处完成的路由路径,并实例化Controller并调用对应的方法。