包教包会装饰器

34 阅读5分钟

简介

  • 当我们想对类的方法、属性、参数做某些操作,可以理解为增加的逻辑,比如日志、鉴权,但是不希望侵入原逻辑,这时可以用到装饰器。
  • 装饰器:在TypeScript中,装饰器是一种特殊的类型声明,它可以放在类、方法、访问器、属性、参数上,可以修改类的行为。
  • 作用:把类、方法的逻辑和增加的逻辑(如日志、鉴权等)进行分离、解耦。
  • 类型:类装饰器、方法装饰器、访问器装饰器、属性装饰器、参数装饰器。

类装饰器

  • 定义:用于类的构造函数,可以用于修改类的定义。
  • 签名:(constructor:Function):void
  • 参数:
    • constructor:类

日志记录

  • 期望上报日志,查看哪些类被示例化。
  • 但是不想修改构造函数,此时可以用日志记录装饰器来记录日志。
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('小明'); //创建类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);//10 basketball

方法装饰器

  • 定义:应用于方法,可以用于修改方法的行为。可以用于修改方法的行为、添加元数据、进行日志记录、权限检查等。
  • 签名:(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)');    //mathOps.factorial(5): 5.000s
console.time('mathOps.factorial(5)');
console.log(mathOps.factorial(5));         // 从缓存中获取结果
console.timeEnd('mathOps.factorial(5)');   //mathOps.factorial(5): 0.048ms

访问器装饰器

  • 定义:用于装饰类的访问属性(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);//Property username is required
}

设置初始值

  • 使用属性访问器定义属性的初始值。
  • 注意:如果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); //Missing required argument at position 0
}

执行顺序

  • 属性装饰器、方法装饰器、访问器装饰器:按照出现的顺序,从上到下执行。
  • 参数装饰器:在执行方法装饰器之前,按照参数的位置从右向左执行;同一个参数的多个装饰器,也是从右向左执行。
  • 类装饰器:最后执行。
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');
    }
}
/**
 *  Property decorator
    Accessor decorator
    Parameter decorator
    Method decorator
    Class decorator
 */
  • 参数装饰器
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
    ) { }
}
/**
 *  parameter2Decorator2
    parameter2Decorator1
    parameter1Decorator2
    parameter1Decorator1
 */

总结

  • 装饰器分为:类装饰器、方法装饰器、访问器装饰器、属性装饰器、参数装饰器。
  • 装饰器可以在不改变原代码逻辑的前提下,增加功能。
  • 装饰器可以使代码解耦,提高代码的可维护性、可扩展性。