装饰器
装饰器是 TypeScript
的实验性特性,目的是给装饰的目标添加上某些特定的功能,就好比是AOP切面编程
,可以对类、方法、属性、参数进行切面增强。
Nestjs
完全使用装饰器语法来构建应用,学习作为前置知识之一的装饰器是必不可少的。
装饰器环境
装饰器属于 TypeScript
的实验性特性,所以需要在配置文件 tsconfig.json
中开启支持。
{
"compilerOptions": {
// ...
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
类装饰器
对类进行切面增强的装饰器,会在绑定时注入一个参数,即使这个类还未实例化,装饰器函数也会执行。
装饰器在代码解析执行时,应用一次,即时我们并没有使用 Animal
类。
类型声明:
type ClassDecorator = <TFunction extends Function>
(target: TFunction) => TFunction | void;
@参数:
target
: 当前装饰的类构造器。
@返回:
如果类装饰器返回了一个值,它将会被用来代替原有的类构造器的声明。
const LoggerDecorator = <T extends { new (...args: any[]): {} }>(target: T)=>{
console.log('无须new实例,类装饰器也会执行')
console.log(target) // target === 当前装饰的类 -> Animal
}
@LoggerDecorator
class Animal{}
如果类装饰器返回一个新的类,它会将返回的类来替换原有类的定义。
const LoggerDecoratorNewClass = <T extends { new (...args: any[]): {} }>(
target: T,
) => {
return class extends IAnimal {
name: string = "new class";
age: number = 18;
};
};
class IAnimal {
name?: string;
age?: number;
}
@LoggerDecoratorNewClass
class NewAnimal extends IAnimal {}
console.log(new NewAnimal().name);
遗憾的是,对于拓展的类,TypeScript
并无类型提示,意味着我们只能额外提供一个类用于提供类型提示,比如 IAnimal
类就是这个作用。
语法糖
装饰器本质上是一个普通的函数,装饰器的语法 @Decorator
相当于将被装饰的类传递进这个函数中,然后在函数内进行增强操作。
const Decorator: ClassDecorator = (target) => {
target.prototype.name = "class装饰器";
};
// 1.语法糖写法
@Decorator
class SyntacticSugar {
constructor() {
console.log((this as any).name);
}
}
// 2.函数写法
Decorator(SyntacticSugar);
new SyntacticSugar(); // "class装饰器"
叠加使用
多个装饰器可以叠加使用,不同类型的装饰器的执行顺序是明确定义的:
- 实例成员:参数、方法、访问器、属性
- 静态成员:参数、方法、访问器、属性
- 构造函数:参数
- 类装饰器
对于属性、方法、访问器装饰器来说,静态与实例的执行顺序是根据它们的定义顺序来决定的。
同类型装饰器执行顺序
对同一个目标应用多个装饰器,它们呈现的顺序是 Koa洋葱模型
的顺序。也就是先会执行外层的装饰器代码,再执行调用内从的装饰器代码。
const OrderDecorator: (key: string) => ClassDecorator = (key) => {
console.log("evaluate: ", key);
return (target) => {
console.log("call: ", key);
return target;
};
};
@OrderDecorator("class A")
@OrderDecorator("class B")
class User {}
不同类型装饰器执行顺序
不同类型的装饰器(类、方法、参数)的装饰器可以对同一个类叠加使用。静态成员和实例成员的执行顺序是根据它们定义的顺序来定的。
const PrintDecorator: (key: string) => any = (key) => {
console.log("解释: ", key);
return function () {
console.log("执行: ", key);
};
};
@PrintDecorator("类")
class C {
@PrintDecorator("静态属性")
static prop?: number;
@PrintDecorator("静态方法")
static method(@PrintDecorator("静态方法参数") foo: any) {}
constructor(@PrintDecorator("构造器参数") foo: any) {}
@PrintDecorator("实例方法")
method(@PrintDecorator("实例方法参数") foo: any) {}
@PrintDecorator("实例属性")
prop?: number;
}
装饰器工厂
装饰器工厂是一个高阶函数,让装饰器函数接收参数,进一步增强切面处理。
接收任意类型的参数,返回一个装饰器函数,就是装饰器工厂。
const DecoratorFactory = (type: string): ClassDecorator => {
return function (target) {
target.prototype.name = `类型-- ${type}`;
};
};
@DecoratorFactory("Animal")
class Animal {}
@DecoratorFactory("person")
class Person {}
console.log((new Animal() as any).name); // "类型-- Animal"
console.log((new Person() as any).name); // "类型-- Person"
方法装饰器
对方法进行切面增强的装饰器,可以对方法的原型修改、切面增强、替换实现等操作方式。
类型声明:
type MethodDecorator = <T>(
target: Object,
propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;
- @参数:
target
:对于静态成员来说是类的构造器,对于实例成员来说是类的原型链。propertyKey
:装饰的方法的名称。descriptor
:装饰的方法的属性描述符。
方法装饰器不同于其他装饰器的地方在于,我们可以通过 descriptor
参数属性描述符,来对方法进行增强,添加一些通用的逻辑。
例如我们可以给方法新增切面打印参数和结果的效果:
const LoggerDecorator: MethodDecorator = function (
target,
propertyKey,
descriptor: PropertyDescriptor,
) {
const original = descriptor.value as Function;
descriptor.value = function (...args: any[]) {
console.log("params: ", ...args);
const result = original.call(this, ...args);
console.log("result: ", result);
return result;
};
};
class LoggerTest {
@LoggerDecorator
add(x: number, y: number) {
return x + y;
}
}
const l = new LoggerTest();
l.add(1, 2);
下面是用方法装饰器实现一个最小的 Get
装饰器案例。
import axios from "axios";
const Get = (url: string): MethodDecorator => {
return (
target: Object,
propertyKey: string | symbol,
descriptor: PropertyDescriptor,
) => {
const original = descriptor.value as Function;
descriptor.value = async function (...args: any[]) {
const params = Object.keys(args[0] as IParams).reduce(
(prev, item, currentIndex, array) => {
prev += `${currentIndex === 0 ? "?" : "&"}${item}=${args[0][item]}`;
return prev;
},
"",
);
const response = await axios.get(url + params);
const responseData = response.data;
console.log("params: ", ...args);
const result = original.call(this, ...args, responseData);
console.log("result: ", result);
return result;
};
};
};
interface IParams {
page: number;
size: number;
}
class Http {
@Get("https://api.apiopen.top/api/getHaoKanVideo")
public getList(params: IParams, data?: any) {
console.log("🚀 ~ data", data);
return data;
}
}
new Http().getList({ page: 1, size: 5 });
参数装饰器
类型声明:
type ParameterDecorator = (
target: Object,
propertyKey: string | symbol,
parameterIndex: number
) => void;
- @参数:
target
:对于静态成员来说是类的构造器,对于实例成员来说是类的原型链。propertyKey
:装饰的参数名称。parameterIndex
:参数在方法中所处的位置的索引。
@返回:
返回的结果将被舍弃忽略,也就是不需要返回。
单独的参数装饰器能做的事情很有限,它一般都被用于记录可被其它装饰器使用的信息。
借助方法装饰器“最小Get
案例”,传递一个 key
,得到对应 key
的 data
数据。
import axios from "axios";
import "reflect-metadata";
interface IParams {
page: number;
size: number;
}
const Get = (url: string): MethodDecorator => {
return (
target: Object,
propertyKey: string | symbol,
descriptor: PropertyDescriptor,
) => {
const original = descriptor.value as Function;
descriptor.value = async function (...args: any[]) {
const params = Object.keys(args[0] as IParams).reduce(
(prev, item, currentIndex, array) => {
prev += `${currentIndex === 0 ? "?" : "&"}${item}=${args[0][item]}`;
return prev;
},
"",
);
const response = await axios.get(url + params);
const responseData = response.data;
const key = Reflect.getMetadata("key", target, propertyKey) as string;
return original.call(
this,
...args,
key ? responseData[key] : responseData,
);
};
};
};
const Result = (key: string): ParameterDecorator => {
return (target: Object, propertyKey: string | symbol | undefined) => {
// 记录数据,方便方法装饰器校验处理
Reflect.defineMetadata("key", key, target, propertyKey as string | symbol);
};
};
class Http {
@Get("https://api.apiopen.top/api/getHaoKanVideo")
public getList(params: IParams, @Result("result") data?: any) {
console.log("🚀 ~ data", data);
}
}
new Http().getList({ page: 1, size: 5 });
属性装饰器
类型声明:
type PropertyDecorator =
(target: Object, propertyKey: string | symbol) => void;
- @参数:
target
:对于静态成员来说是类的构造器,对于实例成员来说是类的原型链。propertyKey
:装饰的属性名称。
@返回:
返回的结果将被忽略。
属性装饰器常用于监视、修改或替换类的属性定义。比如实现类型检查,或者实现 get/set
方法。属性装饰器一般不单独使用,主要用于配合类/方法装饰器进行组合装饰。
实现属性类型检查的例子:
const MinLength = (length: number): PropertyDecorator => {
return (target: Object, propertyKey: string | symbol) => {
let value: string;
let descriptor: PropertyDescriptor = {
set(v: string) {
if (v.length < length) {
throw new Error(`密码长度不能低于${length}位`);
}
value = v;
},
get() {
return value;
},
};
Object.defineProperty(target, propertyKey, descriptor);
};
};
class User {
@MinLength(6)
public password: string;
constructor(password: string) {
this.password = password;
}
}
new User("123");
元数据
假如你想实现反射这样的能力,那么就需要元数据 reflect-metadata
。
元数据可以帮助我们获取编译器的类型。
目前只有三个可用的键:
design:type
: 属性的类型。desin:paramtypes
: 方法的参数的类型。design:returntype
: 方法的返回值的类型。
以下是使用反射编写的自动类型检查器的例子:
import '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(
`Failed for validating parameter: ${arg} of the index: ${index}`
);
}
});
return originalFn.call(this, ...args);
}
}
class C {
@validate
sayRepeat(word: string, x: number) {
return Array(x).fill(word).join('');
}
}
const c = new C();
c.sayRepeat('hello', 2); // pass
c.sayRepeat('', 'lol' as any); // throw an error
类型不对时,抛出错误,符合例子的失败的用例。
何时使用?
在阅读上面例子之后,对于何时使用装饰器相信你已经得出结论,一般不同类型的装饰器和元数据进行结合使用,以下是一些常用的使用场景:
- 切面编程
- 监听属性改变或者方法调用
- 对方法的参数进行转换
- 运行时类型检查
- 添加额外的方法和属性
- 。。。等等