MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

TypeScript装饰器进阶:组合装饰器的使用与技巧

2022-05-176.6k 阅读

什么是组合装饰器

在前端开发中,TypeScript 的装饰器为我们提供了一种元编程的能力,可以在不改变类或函数的核心逻辑的前提下,为它们添加额外的行为。组合装饰器则是将多个装饰器按照一定顺序应用到同一个目标上,以实现更为复杂和强大的功能。

想象一下,我们有一个简单的类 User,它可能有基本的属性和方法。现在我们希望对这个类添加一些额外的功能,比如日志记录、权限验证等。通过组合装饰器,我们可以像堆叠乐高积木一样,将不同功能的装饰器依次应用到 User 类上,而不需要在 User 类的代码内部进行大量修改。

组合装饰器的基本使用

在 TypeScript 中,组合装饰器的语法很直观。假设我们有两个装饰器 @Logger@Authorize,我们可以将它们组合起来应用到一个类上,如下所示:

// 定义 Logger 装饰器
function Logger(target: any) {
    console.log(`Class ${target.name} has been created.`);
}

// 定义 Authorize 装饰器
function Authorize(target: any) {
    console.log(`Class ${target.name} is authorized.`);
}

// 组合装饰器应用到类上
@Logger
@Authorize
class User {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
}

在上述代码中,@Logger@Authorize 两个装饰器依次作用于 User 类。当 User 类被定义时,首先会执行 @Logger 装饰器,打印出 Class User has been created.,接着执行 @Authorize 装饰器,打印出 Class User is authorized.

装饰器执行顺序

理解装饰器的执行顺序对于正确使用组合装饰器至关重要。当多个装饰器应用到一个目标上时,它们的执行顺序是从最靠近目标的装饰器开始,自下而上(从目标到装饰器定义的方向)执行。

以我们上面的例子来说,@AuthorizeUser 类更近,所以它会先执行,然后是 @Logger。如果我们有更多的装饰器,比如 @Logger@Authorize@Cache 按顺序应用到 User 类上,执行顺序就是 @Cache 最先执行,接着是 @Authorize,最后是 @Logger

// 定义 Cache 装饰器
function Cache(target: any) {
    console.log(`Class ${target.name} is cached.`);
}

// 组合多个装饰器应用到类上
@Logger
@Authorize
@Cache
class User {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
}

在这个例子中,控制台输出将依次是 Class User is cached.Class User is authorized.Class User has been created.

为类方法添加组合装饰器

除了类本身,我们还可以对类的方法应用组合装饰器,以实现对方法行为的增强。例如,我们可能希望在某个方法执行前进行权限验证,执行后进行日志记录。

// 定义 MethodLogger 装饰器
function MethodLogger(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
        console.log(`Method ${propertyKey} is about to execute.`);
        const result = originalMethod.apply(this, args);
        console.log(`Method ${propertyKey} has executed.`);
        return result;
    };
    return descriptor;
}

// 定义 MethodAuthorize 装饰器
function MethodAuthorize(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
        console.log(`Checking authorization for method ${propertyKey}`);
        // 这里可以添加实际的权限验证逻辑
        return originalMethod.apply(this, args);
    };
    return descriptor;
}

class Calculator {
    @MethodLogger
    @MethodAuthorize
    add(a: number, b: number) {
        return a + b;
    }
}

const calculator = new Calculator();
calculator.add(2, 3);

在上述代码中,@MethodAuthorize@MethodLogger 装饰器应用到 Calculator 类的 add 方法上。当调用 add 方法时,首先会执行 @MethodAuthorize 装饰器,打印出 Checking authorization for method add,接着执行 @MethodLogger 装饰器,打印出 Method add is about to execute.,然后执行原始的 add 方法,最后 @MethodLogger 再次打印 Method add has executed.

装饰器工厂函数与组合

装饰器工厂函数是一种特殊的装饰器定义方式,它返回一个实际的装饰器。这种方式在组合装饰器时非常有用,因为它可以为装饰器提供额外的配置参数。

例如,我们希望定义一个 @Timeout 装饰器,它可以设置方法执行的超时时间。

// 装饰器工厂函数
function Timeout(time: number) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        const originalMethod = descriptor.value;
        descriptor.value = function (...args: any[]) {
            setTimeout(() => {
                originalMethod.apply(this, args);
            }, time);
        };
        return descriptor;
    };
}

class DelayedTask {
    @Timeout(2000)
    task() {
        console.log('Task is executed after 2 seconds.');
    }
}

const delayedTask = new DelayedTask();
delayedTask.task();

在上述代码中,Timeout 是一个装饰器工厂函数,它接受一个 time 参数。通过 @Timeout(2000),我们为 DelayedTask 类的 task 方法设置了 2 秒的超时时间。

我们可以将这个 @Timeout 装饰器与其他装饰器组合使用。比如,我们可以在任务执行前进行日志记录。

// 定义 MethodLogger 装饰器
function MethodLogger(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
        console.log(`Method ${propertyKey} is about to execute.`);
        const result = originalMethod.apply(this, args);
        console.log(`Method ${propertyKey} has executed.`);
        return result;
    };
    return descriptor;
}

class DelayedTask {
    @MethodLogger
    @Timeout(2000)
    task() {
        console.log('Task is executed after 2 seconds.');
    }
}

const delayedTask = new DelayedTask();
delayedTask.task();

在这个组合中,首先会执行 @Timeout 装饰器,它会设置超时逻辑。然后 @MethodLogger 装饰器会在超时后执行的 task 方法前后打印日志。

组合装饰器在实际项目中的应用场景

  1. 日志与监控:在企业级应用中,对关键业务逻辑进行日志记录和监控是非常重要的。通过组合装饰器,我们可以在方法执行前后记录详细的日志信息,包括方法参数、执行时间、返回结果等。同时,可以结合监控工具,对方法的执行情况进行实时监控。

例如,在一个电商系统中,订单创建的方法可能涉及到多个步骤和复杂的业务逻辑。我们可以使用组合装饰器,在方法执行前记录输入参数,执行后记录订单创建是否成功以及耗时等信息。

// 定义 LogInput 装饰器
function LogInput(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
        console.log(`Method ${propertyKey} received input: ${JSON.stringify(args)}`);
        return originalMethod.apply(this, args);
    };
    return descriptor;
}

// 定义 LogOutput 装饰器
function LogOutput(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
        const result = originalMethod.apply(this, args);
        console.log(`Method ${propertyKey} returned output: ${JSON.stringify(result)}`);
        return result;
    };
    return descriptor;
}

// 定义 LogDuration 装饰器
function LogDuration(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
        const start = Date.now();
        const result = originalMethod.apply(this, args);
        const end = Date.now();
        console.log(`Method ${propertyKey} took ${end - start} ms to execute.`);
        return result;
    };
    return descriptor;
}

class OrderService {
    @LogInput
    @LogOutput
    @LogDuration
    createOrder(order: any) {
        // 实际的订单创建逻辑
        return { orderId: '12345', status: 'created' };
    }
}

const orderService = new OrderService();
orderService.createOrder({ product: 'iPhone', quantity: 1 });
  1. 权限控制与数据验证:在多用户的应用程序中,不同用户具有不同的权限。通过组合装饰器,我们可以在方法调用前进行权限验证,确保只有有权限的用户才能执行相应操作。同时,也可以对输入数据进行验证,保证数据的合法性。

例如,在一个后台管理系统中,只有管理员用户才能执行删除用户的操作。我们可以定义权限验证和数据验证的装饰器,并将它们组合应用到删除用户的方法上。

// 模拟用户权限信息
const currentUser = { role: 'admin' };

// 定义 Permission 装饰器
function Permission(requiredRole: string) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        const originalMethod = descriptor.value;
        descriptor.value = function (...args: any[]) {
            if (currentUser.role === requiredRole) {
                return originalMethod.apply(this, args);
            } else {
                throw new Error('Permission denied.');
            }
        };
        return descriptor;
    };
}

// 定义 ValidateUserInput 装饰器
function ValidateUserInput(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (userId: string) {
        if (typeof userId ==='string' && userId.length > 0) {
            return originalMethod.apply(this, [userId]);
        } else {
            throw new Error('Invalid user ID.');
        }
    };
    return descriptor;
}

class UserManagementService {
    @Permission('admin')
    @ValidateUserInput
    deleteUser(userId: string) {
        // 实际的删除用户逻辑
        console.log(`User ${userId} has been deleted.`);
    }
}

const userManagementService = new UserManagementService();
userManagementService.deleteUser('123');
  1. 缓存与性能优化:在一些频繁调用且计算成本较高的方法上,使用缓存可以显著提高性能。通过组合装饰器,我们可以在方法调用前检查缓存中是否已经存在结果,如果存在则直接返回缓存结果,否则执行方法并将结果缓存起来。

例如,在一个天气预报查询服务中,获取某个城市的天气预报可能需要调用外部 API,这个操作相对耗时。我们可以使用缓存装饰器来优化性能。

// 定义 CacheResult 装饰器
function CacheResult(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const cache = new Map();
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
        const key = args.toString();
        if (cache.has(key)) {
            return cache.get(key);
        }
        const result = originalMethod.apply(this, args);
        cache.set(key, result);
        return result;
    };
    return descriptor;
}

class WeatherService {
    @CacheResult
    getWeather(city: string) {
        // 实际的获取天气预报逻辑,这里简单模拟返回数据
        return `Weather in ${city} is sunny.`;
    }
}

const weatherService = new WeatherService();
console.log(weatherService.getWeather('New York'));
console.log(weatherService.getWeather('New York')); // 第二次调用将直接从缓存中获取结果

处理组合装饰器中的依赖关系

在实际应用中,组合装饰器可能存在依赖关系。例如,一个装饰器可能需要另一个装饰器提供的信息才能正确工作。在这种情况下,我们需要特别注意装饰器的执行顺序和数据传递。

假设我们有一个 @Transaction 装饰器,它用于管理数据库事务,还有一个 @LogTransaction 装饰器,它依赖于 @Transaction 装饰器执行过程中的事务信息来记录详细的日志。

// 定义 Transaction 装饰器
function Transaction(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
        let transactionInfo = { status: 'init', details: 'Transaction started' };
        try {
            // 这里模拟数据库事务操作
            console.log('Transaction is in progress.');
            const result = originalMethod.apply(this, args);
            transactionInfo.status = 'completed';
            transactionInfo.details = 'Transaction completed successfully';
            return result;
        } catch (error) {
            transactionInfo.status = 'failed';
            transactionInfo.details = `Transaction failed: ${error.message}`;
            throw error;
        } finally {
            // 这里可以进行事务的提交或回滚操作
            console.log('Transaction is either committed or rolled back.');
        }
    };
    return descriptor;
}

// 定义 LogTransaction 装饰器,依赖于 Transaction 装饰器的事务信息
function LogTransaction(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
        const originalExecute = () => originalMethod.apply(this, args);
        try {
            const result = originalExecute();
            console.log('Transaction log: Success.');
            return result;
        } catch (error) {
            console.log('Transaction log: Failure.');
            throw error;
        }
    };
    return descriptor;
}

class DatabaseService {
    @LogTransaction
    @Transaction
    updateData(data: any) {
        // 实际的数据库更新逻辑
        console.log('Data has been updated.');
    }
}

const databaseService = new DatabaseService();
databaseService.updateData({ key: 'value' });

在上述代码中,@Transaction 装饰器先执行,它管理整个事务过程并设置相关的事务信息。@LogTransaction 装饰器依赖于 @Transaction 装饰器执行过程中的事务状态来记录日志。注意,这里的执行顺序很关键,如果 @LogTransaction 先执行,它将无法获取到正确的事务信息。

组合装饰器与继承

当涉及到类的继承时,组合装饰器的行为可能会有些微妙。子类会继承父类的装饰器,但是如果子类重新定义了被装饰的方法,那么子类的装饰器会覆盖父类的装饰器。

// 定义 ParentLogger 装饰器
function ParentLogger(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
        console.log('Parent method is about to execute.');
        const result = originalMethod.apply(this, args);
        console.log('Parent method has executed.');
        return result;
    };
    return descriptor;
}

class ParentClass {
    @ParentLogger
    parentMethod() {
        console.log('This is the parent method.');
    }
}

// 定义 ChildLogger 装饰器
function ChildLogger(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
        console.log('Child method is about to execute.');
        const result = originalMethod.apply(this, args);
        console.log('Child method has executed.');
        return result;
    };
    return descriptor;
}

class ChildClass extends ParentClass {
    @ChildLogger
    parentMethod() {
        console.log('This is the overridden parent method in child class.');
    }
}

const child = new ChildClass();
child.parentMethod();

在上述代码中,ParentClassparentMethod@ParentLogger 装饰,而 ChildClass 重写了 parentMethod 并应用了 @ChildLogger 装饰器。当调用 child.parentMethod() 时,执行的是 @ChildLogger 装饰器的逻辑,而不是 @ParentLogger 的逻辑。

如果我们希望在子类中同时保留父类装饰器的功能,可以通过一些技巧来实现。例如,可以在子类的装饰器中手动调用父类的方法。

// 定义 ParentLogger 装饰器
function ParentLogger(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
        console.log('Parent method is about to execute.');
        const result = originalMethod.apply(this, args);
        console.log('Parent method has executed.');
        return result;
    };
    return descriptor;
}

class ParentClass {
    @ParentLogger
    parentMethod() {
        console.log('This is the parent method.');
    }
}

// 定义 ChildLogger 装饰器
function ChildLogger(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    const parentMethod = target.prototype.__proto__[propertyKey];
    descriptor.value = function (...args: any[]) {
        console.log('Child method is about to execute.');
        const parentResult = parentMethod.apply(this, args);
        const result = originalMethod.apply(this, args);
        console.log('Child method has executed.');
        return result;
    };
    return descriptor;
}

class ChildClass extends ParentClass {
    @ChildLogger
    parentMethod() {
        console.log('This is the overridden parent method in child class.');
    }
}

const child = new ChildClass();
child.parentMethod();

在这个改进的代码中,ChildLogger 装饰器通过 target.prototype.__proto__[propertyKey] 获取到父类的 parentMethod,并在执行子类的 parentMethod 前后,手动调用父类的 parentMethod,从而实现了同时保留父类和子类装饰器的功能。

组合装饰器的局限与注意事项

  1. 装饰器的兼容性:虽然 TypeScript 支持装饰器,但是它们在不同的运行环境(如不同版本的 Node.js 或浏览器)中的支持程度可能不同。在实际项目中,需要确保目标运行环境能够正确解析和执行装饰器代码。如果需要在不支持装饰器的环境中运行,可以考虑使用 Babel 等工具进行转译。
  2. 装饰器对性能的影响:尽管装饰器为代码带来了极大的便利性,但过多地使用装饰器,尤其是在性能敏感的代码路径上,可能会对性能产生一定的影响。每个装饰器都会在目标对象或方法上添加额外的逻辑,这可能会增加函数调用的开销。因此,在性能关键的代码区域,需要谨慎使用装饰器,并进行必要的性能测试。
  3. 调试的复杂性:组合装饰器使得代码逻辑在一定程度上变得更加复杂,这可能会给调试带来困难。当出现问题时,很难快速确定是哪个装饰器导致了错误。为了便于调试,可以在每个装饰器中添加详细的日志输出,记录装饰器的执行过程和关键变量的值。
  4. 装饰器的滥用:由于装饰器具有强大的功能,很容易出现滥用的情况。过度使用装饰器可能会导致代码难以理解和维护,使得代码的可读性和可维护性下降。在使用装饰器时,应该遵循适度原则,确保装饰器的使用是为了提高代码的可维护性和复用性,而不是增加代码的复杂性。

通过深入理解组合装饰器的使用与技巧,我们可以在前端开发中更加灵活和高效地运用 TypeScript 的装饰器功能,为我们的代码添加丰富的额外行为,同时保持代码的清晰和可维护性。在实际项目中,结合具体的业务需求,合理地选择和组合装饰器,将为项目的开发和维护带来极大的便利。