Decorators are used to add some auxiliary functions to classes, methods, properties, and method parameters without affecting their original characteristics. Its main role in Typescript applications is similar to annotations in Java, and it is very useful in AOP (Aspect Oriented Programming) usage scenarios.
面向切面编程(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:执行完毕-----------------------');
};
了解了以上概念,接下去让我们学习真正的装饰器。
装饰器类型
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;
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:执行完毕-----------------------');
};
装饰器工厂
上面的方法我们为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:执行完毕-----------------------');
};
其它用法
我们还可以对类原型链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;
return constructor as typeof Original;
};
interface StaticUser {
new (): UserService;
myinfo: string;
}
@ProfileDecorator({ phone: 133, address: 'zhejiang' })
class ProfileService {}
const exp4 = () => {
console.log();
console.log(
'-----------------------示例4:修类的构造函数,原型属性,静态属性等-----------------------',
);
console.log(
'-----------------------设置原型属性值,重载构造防反,添加静态属性-----------------------',
);
console.log();
const profile = new ProfileService();
console.log((profile as any).userinfo);
console.log();
console.log('-----------------------示例4:执行完毕-----------------------');
};
属性装饰器
属性装饰器一般不单独使用,主要用于配合类或方法装饰器进行组合装饰
属性装饰器函数有两个参数:
target
对于普通属性,target 就是当前对象的原型,也就是说,假设 Employee 是对象,那么 target 就是 Employee.prototype
对于静态属性,target 就是当前对象的类
propertyKey
属性的名称
使用示例
const userRoles: string[] = [];
const RoleDerorator = (roles: string[]) => (target: any, key: string) => {
roles.forEach((role) => userRoles.push(role));
};
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:执行完毕-----------------------');
};
方法装饰器
在一开始我们介绍了装饰器的原理,其实这就是方法装饰器的原始实现。与属性装饰器不同的是,方法装饰器接受三个参数
方法装饰器重载的时候需要注意的一点是定义 value 务必使用 function,而不是箭头函数,因为我们在调用原始的旧方法使用会使用到 this,如:method.apply(this, args),这里的 this 指向需要 function 来定义,具体原因可参考我的另一篇文章apply,bind,call 使用
target
对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
key
方法名称
descriptor: PropertyDescriptor
方法的属性描述符(最重要的参数)
属性描述符
属性描述包含以下几个属性
-
configurable?: boolean; // 能否使用 delete、能否修改方法特性或修改访问器属性
-
enumerable?: boolean; 是否在遍历对象的时候存在
-
value?: any; 用于定义新的方法代替旧方法
-
writable?: boolean; 是否可写
-
get?(): any; // 访问器
-
set?(v: any): void; // 访问器
接下来我们使用方法装饰器修改一开始的装饰器原理中的登录日志记录器
const loggerDecorator = () => {
return function logMethod(
target: any,
propertyName: string,
propertyDescriptor: PropertyDescriptor,
): PropertyDescriptor {
const method = propertyDescriptor.value;
propertyDescriptor.value = function async(...args: any[]) {
try {
return method.apply(this, args);
} finally {
const now = new Date().valueOf();
console.log(`lasted logged in ${now}`);
}
};
return propertyDescriptor;
};
};
class UserService {
@loggerDecorator()
async login() {
console.log('login success');
await new Promise((resolve) => {
setTimeout(resolve, 100);
});
}
}
export const exp6 = () => {
console.log();
console.log(
'-----------------------示例6:方法装饰器-----------------------',
);
console.log(
'-----------------------使用装饰器重写示例1-----------------------',
);
console.log();
const user = new UserService();
user.login();
console.log();
console.log('-----------------------示例6:执行完毕-----------------------');
};
参数装饰器
一个类中每个方法的参数也可以有自己的装饰器。
与属性装饰器类似,参数装饰器一般不单独使用,而是配合类或方法装饰器组合使用
-
target: 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
-
key:方法名称
-
index: 参数数组中的位置
比如我们需要格式化一个方法的参数,那么可以创建一个转么用于格式化的装饰器
const parseConf: ((...args: any[]) => any)[] = [];
export const parse =
(parseTo: (...args: any[]) => any) =>
(target: any, propertyName: string, index: number) => {
parseConf[index] = parseTo;
};
export const parseDecorator = (
target: any,
propertyName: string,
descriptor: PropertyDescriptor,
): PropertyDescriptor => {
console.log('开始格式化数据');
return {
...descriptor,
value(...args: any[]) {
const newArgs = args.map((v, i) =>
parseConf[i] ? parseConf[i](v) : v,
);
console.log('格式化完毕');
return descriptor.value.apply(this, newArgs);
},
};
};
export interface UserType {
id: number;
username: string;
}
class UserService {
private users: UserType[] = [
{ id: 1, username: 'admin' },
{ id: 2, username: 'pincman' },
];
getUsers() {
return this.users;
}
@parseDecorator
delete(@parse((arg: any) => Number(arg)) id: number) {
this.users = this.users.filter((userObj) => userObj.id !== id);
return this;
}
}
export const exp78 = () => {
console.log();
console.log(
'-----------------------示例7:参数装饰器-----------------------',
);
console.log('-----------------------格式化参数-----------------------');
console.log();
const userService = new UserService();
userService.delete(1);
console.log(userService.getUsers());
console.log();
console.log('-----------------------示例7:执行完毕-----------------------');
};
访问器装饰器
访问器其实只是那些添加了get
,set
前缀的方法,用于使用调用属性的方式获取和设置一些属性的方法,类似于 PHP 中的魔术方法__get
,__set
。其装饰器使用方法与普通方法并无差异,只是在获取值的时候是调用描述符的get
和set
来替代value
而已。
例如,我们添加一个nickname字段,给设置nickname添加一个自定义前缀,并禁止在遍历user对象时出现nickname的值,添加一个fullname字段,在设置nickname时添加一个字符串后缀生成。
export const HiddenDecorator = () => {
return (
target: any,
propertyName: string,
descriptor: PropertyDescriptor,
) => {
descriptor.enumerable = false;
};
};
export const PrefixDecorator = (prefix: string) => {
return (
target: any,
propertyName: string,
descriptor: PropertyDescriptor,
) => {
return {
...descriptor,
set(value: string) {
descriptor.set.apply(this, [`${prefix}_${value}`]);
},
};
};
};
export class UserEntity {
private _nickname: string;
private fullname: string;
@HiddenDecorator()
@PrefixDecorator('jesse_')
get nickname() {
return this._nickname;
}
set nickname(value: string) {
this._nickname = value;
this.fullname = `${value}_fullname`;
}
}
export const exp78 = () => {
console.log();
console.log(
'-----------------------示例8:get/set装饰器-----------------------',
);
console.log(
'-----------------------禁止nickname出现在遍历中,为nickname添加前缀-----------------------',
);
console.log();
const user = new UserEntity();
user.nickname = 'pincman';
console.log(user);
console.log(user.nickname);
console.log();
console.log('-----------------------示例8:执行完毕-----------------------');
};
装饰器写法
通过装饰器重载方法有许多写法,可以根据自己的喜好来,以下举例几种
继承法
一般用于类装饰器中添加属性或方法,例如:
return <T extends new (...args: any[]) => any>(target: T) => {
return class extends target {
getMyName() {
return this._name;
}
};
};
原型法
一般用于类装饰器上重载构造函数以及添加属性或方法,例如:
const ProfileDerorator = (profile: UserProfile) => {
return (target: any) => {
const original = target;
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;
};
};
赋值法
一般用于方法装饰器上修改某个描述符,例如
const loggerDecorator = () => {
return function logMethod(
target: Object,
propertyName: string,
propertyDescriptor: PropertyDescriptor,
): PropertyDescriptor {
const method = propertyDescriptor.value;
propertyDescriptor.value = function async (...args: any[]) {...};
return propertyDescriptor;
};
};
展开法
与赋值法类似,只不过使用 ES6+的展开语法,更容易理解和使用,例如
const parseFunc = (
target: Object,
propertyName: string,
descriptor: PropertyDescriptor,
): PropertyDescriptor => {
return {
...descriptor,
value(...args: any[]) {
const newArgs = parseConf.map((toParse, index) => toParse(args[index]));
return descriptor.value.apply(this, newArgs);
},
};
};
元信息反射 API
元信息反射 API (例如 Reflect
)能够用来以标准方式组织元信息。而装饰器中的元信息反射使用非常简单,外观上仅仅可以看做在类的某个方法上附加一些随时可以获取的信息而已。
使用之前我们必须先安装reflect-metadata
这个库
npm i reflect-metadata --save
并且在tsconfig.json
中启用原信息配置
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
基本使用
我们看一下 TS 官方的示例是如何通过反射 API 获取属性设计阶段的类型信息的。
需要注意的是目前预定义的元信息只有三种
import 'reflect-metadata';
import { parse, parseDecorator, UserType } from './exp7-8';
class Point {
x: number;
y: number;
}
class Line {
private _p0: Point;
private _p1: Point;
@validate
set p0(value: Point) {
this._p0 = value;
}
get p0() {
return this._p0;
}
@validate
set p1(value: Point) {
this._p1 = value;
}
get p1() {
return this._p1;
}
}
function validate<T>(
target: any,
propertyKey: string,
descriptor: TypedPropertyDescriptor<T>,
) {
const { set } = descriptor;
descriptor.set = function (value: T) {
const type = Reflect.getMetadata('design:type', target, propertyKey);
if (!(value instanceof type)) {
throw new TypeError('Invalid type.');
}
set.apply(this, [value]);
};
return descriptor;
}
export const exp910 = () => {
console.log();
console.log(
'-----------------------示例9:基本元元素类型反射-----------------------',
);
console.log(
'-----------------------为访问器的set方法添加类型验证-----------------------',
);
console.log();
const line = new Line();
const p0 = new Point();
p0.x = 1;
p0.y = 2;
line.p1 = p0;
console.log(line);
console.log();
console.log('-----------------------示例9:执行完毕-----------------------');
};
自定义元信息
除了使用类似design:type
这种预定义的原信息外,我们也可以自定义信息,因为一般我们都是用reflect-metadata
来自定义原信息的。比如我们可以在删除用户的方法上添加一个角色判断,只有拥有我们设定角色的用户才能删除用户,比如管理员角色,具体可参考以下代码:
export const RoleGuardDecorator = (roles: string[]) => {
console.log('开始验证角色');
return function roleGuard(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor,
) {
Reflect.defineMetadata('roles', roles, target, propertyKey);
const method = descriptor.value;
descriptor.value = function (...args: any[]) {
const currentRoles = target.getRoles();
const needRoles = Reflect.getMetadata('roles', target, propertyKey);
for (const role of needRoles) {
if (!currentRoles.includes(role)) {
throw new Error(
`you have not permission to run ${propertyKey}`,
);
}
}
console.log('验证角色完毕');
return method.apply(this, args);
};
return descriptor;
};
};
export class UserService {
protected users: UserType[] = [
{ id: 1, username: 'admin' },
{ id: 2, username: 'pincman' },
];
getUsers() {
return this.users;
}
getRoles() {
return ['user'];
}
@RoleGuardDecorator(['admin'])
@parseDecorator
delete(@parse((arg: any) => Number(arg)) id: number): UserService {
this.users = this.getUsers().filter((userObj) => userObj.id !== id);
return this;
}
}
export const exp910 = () => {
console.log();
console.log(
'-----------------------示例10:自定义元元素反射-----------------------',
);
console.log(
'-----------------------添加角色守卫来判断当前用户是否有删除权限-----------------------',
);
console.log();
const user = new UserService();
user.delete(1);
console.log(user.getUsers());
console.log();
console.log(
'-----------------------示例10:执行完毕-----------------------',
);
};
组合与顺序
每一个属性,参数或方法都可以使用多组装饰器。每个类型的装饰器的调用顺序也是不同的。
组合使用
我们可以对任意一个被装饰者调用多组装饰器,多组装饰器一般书写在多行上(当然你也可以写在一行上,多行书写只不过是个约定俗成的惯例),比如
@RoleGuardDecorator
@parseDecorator
delete(@parse((arg: any) => Number(arg)) id): UserService
当多个装饰器应用于一个声明上,它们求值方式与高阶函数相似。在这个模型下,当复合*RoleGuardDecorator
和parseDecorator
时,复合的结果等同于RoleGuardDecorator
*(
parseDecorator
(
delete
))
。
同时,我们可以参考 react 中的高阶,原理相似
它们的调用步骤类似剥洋葱法,即:
-
由上至下依次对装饰器表达式求值。
-
求值的结果会被当作函数,由下至上依次调用。
比如
export const parseDecorator = () => {
console.log('开始格式化数据');
return (
target: Object,
propertyName: string,
descriptor: PropertyDescriptor,
): PropertyDescriptor => {
return {
...descriptor,
value(...args: any[]) {
const newArgs = parseConf.map((toParse, index) => toParse(args[index]));
console.log('格式化完毕');
return descriptor.value.apply(this, newArgs);
},
};
};
};
export const RoleGuardDecorator = (roles: string[]) => {
return function roleGuard(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor,
) {
console.log('开始验证角色');
...
descriptor.value = function(...args: any[]) {
...
console.log('验证角色完毕');
return method.apply(this, args);
};
return descriptor;
};
};
export class UserService {
...
@RoleGuardDecorator(['admin'])
@parseDecorator()
getRoles() {
return ['admin'];
}
}
调用顺序
特别需要注意的是getMetadata
与getOwneMetadata
的区别
每种类型的装饰器的调用顺序是不同的,具体顺序如下:
-
参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个实例成员(即类原型的成员)。
-
参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个静态成员。
-
参数装饰器应用到构造函数(即类原型)。
-
类装饰器应用到类。
例如:我们使用元信息结合方法和参数装饰器来验证参数的required,其调用顺序为参数装饰器->方法装饰器
import { UserType } from './exp7-8';
import { UserService as ParentUserService } from './exp9-10';
const requiredMetadataKey = Symbol('required');
export const RequiredDecorator = (
target: any,
propertyKey: string | symbol,
parameterIndex: number,
) => {
const existingRequiredParameters: number[] =
Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
existingRequiredParameters.push(parameterIndex);
Reflect.defineMetadata(
requiredMetadataKey,
existingRequiredParameters,
target,
propertyKey,
);
};
export const ValidateDecorator = (
target: any,
propertyName: string,
descriptor: TypedPropertyDescriptor<(...args: any[]) => any>,
) => {
const method = descriptor.value;
descriptor.value = function (...args: any[]) {
const requiredParameters: number[] = Reflect.getOwnMetadata(
requiredMetadataKey,
target,
propertyName,
);
if (requiredParameters) {
for (const parameterIndex of requiredParameters) {
if (
parameterIndex >= args.length ||
args[parameterIndex] === undefined
) {
throw new Error('Missing required argument.');
}
}
}
return method.apply(this, args);
};
};
class UserService extends ParentUserService {
@ValidateDecorator
createUser(@RequiredDecorator username?: string, id?: number) {
const ids: number[] = this.users.map((userEntity) => userEntity.id);
const newUser: UserType = {
id: id || Math.max(...ids) + 1,
username: username || Math.random().toString(36).substring(2, 15),
};
this.users.push(newUser);
return newUser;
}
}
export const exp11 = () => {
console.log();
console.log(
'-----------------------示例11:装饰器组合-----------------------',
);
console.log(
'-----------------------为username参数提供必填验证-----------------------',
);
console.log();
const user = new UserService();
user.createUser();
console.log(user.getUsers());
console.log();
console.log(
'-----------------------示例11:执行完毕-----------------------',
);
};