在NestJs中大量使用了装饰器,所以学习NestJs前首先需要了解装饰器的基本用法。
装饰器可以解决的问题:
-
一个类、一个方法应该专注于处理一块单一功能的逻辑(即单一功能原则),但在实际的业务场景中,我们经常会在业务代码中插入额外的逻辑代码。
比如统计行为、性能,记录日志等。使用装饰器可以让代码保持单一原则,把这些额外的代码抽离出来让代码更简洁,另一方面也对这些额外的逻辑代码做了封装提高了复用性。
-
又或是需要解决强依赖的过度耦合问题,比如过于依赖一个a类来实现另一个新的b类,当a类需要修改的时候b类也需要修改。
使用装饰器可以做到依赖注入从到实现依赖反转。
模拟装饰器
有一个User类,其中它有自己的login方法。现在我们需要在它的登录方法中添加登录时间的日志记录逻辑。
通常我们会直接在login方法中添加日志逻辑,但这回我们需要不直接修改login方法。
js中有一个方法叫Object.defineProperty,熟悉vue2的同学会知道这个API,它可以用来定义对象的属性。
它也可以用来定义类的方法并返回这个类本身。
Object.defineProperty(target, key, descriptor)
如果key对应的是普通方法的名字,target就是类的原型
如果key对应的是静态方法的名字,target就是类
在descriptor中可以获取到这个方法的描述符,比如value、writable、enumerable、configurable等
如果key对应的是一个方法名,value就是这个方法的引用
模拟装饰器的实现步骤:
- 获取类原本的方法内容(即descriptor),可以通过Object.getOwnPropertyDescriptor(target, key);。
- 修改这个方法,比如在原有的方法上加入登录日志逻辑。
- 用新的方法覆盖原有的方法,可以通过Object.defineProperty(target, key, descriptor); 。
这个就是类装饰器想要达到的目的。
装饰器不同类型
1. 类装饰器
从模拟类装饰器中我们得出需要在原有类的基础上生成一个新的类,可以用继承或者修改原型的方法达到。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
const happyHuman = <T extends new (...args: any[]) => any>(
constructor: T,
) => {
return class extends constructor {
name = 'happy human'; // 装饰器的属性会覆盖类的属性
getName() { // 会覆盖类已有方法,或添加新的类方法
return this.name;
}
};
};
@happyHuman
class User {
[key: string]: any;
name: string;
constructor() {
this.name = 'human';
}
}
console.log(new User().getName()); // happy human
|
当类装饰器需要传参数时,可以把参数套在装饰器函数的外面(高阶函数),这样就可以在装饰器函数内部使用这个参数了。
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
|
const setNameDecorator = (firstName: string, lastName: string) => {
const fullName = `${firstName}.${lastName}`;
return <T extends new (...args: any[]) => any>(target: T) => {
return class extends target {
getName() {
return `My name is ${fullName}.`;
}
};
};
};
@setNameDecorator('Llane', 'zhang')
class User {
[key: string]: any;
constructor() {
this.name = 'human';
}
getName() {
return this.name;
}
}
console.log(new User().getName()); // My name is Llane.zhang.
|
除了使用类继承方法来生成一个新的类,我们还可以修改类的原型
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
39
40
41
42
43
44
45
46
47
48
49
50
|
type TUserProfile = {
firstName: string;
lastName: string;
[key: string]: any;
};
const ProfileDecorator = (profile: TUserProfile) => {
const { firstName, lastName } = profile;
const fullName = `${firstName}.${lastName}`;
return <T extends new (...args: any[]) => any>(target: T) => {
const Original = target;
// 在原型上添加一个新属性
Original.prototype.fullName = fullName;
// 修改原型上的方法
Original.prototype.getName = function () {
return `My name is ${fullName}.`;
};
// override构造函数
function constructor(...args: any[]) {
return new Original(...args);
}
// 赋值原型链
constructor.prototype = Original.prototype;
// 添加一个静态方法
constructor.myInfo = `myInfo ${JSON.stringify(profile)}`;
return constructor as unknown as typeof Original;
};
};
@ProfileDecorator({ firstName: 'Llane', lastName: 'zhang', age: 25 })
class User {
[key: string]: any;
constructor() {
this.name = 'human';
}
getName() {
return this.name;
}
}
interface StaticUser {
new (): TUserProfile;
myInfo: string;
}
console.log(new User().getName()); // My name is Llane.zhang.
console.log((User as unknown as StaticUser).myInfo); // myInfo {"firstName":"Llane","lastName":"zhang","age":25}
|
2. 属性装饰器
一般不单独使用,主要用于配合类或方法装饰器进行组合装饰
属性装饰器主要有两个参数
- target 普通属性时为当前对象原型 即:x.prototype,静态属性时为当前对象的类
- propertyKey 属性名称
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
|
// 这里储存属性装饰器中存的参数
const userRoles: string[] = [];
const RoleDecorator =
(roles: string[]) =>
(target: any, key: string) => {
roles.forEach((role) => userRoles.push(role));
};
const SetRoleDecorator = <T extends new (...args: any[]) => { [key: string]: any },>(
constructor: T,
) => {
const roles = [
{ name: 'super-admin', desc: '超级管理员' },
{ name: 'admin', desc: '管理员' },
{ name: 'user', desc: '普通用户' },
];
return class extends constructor {
constructor(...args: any) {
super(...args);
this.roles = roles.filter((role) => userRoles.includes(role.name));
}
};
};
@SetRoleDecorator
class UserEntity {
@RoleDecorator(['admin', 'user'])
roles: string[] = [];
}
const user = new UserEntity();
console.log(user.roles); // [ { name: 'admin', desc: '管理员' }, { name: 'user', desc: '普通用户' } ]
|
3. 方法装饰器
方法装饰器接受三个参数(比属性装饰器多一个方法的属性描述符 descriptor)
- target 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
- propertyKey 方法名
- descriptor 方法的属性描述符
其中属性描述符有以下几个子属性
- configurable?: boolean; 是否能修改或删除这个属性
- enumerable?: boolean; 该属性是否可枚举可被遍历出来
- value?: any; 用于定于新的方法来替换旧方法(请使用function 以免内部不能使用this来绑定)
- writable?: boolean; 是否可写
- get?: any; get访问器
- set(v: any)?: void; set访问器
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
|
const LoggerDecorator = () => {
return function logMethods(
target: any,
propertyName: string,
propertyDescriptor: PropertyDescriptor,
): PropertyDescriptor {
// 保留原有的登录逻辑
const methods = propertyDescriptor.value;
// 重写方法
propertyDescriptor.value = async function (...args: any[]) {
try {
await methods.apply(this, args);
} finally {
const now = new Date().valueOf();
console.log(`logged in ${now}`);
}
};
return propertyDescriptor;
};
};
class UserService {
@LoggerDecorator()
async login() {
await new Promise((resolve) => {
setTimeout(resolve, 2000);
});
console.log('login success');
}
}
const user = new UserService();
user.login(); // login success // logged in 1679550405528
|
4. 参数装饰器
每个类方法的入参也可以有自己的装饰器,一般也配合类或方法装饰器使用
参数装饰器的参数有:
target 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
key 参数名
index 参数在函数参数列表中的索引
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
|
// 创造一个专门用于格式化入参的装饰器
// 储存格式化入参方法
const argsParse: ((...args: any[]) => any)[] = [];
const parse =
(parseFn: (...args: any[]) => number) =>
(target: any, key: string, index: number) => {
argsParse[index] = parseFn;
};
const parseDecorator = (
target: any,
propertyName: string,
descriptor: PropertyDescriptor,
): PropertyDescriptor => {
const method = descriptor.value;
return {
...descriptor,
value(...args: any[]) {
const newArgs = args.map((value, index) => {
return argsParse[index] ? argsParse[index](value) : value;
});
return method.apply(this, newArgs);
},
};
};
interface IUser {
id: number;
name: string;
}
class UserService {
private users: IUser[] = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Ben' },
];
getUsers() {
return this.users;
}
@parseDecorator
delete(@parse((arg: any) => Number(arg)) id: any) {
this.users = this.users.filter((user) => user.id !== id);
}
}
const userService = new UserService();
console.log('before delete 1', userService.getUsers());
userService.delete(1);
console.log('after delete 1', userService.getUsers());
console.log('before delete 2', userService.getUsers());
userService.delete('2'); // 这里将入参string格式化为number了,所以依旧可以运行成功
console.log('after delete 2', userService.getUsers());
// before delete 1 [ { id: 1, name: 'Alice' }, { id: 2, name: 'Ben' } ]
// after delete 1 [ { id: 2, name: 'Ben' } ]
// before delete 2 [ { id: 2, name: 'Ben' } ]
// after delete 2 []
|
5. 访问器装饰器
参数和方法装饰器一样(其实就是属性的get 和 set方法)
通过访问器装饰器可以重写get和set方法和descriptor的属性。
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
export const HiddenDecorator = () => {
return (
target: any,
propertyName: string,
descriptor: PropertyDescriptor,
) => {
descriptor.enumerable = false;
};
};
const PrefixDecorator = (prefix: string) => {
return (
target: any,
propertyName: string,
descriptor: PropertyDescriptor,
) => {
return {
...descriptor,
set(value: string) {
descriptor.set.apply(this, [`${prefix}_${value}`]);
},
};
};
};
class UserEntity {
private _nickName: string;
private fullName: string;
@HiddenDecorator()
@PrefixDecorator('Llane')
get nickName() {
return this._nickName;
}
set nickName(value: string) {
this._nickName = value;
this.fullName = `${value}_fullName`;
}
getFullName() {
return this.fullName;
}
}
const user = new UserEntity();
console.log(user); // UserEntity { _nickName: undefined, fullName: undefined }
console.log(user.nickName);
user.nickName = 'goodman';
console.log(user.nickName); // Llane_goodman
|
装饰器的调用顺序
参数 -> 方法 -> get/set/属性 -> 类
当对一个被装饰者调用多个装饰器的时候
1
2
3
4
5
|
@A
@B
getName() => {
return this.name;
}
|
上述代码相当与A(B(getName))
它们的调用步骤类似剥洋葱法,即:
- 由上至下依次对装饰器表达式求值。
- 求值的结果会被当作函数,由下至上依次调用。