上一节我们学习了装饰器在TypeScript和babel中的支持情况,过了一遍5种类型的装饰器以及它们在Nest中源码是如何定义的,并且最后会调用Metadata
相关API来操作装饰器参数。
有同学可能会问,学习了装饰器之后能做什么,或者装饰器和元数据之间有什么关系呢?这节我们来详细聊一聊!
组合装饰器
知道了每种类型的装饰器如何使用之后,对于一些复杂的场景,我们可能需要结合使用不同的装饰器,例如我们不仅想给我们的接口添加静态检查,还想加上运行时检查的能力。
要实现上面功能,需要下面2个步骤:
- 参数装饰器优先于方法装饰器,首先要标记需要检查的参数
- 重写descriptor的value值,运行参数检查器,失败则抛出异常
首先定义一个参数装饰器,同时收集所有传入的参数和对应的验证器
typescript
type Validator = (x: any) => boolean;
// 保存标记
const validateMap: Record<string, Validator[]> = {};
// 标记需要检查的参数
function typedDecoratorFactory(validator: Validator): ParameterDecorator {
return (_, key, index) => {
const target = validateMap[key as string] ?? [];
target[index] = validator;
validateMap[key as string] = target;
}
}
定义一个方法装饰器,用于批量遍历并验证参数的合法性,如果不合格,抛出参数名称和对应位置索引
javascript
function validate(_: Object, key: string, descriptor: PropertyDescriptor) {
const originalFn = descriptor.value;
descriptor.value = function(...args: any[]) {
// 运行检查器
const validatorList = validateMap[key];
if (validatorList) {
args.forEach((arg, index) => {
const validator = validatorList[index];
if (!validator) return;
const result = validator(arg);
if (!result) {
throw new Error(
`参数校验失败: ${arg} 索引为: ${index}`
);
}
console.log(`参数 ${arg} 校验成功!`)
});
}
// 运行原有的方法
return originalFn.call(this, ...args);
}
}
最后我们在User类中定义一个方法,同时声明参数验证器,绑定需要验证的参数
less
const isInt = typedDecoratorFactory((x) => Number.isInteger(x));
const isString = typedDecoratorFactory((x) => typeof x === 'string');
class Person {
@validate
sayHello(@isString name: string, @isInt age: number) {
return `姓名:${name}, 年龄:${age}`;
}
}
最后的代码是这样
typescript
type Validator = (x: any) => boolean;
// 保存标记
const validateMap: Record<string, Validator[]> = {};
// 标记需要检查的参数
function typedDecoratorFactory(validator: Validator): ParameterDecorator {
return (_, key, index) => {
const target = validateMap[key as string] ?? [];
target[index] = validator;
validateMap[key as string] = target;
}
}
function validate(_: Object, key: string, descriptor: PropertyDescriptor) {
const originalFn = descriptor.value;
descriptor.value = function(...args: any[]) {
// 运行检查器
const validatorList = validateMap[key];
if (validatorList) {
args.forEach((arg, index) => {
const validator = validatorList[index];
if (!validator) return;
const result = validator(arg);
if (!result) {
throw new Error(
`参数校验失败: ${arg} 索引为: ${index}`
);
}
console.log(`参数 ${arg} 校验成功!`)
});
}
// 运行原有的方法
return originalFn.call(this, ...args);
}
}
const isInt = typedDecoratorFactory((x) => Number.isInteger(x));
const isString = typedDecoratorFactory((x) => typeof x === 'string');
class Person {
@validate
sayHello(@isString name: string, @isInt age: number) {
return `姓名:${name}, 年龄:${age}`;
}
}
const p = new Person();
p.sayHello('hello', 2); // pass
p.sayHello('hello', '333'); // error
通过codeopen
执行结果如下:
javascript
"参数 hello 校验成功!"
"参数 2 校验成功!"
"参数 hello 校验成功!"
Uncaught Error: 参数校验失败: 333 索引为: 1
at https://cdpn.io/cpe/boomboom/pen.js?key=pen.js-0dbca2df-e214-0345-2f83-bcfc6dd9de47:37
到这里,你应该可以实现一个类似于Nest的参数验证器了
元数据
元数据和装饰器在ECMAScript中是两个独立的部分,在stage 3中被分为两个提案跟进,但是想要实现类似Java中反射的能力, 你总同时需要它们。
什么是反射?
反射技术是在运行时(runtime)可以访问、检测和修改它本身状态或行为的一种能力。例如在 Nest.js 中,是利用 TypeScript 的装饰器和元数据功能,在运行时动态地获取和操作类、方法、属性等元素的信息的能力。
元数据有什么用呢,我们通过装饰器+元数据来改造一下上面的例子,现在有这样一个需求,我不希望每次新增参数都需要加一个@IsString、@IsBoolean这样的验证器,而是借助TypeScript自身的类型声明来自动检查参数类型。
上面我们说了,元数据提供这样的能力,让我们可以获取编译时的类型。由于metadata还在stage阶段,所以我们使用reflect-metadata
来实现这个功能。
typescript
// 在codeopen中导入这个 等价于 import 'reflect-metadata'
import "https://esm.sh/reflect-metadata";
function validate(target: Object, key: string, descriptor: PropertyDescriptor) {
const originalFn = descriptor.value;
// 获取参数的编译期类型
const designParamTypes = Reflect.getMetadata('design:paramtypes', target, key);
descriptor.value = function(...args: any[]) {
args.forEach((arg, index) => {
const paramType = designParamTypes[index];
const result = arg.constructor === paramType
|| arg instanceof paramType;
if (!result) {
throw new Error(
`参数校验失败: ${arg} 索引为: ${index}`
);
}
console.log(`参数 ${arg} 校验成功!`)
});
// 运行原有的方法
return originalFn.call(this, ...args);
}
}
class Person {
@validate
sayHello(name: string, age: number) {
return `姓名:${name}, 年龄:${age}`;
}
}
const p = new Person();
p.sayHello('hello', 2); // pass
p.sayHello('hello', '333'); // error
执行结果是一样的:
这样我们就通过元数据实现了参数验证功能,核心是这行代码,它表示获取参数类型
javascript
Reflect.getMetadata('design:paramtypes', target, key)
三种内置类型
目前为止一共有三种编译期类型可以拿到,是内置的:
- design:type: 属性的类型。
- desin:paramtypes: 方法的参数的类型。
- design:returntype: 方法的返回值的类型。
这三种方式拿到的结果都是构造函数(例如String和Number)。规则是:
- number -> Number
- string -> String
- boolean -> Boolean
- void/null/never -> undefined
- Array/Tuple -> Array
- Class -> 类的构造函数
- Enum -> 如果是纯数字枚举则为Number , 否则是 Object
- Function -> Function
- 其余都是Object
打印一下案例中的类型就知道了
那在Nest中是不是这样的呢?我们一起来看看,在package/common/constants.ts
源码中也找到了这个常量
Nest中的TypeScript默认开启了emitDecoratorMetadata
这个配置,它会将这个常量注入到运行时,这样我们就可以拿到设置的元数据信息进行动态操作了,依赖注入也是基于这个实现的。
总结
我们通过组合装饰器的方式实现了类似Nest中的参数验证器,掌握这个可以写很多复杂的装饰器了。但是在这个场景下,需要不断新增验证器函数,我们利用TypeScript自带的类型系统,加上metadata运行时特性,实现了自动参数校验。
metadata内置了三种类型,Nest中源码中也可以找到design:paramtypes定义,到这里基本就清楚Nest中各种装饰器是如何实现的了。
最后,Nest是依赖TypeScript配置中默认开启的emitDecoratorMetadata
配置,实现在运行时注入metadata类型来获取装饰器信息,依赖注入和路由映射也是基于这个来实现的,相关核心内容我们后续再学习。