装饰器实现参数的校验

前言

需要用到的工具:

  • 参数装饰器:记录参数的索引,方法名;
  • tslib_1.__metadata:这是ts的内置方法用于保存方法的参数类型、方法类型以及方法返回数据的类型;
  • reflect-metadata:保存参数的数据;
  • class-validator:对类成员进行校验;
  • class-transformer:使用class-transformer的plainToClass方法将对象转化成指定类的实例。

参数装饰器保存相关元数据

参数装饰器返回返回三个参数:1.构造函数的原型对象;2.参数所属方法名;3.参数的索引。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 1.target: 构造函数的原型对象;
// 2.key: 参数所属方法名;
// 3.index: 参数的索引
const createRouteParamDecorator = (paramtype) => {
return (data?: any, ...pipes: PipeTransform[]) => {
return (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 Query = (property?: string, ...pipes: PipeTransform[]) => {
return createRouteParamDecorator(RouteParamtypesEnum.QUERY)(property, ...pipes);
};

保存参数类型元数据

参数装饰器保存相关元数据分别保存了三个数据,但是为什么呢?自然是用于取出保存的某数据,而这个某数据就是参数定义的类型,比如现在有一个类Foo,并定义了一个setInfo成员方法,这个方法的参数定义了一个类型NameInfo

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
class NameInfo {
ch: string;
en: string;
}

class AgeInfo {
num: number;
born: string;
}

class PersonInfo {
name: NameInfo;
age: AgeInfo;
}

@Control('foo')
class Foo {
private name: NameInfo;
private age: AgeInfo;

@Put('name')
setInfo(
@Query() _name: NameInfo,
@Query() _age: AgeInfo,
): PersonInfo {
this.name = _name;
this.age = _age;
return { name: this.name, age: this.age };
}
}

setInfo编译后的代码如下:

1
2
3
4
5
6
7
8
tslib_1.__decorate([
index_1.Put('name'),
tslib_1.__param(0, param_dec_1.Query()),
tslib_1.__param(1, param_dec_1.Query()),
tslib_1.__metadata("design:type", Function),
tslib_1.__metadata("design:paramtypes", [NameInfo, AgeInfo]),
tslib_1.__metadata("design:returntype", PersonInfo)
], Foo.prototype, "setInfo", null);

在定义的参数装饰器外,还另外多定义了三个tslib_1.__metadata装饰器。这个三个tslib_1.__metadata的定义分别是:

  • tslib_1.__metadata("design:type", Function):存储成员方法的方法类型;

  • tslib_1.__metadata("design:paramtypes", [NameInfo, AgeInfo]):方法的参数类型;

  • tslib_1.__metadata("design:returntype", PersonInfo):方法返回的类型。

而这个tslib_1.__metadata方法相当于调用reflect-metadata库的Reflect.defineMetadata方法:

1
Reflect.defineMetadata(metadataKey, metadataValue, C.prototype, "method");

关于tslib_1.__metadata的具体实现可以参考附录中的tslib_1.__metadata

由上可知,在执行参数装饰器时,将方法的参数类型作为一个数组保存到构造函数的原型对象->参数所属方法名->metadataKey之中。

参数校验

根据构造函数原型对象引用、参数所属方法名、metadata key即刚刚的design:paramtypes就可以拿到刚刚存储的参数类型数组。

1
2
3
4
5
6
const handlerArgsTypes: any[] = Reflect.getMetadata(
ReflectDefaultMetadata.DESGIN_PARAMTYPES,
prototype,
methodName,
);
// [NameInfo, AgeInfo]

然后在根据存储的参数索引即可拿到参数对应的类型。

有了类型信息,再加上请求是传过来的对象,就可以使用class-transformer的plainToClass方法将请求的参数转化成参数类型的实例:

1
clsObj = plainToClass(type, param)

有了实例,就可以使用class-validatorvalidate方法对请求参数进行校验。class-validator这个库提供了许多装饰器可以对类实例的成员进行校验,比如@isInt可以校验可以校验整型数据,@isString可以校验字符类型的数据

1
2
3
4
5
6
7
8
9
10
11
import { IsInt, IsString, validate} from 'class-validator';

class AgeInfo {
@IsInt({ message: '$property必须是整型' })
num: number;

@IsString()
born: string;
}

const errors = await validate(clsObj);

附录

tslib_1.__metadata

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
// tslib_1.__metadata("design:paramtypes", [NameInfo, AgeInfo])
__metadata = function (metadataKey, metadataValue) {
if (
typeof Reflect === "object"
&& typeof Reflect.metadata === "function"
) {
return Reflect.metadata(metadataKey, metadataValue);
}
}

// Reflect.metadata的实现
function metadata(metadataKey, metadataValue) {
function decorator(target, propertyKey) {
if (!IsObject(target))
throw new TypeError();
if (!IsUndefined(propertyKey) && !IsPropertyKey(propertyKey))
throw new TypeError();
OrdinaryDefineOwnMetadata(metadataKey, metadataValue, target, propertyKey);
}
return decorator;
}

// Reflect.defineMetadata的实现
function defineMetadata(metadataKey, metadataValue, target, propertyKey) {
if (!IsObject(target))
throw new TypeError();
if (!IsUndefined(propertyKey))
propertyKey = ToPropertyKey(propertyKey);
return OrdinaryDefineOwnMetadata(metadataKey, metadataValue, target, propertyKey);
}

__decorate

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
__decorate = function (decorators, target, key, desc) {
var c = arguments.length,
r = c < 3
? target
: desc === null
? desc = Object.getOwnPropertyDescriptor(target, key)
: desc,
d;
if (
typeof Reflect === "object"
&& typeof Reflect.decorate === "function"
) {
r = Reflect.decorate(decorators, target, key, desc);
} else {
for (var i = decorators.length - 1; i >= 0; i--) {
if (d = decorators[i]) {
r = (
c < 3
? d(r)
: c > 3
? d(target, key, r)
: d(target, key)
) || r;
}
}
}
return c > 3 && r && Object.defineProperty(target, key, r), r;
};