ts-decorator
在看本文前最好先看一下《阮一峰-es6 中的装饰器》
装饰器用于给类,方法,属性以及方法参数等增加一些附属功能而不影响其原有特性。其在 Typescript 应用中的主要作用类似于 Java 中的注解,在 AOP(面向切面编程)使用场景下非常有用。
面向切面编程(AOP) 是一种编程范式,它允许我们分离横切关注点,藉此达到增加模块化程度的目标。它可以在不修改代码自身的前提下,给已有代码增加额外的行为(通知)
装饰器一般用于处理一些与类以及类属性本身无关的逻辑,例如: 一个类方法的执行耗时统计或者记录日志,可以单独拿出来写成装饰器。
看一下官方的解释更加清晰明了
装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上。 装饰器使用
@expression
这种形式,expression
求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。
如果有使用过 spring boot 或者 php 的 symfony 框架的话,就基本知道装饰器的作用分别类似于以上两者注解和 annotation,而 node 中装饰器用的比较好的框架是 nest.js。不过不了解也没关系,接下来我就按我的理解讲解一下装饰器的使用。
不过目前装饰器还不属于标准,还在建议征集的第二阶段,但这并不妨碍我们在 ts 中的使用。只要在 tsconfig.json
中开启 experimentalDecorators
编译器选项就可以愉快地使用啦^^
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
基本概念
可能有些时候,我们会对传入参数的类型判断、对返回值的排序、过滤,对函数添加节流、防抖或其他的功能性代码,基于多个类的继承,各种各样的与函数逻辑本身无关的、重复性的代码。
比如,我们要在用户登录的时候记录一下登录时间
const logger = (now: number) => console.log(`lasted logged in ${now}`);
class User {
async login() {
await setTimeout(() => console.log('login success'), 100);
logger(new Date().valueOf());
}
}
以上代码把记录日志的代码强行写入登录的逻辑处理,这样代码量越高则代码越冗余。我们需要把日志逻辑单独拿出来,使 login 方法更专注于处理登录的逻辑,接下去我们用高阶函数模拟一下装饰器的原理,以便于后面更好的理解装饰器。
/**
* 使用高阶函数
* 柯里化解构登录与日志记录
*/
type DecoratorFunc = (
target: any,
key: string,
descriptor: PropertyDescriptor,
) => void;
// 模拟的装饰器工厂函数
const createDecorator =
(decorator: DecoratorFunc) => (Model: any, key: string) => {
// 获取即将使用装饰器的类原型
const target = Model.prototype;
// 获取这个原型上某个方法的描述
const descriptor = Object.getOwnPropertyDescriptor(target, key);
// 更改描述,生成新的方法
decorator(target, key, descriptor);
};
const logger: DecoratorFunc = (target, key, descriptor) =>
// 将修改后的函数重新定义到原型链上
Object.defineProperty(target, key, {
...descriptor,
value: async (...args: any[]) => {
try {
return descriptor.value.apply(this, args); // 调用之前的函数
} finally {
const now = new Date().valueOf();
console.log(`lasted logged in ${now}`);
}
},
});
class User {
async login() {
console.log('login success');
await new Promise((resolve) => {
setTimeout(resolve, 100);
});
}
}
export const exp1 = () => {
console.log();
console.log(
'-----------------------示例1:高阶函数柯里化(装饰器内部原理)-----------------------',
);
console.log(
'-----------------------实现登录和日志记录解耦-----------------------',
);
console.log();
const loggerDecorator = createDecorator(logger);
loggerDecorator(User, 'login');
const user = new User();
user.login();
console.log();
console.log('-----------------------示例1:执行完毕-----------------------');
};
// 控制台输出
// login success
// 停顿100ms
// lasted logged in 1571771681793
了解了以上概念,接下去让我们学习真正的装饰器。
装饰器类型
TS 中的装饰器有几种类型,如下:
-
参数装饰器
-
方法装饰器
-
访问符装饰器
-
属性装饰器
-
类装饰器
以上每中装饰器分别可以作用于类原型(prototype属性)和类本身
类装饰器
TS 官方文档中举了一个类装饰器的例子,也可以看一下。类装饰器其实就是把我们本身的类传入装饰器注解中,并对这个类或类的原型进行一些处理,仅此而已。例 如:
const HelloDerorator = <T extends new (...args: any[]) => any>(
constructor: T,
) => {
return class extends constructor {
newProperty = 'new property';
hello = 'override';
sayHello() {
return this.hello;
}
};
};
@HelloDerorator
export class Hello {
[key: string]: any; // 此处用于防止eslint提示sayHello方法不存在
hello: string;
constructor() {
this.hello = 'test';
}
}
const exp2 = () => {
console.log(
'-----------------------示例2:简单的类装饰器-----------------------',
);
console.log(
'-----------------------动态添加一个sayHello方法以及覆盖hello的值-----------------------',
);
console.log();
const hello = new Hello();
console.log(hello.sayHello());
console.log();
console.log('-----------------------示例2:执行完毕-----------------------');
};
// 控制台打印 override
装饰器工厂
上面的方法我们为UserService
添加了一个HelloDerorator
装饰器,这个装饰器的属性将覆盖UserService
的默认属性。为了方便给装饰器添加其它参数,我们把HelloDerorator
改造成为一个装饰器工厂,如下:
const SetNameDerorator = (firstname: string, lastname: string) => {
const name = `${firstname}.${lastname}`;
return <T extends new (...args: any[]) => any>(target: T) => {
return class extends target {
_name: string = name;
getMyName() {
return this._name;
}
};
};
};
@SetNameDerorator('jesse', 'pincman')
class UserService {
getName() {}
}
const exp3 = () => {
console.log();
console.log(
'-----------------------示例3:装饰器工厂-----------------------',
);
console.log(
'-----------------------通过继承方式 重载getName方法-----------------------',
);
console.log();
const user = new UserService();
console.log(user.getName());
console.log();
console.log('-----------------------示例3:执行完毕-----------------------');
};
// 控制台打印 jesse.pincman
其它用法
我们还可以对类原型链property
上的属性/方法和类本身的静态属性/方法进行赋值或重载操作,还可以重载构造函数,如下:
type UserProfile = Record<string, any> & {
phone?: number;
address?: string;
};
const ProfileDecorator = (profile: UserProfile) => (target: any) => {
const Original = target;
let userinfo = '';
Object.keys(profile).forEach((key) => {
userinfo = `${userinfo}.${profile[key].toString()}`;
});
// 添加一个原型属性
Original.prototype.userinfo = userinfo;
// 使用函数创建一个新的类(类构造器),返回值为传入类的对象,这样就重载了构造函数
function constructor(...args: any[]) {
console.log('contruct has been changed');
return new Original(...args);
}
// 赋值原型链
constructor.prototype = Original.prototype;
// 添加一个静态属性
// constructor.myinfo = `myinfo ${userinfo}`;
return constructor as typeof Original;
};
// 因为静态属性是无法通过[key: string]: any;获取类型提示的,所以这里添加一个接口用于动态各类添加静态属性
interface StaticUser {
new (): UserService;
myinfo: string;
}
@ProfileDecorator({ phone: 133, address: 'zhejiang' })
class ProfileService {}
const exp4 = () => {
console.log();
console.log(
'-----------------------示例4:修类的构造函数,原型属性,静态属性等-----------------------',
);
console.log(
'-----------------------设置原型属性值,重载构造防反,添加静态属性-----------------------',
);
console.log();
// console.log((ProfileService as unknown as StaticUser).myinfo);
const profile = new ProfileService();
console.log((profile as any).userinfo);
console.log();
console.log('-----------------------示例4:执行完毕-----------------------');
};
// 打印静态属性则控制台输出 myinfo .133.zhejiang
// 控制台输出 contruct has been changed
// 控制台输出 .133.zhejiang
属性装饰器
属性装饰器一般不单独使用,主要用于配合类或方法装饰器进 行组合装饰
参数
属性装饰器函数有两个参数:
target
对于普通属性,target 就是当前对象的原型,也就是说,假设 Employee 是对象,那么 target 就是 Employee.prototype
对于静态属性,target 就是当前对象的类
propertyKey
属性的名称
使用示例
const userRoles: string[] = [];
// 通过属性装饰器把角色赋值给userRoles
const RoleDerorator = (roles: string[]) => (target: any, key: string) => {
roles.forEach((role) => userRoles.push(role));
};
// 根据userRoles生成Roles对象并赋值给类原型的roles属性
const SetRoleDerorator = <
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));
}
};
};
@SetRoleDerorator
class UserEntity {
@RoleDerorator(['admin', 'user'])
roles: string[] = [];
}
export const exp5 = () => {
console.log();
console.log(
'-----------------------示例5:属性装饰器-----------------------',
);
console.log(
'-----------------------使用装饰器根据权限过滤用户列表-----------------------',
);
console.log();
const user = new UserEntity();
console.log(user.roles);
console.log();
console.log('-----------------------示例5:执行完毕-----------------------');
};
// 控制台输出 [ { name: 'admin', desc: '管理员' }, { name: 'user', desc: '普通用户' } ]
方法装饰器
在一开始我们介绍了装饰器的原理,其实这就是方法装饰器的原始实现。与属性装饰器不同的是,方法装饰器接受三个参数
方法装饰器重载的时候需要注意的一点是定义 value 务必使用 function,而不是箭头函数,因为我们在调用原始的旧方法使用会使用到 this,如:method.apply(this, args),这里的 this 指向需要 function 来定义,具体原因可参考我的另一篇文章apply,bind,call 使用