简介
- 当我们想对类的方法、属性、参数做某些操作,可以理解为增加的逻辑,比如日志、鉴权,但是不希望侵入原逻辑,这时可以用到装饰器。
- 装饰器:在TypeScript中,装饰器是一种特殊的类型声明,它可以放在类、方法、访问器、属性、参数上,可以修改类的行为。
- 作用:把类、方法的逻辑和增加的逻辑(如日志、鉴权等)进行分离、解耦。
- 类型:类装饰器、方法装饰器、访问器装饰器、属性装饰器、参数装饰器。
类装饰器
- 定义:用于类的构造函数,可以用于修改类的定义。
- 签名:
(constructor:Function):void
- 参数:
日志记录
- 期望上报日志,查看哪些类被示例化。
- 但是不想修改构造函数,此时可以用日志记录装饰器来记录日志。
function createClassLog<T extends { new(...args: any[]): {} }>(name:string) {
return function (constructor: T) {
return class extends constructor {
constructor(...args: any[]) {
super(...args);
console.log(`创建类${name}的实例`);
}
}
}
}
@createClassLog('Person')
class Person {
constructor(public name: string) { }
}
new Person('小明');
默认值
function defaultValues(defaults: { [x: string]: any }) {
return function <T extends { new(...args: any[]) }>(constructor: T) {
return class extends constructor {
constructor(...args) {
super(...args);
Object.keys(defaults).forEach(key => {
if (this[key] === undefined) {
this[key] = defaults[key];
}
})
}
}
}
}
@defaultValues({
age: 10,
hobby: 'basketball'
})
class Student {
age: number;
hobby: string;
}
const s = new Student();
console.log(s.age, s.hobby);
方法装饰器
- 定义:应用于方法,可以用于修改方法的行为。可以用于修改方法的行为、添加元数据、进行日志记录、权限检查等。
- 签名:
(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) => void | PropertyDescriptor
- 参数:
- target:装饰的目标对象。对于静态方法来说是类的构造函数,对于实例方法来说是类的原型对象。
- propertyKey:修饰的成员描述。
- descriptor:方法的属性描述符。
日志记录
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyKey} with arguments:${args}`);
const result = originalMethod.apply(this, args);
console.log(`Result:${result}`);
return result;
}
return descriptor;
}
class Calculator {
@log
static add(a: number, b: number): number {
return a + b;
}
@log
add(a: number, b: number): number {
return a + b;
}
}
const calc = new Calculator();
calc.add(1, 2);
Calculator.add(3, 4);
鉴权
- 下面是查询公司收入的示例,该操作只有老板可以查询。
- 不希望把鉴权的代码与原逻辑耦合,此时可以用鉴权装饰器。
- 实际开发时,用户信息可以从请求头里获取,示例中用
userId
代替。
function authorize(users: string[]) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
if (!users.includes(args[0])) {
console.log("User is not authorized to call this method");
return;
}
return originalMethod.apply(this, args);
}
return descriptor;
}
}
class FinanceService {
@authorize(['admin'])
getRevenue(userId: string) {
console.log(`Revenue is 1000`);
}
}
const userService = new FinanceService();
userService.getRevenue('admin');
userService.getRevenue('123');
缓存
- 有些计算特别费时,可以把计算结果放在缓存中,下次计算时取出来。
function cache(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const cacheMap = new Map<string, any>();
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const key = JSON.stringify(args);
if (cacheMap.has(key)) {
console.log('从缓存中取数据');
return cacheMap.get(key);
}
const result = originalMethod.apply(this, args);
cacheMap.set(key, result);
return result;
}
return descriptor;
}
class MathOperations {
@cache
factorial(n: number): number {
const start = Date.now();
while (Date.now() < start + 5000);
return n;
}
}
const mathOps = new MathOperations();
console.time('mathOps.factorial(5)');
console.log(mathOps.factorial(5));
console.timeEnd('mathOps.factorial(5)');
console.time('mathOps.factorial(5)');
console.log(mathOps.factorial(5));
console.timeEnd('mathOps.factorial(5)');
访问器装饰器
- 定义:用于装饰类的访问属性(getter和setter)。访问装饰器可以用于修改或替换访问器的行为,添加元数据,进行日志记录等。
- 签名:
(target:Object,propertyKey:string|symbol,descriptor:PropertyDescriptor)=>void|PropertyDescriptor
- 参数:
- target:装饰器的目标对象。对于静态成员来说是类的构造函数,对于实例成员来说是类的原型对象。
- propertyKey:访问器的名称。
- descriptor:访问器的属性描述符。
- 访问器装饰器和方法装饰器类似,不过多介绍。
日志记录
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originGet = descriptor.get, originSet = descriptor.set;
if (originGet) {
descriptor.get = function () {
const result = originGet.apply(this);
console.log(`Getting value of ${propertyKey}:${result}`);
return result;
};
}
if (originSet) {
descriptor.set = function (...args: any[]) {
console.log(`Setting value of ${propertyKey}:${args}`);
originSet.apply(this, args);
};
}
return descriptor;
}
class User {
private _name: string;
constructor(name: string) {
this._name = name;
}
@log
get name() {
return this._name;
}
set name(value: string) {
this._name = value;
}
}
const user = new User("Alice");
console.log(user.name);
user.name = "Bob";
console.log(user.name);
权限控制
function adminOnly(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalGet = descriptor.get;
descriptor.get = function () {
const user = { role: 'admin' };
if (user.role !== 'admin') {
throw new Error("Access denied");
}
return originalGet.apply(this);
}
}
class SecureData {
private _secret: string = "top secret";
@adminOnly
get secret() {
return this._secret;
}
}
const data = new SecureData();
try {
console.log(data.secret);
} catch (error) {
console.log(error.message);
}
属性装饰器
- 定义:用于修饰类的属性。属性装饰器用于添加元数据或进行属性初始化等操作,不能直接修改属性的值或属性描述符。
- 签名:
(target:Object,propertyKey:string|symbol)=>void
- 参数:
- target:装饰的目标对象。对于静态属性来说是类的构造函数,对于实例属性来说是类的原型对象。
- propertyKey:装饰的属性名称。
- 元数据即变量的描述信息。添加元数据,用到了
reflect-metadata
,可点击查阅,本文不介绍。
校验必填项
- 类的属性是必填项,需要校验实例的属性是否有值。
- 可以用属性装饰器给该属性添加元数据,表示必填。
- 校验时,从实例上获取必填属性,判断是否有值。
import "reflect-metadata";
function required(target: any, propertyKey: string) {
Reflect.defineMetadata("required", true, target, propertyKey);
}
class Student {
@required
username: string;
age:number;
}
function validate(student: Student) {
for (const key in student) {
if (Reflect.getMetadata("required", student, key) && !student[key]) {
throw new Error(`Property ${key} is required`);
}
}
}
const student = new Student();
student.username = "";
try {
validate(student);
} catch (error) {
console.log(error.message);
}
设置初始值
- 使用属性访问器定义属性的初始值。
- 注意:如果tsconfig.json的配置项目是
"target": "ESNext"
,则不支持这种方式。
function defaultValue(value: string) {
return function (target: any, propertyKey: string) {
let val = value;
const getter = function () {
return val;
};
const setter = function (newValue) {
val = newValue;
};
Object.defineProperty(target, propertyKey, {
enumerable: true,
configurable: true,
get: getter,
set: setter,
});
}
}
class Settings {
@defaultValue("dark")
theme: string;
}
const s = new Settings();
console.log(s.theme);
参数装饰器
- 定义:用于修饰类构造函数或方法的参数。主要用于为参数添加元数据,以便在运行时能够获取元数据并进行处理。不能直接修改参数的行为或值。
- 签名:
(target:Object,propertyKey:string|symbol,parameterIndex:number)=>void
- 参数:
- target:装饰器的目标对象。对于静态属性来说是类的构造函数,对于实例属性来说是类的原型对象。
- propertyKey:参数所属的方法的名称。
- parameterIndex:参数在参数列表中的索引。
参数验证
- 校验参数比填,或者其他验证规则。
- 可以给参数增加元数据,表示该参数符合某种验证规则。
- 结合方法装饰器,重写类的方法。调用该方法时,验证参数的合法性。
import "reflect-metadata";
function validate(target: any, propertyKey: string, parameterIndex: number) {
const requiredParameters: number[] = Reflect.getOwnMetadata("requiredParameters", target, propertyKey) || [];
requiredParameters.push(parameterIndex);
Reflect.defineMetadata("requiredParameters", requiredParameters, target, propertyKey);
}
function validateParameters(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const method = descriptor.value;
descriptor.value = function (...args: any[]) {
const requiredParameters: number[] = Reflect.getOwnMetadata("requiredParameters", target, propertyKey) || [];
for (const parameterIndex of requiredParameters) {
if (args[parameterIndex] === undefined) {
throw new Error(`Missing required argument at position ${parameterIndex}`);
}
}
return method.apply(this, args);
}
}
class Student {
constructor(private name: string) { }
@validateParameters
setName(@validate newName: string) {
this.name = newName;
}
}
const student = new Student('Bob');
student.setName("Ada");
try {
student.setName(undefined);
} catch (error) {
console.log(error.message);
}
执行顺序
- 属性装饰器、方法装饰器、访问器装饰器:按照出现的顺序,从上到下执行。
- 参数装饰器:在执行方法装饰器之前,按照参数的位置从右向左执行;同一个参数的多个装饰器,也是从右向左执行。
- 类装饰器:最后执行。
function classDecorator() {
return function (constructor: Function) {
console.log('Class decorator');
};
}
function methodDecorator() {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log('Method decorator');
};
}
function accessorDecorator() {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log('Accessor decorator');
};
}
function propertyDecorator() {
return function (target: any, propertyKey: string) {
console.log('Property decorator');
};
}
function parameterDecorator() {
return function (target: any, propertyKey: string, parameterIndex: number) {
console.log('Parameter decorator');
};
}
@classDecorator()
class Example {
@propertyDecorator()
prop: string;
@accessorDecorator()
get myProp() {
return this.prop;
}
@methodDecorator()
method(@parameterDecorator() param: any) {
console.log('Method execution');
}
}
function parameter1Decorator1() {
return function (target: any, propertyKey: string, parameterIndex: number) {
console.log('parameter1Decorator1');
};
}
function parameter1Decorator2() {
return function (target: any, propertyKey: string, parameterIndex: number) {
console.log('parameter1Decorator2');
};
}
function parameter2Decorator1() {
return function (target: any, propertyKey: string, parameterIndex: number) {
console.log('parameter2Decorator1');
};
}
function parameter2Decorator2() {
return function (target: any, propertyKey: string, parameterIndex: number) {
console.log('parameter2Decorator2');
};
}
class Example {
method(
@parameter1Decorator1() @parameter1Decorator2() param1,
@parameter2Decorator1() @parameter2Decorator2() param2
) { }
}
总结
- 装饰器分为:类装饰器、方法装饰器、访问器装饰器、属性装饰器、参数装饰器。
- 装饰器可以在不改变原代码逻辑的前提下,增加功能。
- 装饰器可以使代码解耦,提高代码的可维护性、可扩展性。