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

TypeScript装饰器应用:实际项目中的类装饰器实践

2023-12-211.8k 阅读

一、TypeScript 装饰器基础概念

在深入探讨实际项目中类装饰器的实践之前,我们先来回顾一下 TypeScript 装饰器的基本概念。装饰器是一种特殊类型的声明,它能够附加到类声明、方法、属性或参数上,为它们添加额外的行为或元数据。

TypeScript 中的装饰器本质上是一个函数,它接收目标对象(比如类、方法等)作为参数,并返回一个新的对象(或直接修改目标对象)。装饰器函数的定义形式如下:

function myDecorator(target: any) {
    // 在这里对目标对象进行操作
    return target;
}

1.1 类装饰器

类装饰器应用于类的定义,它的参数就是类的构造函数。类装饰器可以用来修改类的行为,比如添加新的属性或方法,或者修改类的原型。

function classDecorator(target: Function) {
    target.prototype.newMethod = function() {
        console.log('This is a new method added by the class decorator');
    };
    return target;
}

@classDecorator
class MyClass {
    // 类的原有内容
}

const myObj = new MyClass();
(myObj as any).newMethod(); // 输出: This is a new method added by the class decorator

在上述代码中,classDecorator 是一个类装饰器,它为 MyClass 的原型添加了一个新的方法 newMethod

1.2 方法装饰器

方法装饰器应用于类的方法,它接收三个参数:目标对象的原型、方法名以及描述符对象。描述符对象包含了方法的属性,如 value(方法的实际实现)、writable(是否可写)、enumerable(是否可枚举)和 configurable(是否可配置)。

function methodDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function(...args: any[]) {
        console.log('Before method execution');
        const result = originalMethod.apply(this, args);
        console.log('After method execution');
        return result;
    };
    return descriptor;
}

class MyMethodClass {
    @methodDecorator
    myMethod() {
        console.log('This is my method');
    }
}

const methodObj = new MyMethodClass();
methodObj.myMethod(); 
// 输出: 
// Before method execution
// This is my method
// After method execution

这里的 methodDecorator 方法装饰器在原方法执行前后添加了日志输出。

1.3 属性装饰器

属性装饰器应用于类的属性,它接收两个参数:目标对象的原型和属性名。属性装饰器可以用来添加属性的元数据,或者对属性的访问进行拦截。

function propertyDecorator(target: any, propertyKey: string) {
    let value: any;
    const getter = function() {
        return value;
    };
    const setter = function(newValue: any) {
        console.log('Setting property value:', newValue);
        value = newValue;
    };
    Object.defineProperty(target, propertyKey, {
        get: getter,
        set: setter,
        enumerable: true,
        configurable: true
    });
}

class MyPropertyClass {
    @propertyDecorator
    myProperty: string;
}

const propertyObj = new MyPropertyClass();
propertyObj.myProperty = 'Hello'; // 输出: Setting property value: Hello
console.log(propertyObj.myProperty); // 输出: Hello

在这个例子中,propertyDecorator 为属性 myProperty 添加了自定义的存取器,在设置属性值时输出日志。

1.4 参数装饰器

参数装饰器应用于类方法的参数,它接收三个参数:目标对象的原型、方法名以及参数在参数列表中的索引。参数装饰器通常用于记录方法调用时的参数信息。

function parameterDecorator(target: any, propertyKey: string, parameterIndex: number) {
    const method = target[propertyKey];
    target[propertyKey] = function(...args: any[]) {
        console.log(`Parameter at index ${parameterIndex} is:`, args[parameterIndex]);
        return method.apply(this, args);
    };
}

class MyParameterClass {
    myFunction(@parameterDecorator param: string) {
        console.log('Function called with parameter:', param);
    }
}

const parameterObj = new MyParameterClass();
parameterObj.myFunction('World'); 
// 输出: 
// Parameter at index 0 is: World
// Function called with parameter: World

parameterDecorator 在方法 myFunction 调用时,输出了指定索引位置的参数值。

二、实际项目中类装饰器的应用场景

2.1 日志记录

在实际项目中,日志记录是一个非常常见的需求。通过类装饰器,我们可以方便地为整个类的方法添加日志记录功能。

function logClassMethods(target: Function) {
    const originalPrototype = target.prototype;
    const methodNames = Object.getOwnPropertyNames(originalPrototype).filter(name => typeof originalPrototype[name] === 'function' && name!== 'constructor');
    methodNames.forEach(methodName => {
        const originalMethod = originalPrototype[methodName];
        originalPrototype[methodName] = function(...args: any[]) {
            console.log(`Calling method ${methodName} with arguments:`, args);
            const result = originalMethod.apply(this, args);
            console.log(`Method ${methodName} returned:`, result);
            return result;
        };
    });
    return target;
}

@logClassMethods
class MathCalculator {
    add(a: number, b: number) {
        return a + b;
    }

    subtract(a: number, b: number) {
        return a - b;
    }
}

const calculator = new MathCalculator();
console.log(calculator.add(2, 3)); 
// 输出: 
// Calling method add with arguments: [ 2, 3 ]
// Method add returned: 5
// 5

在上述代码中,logClassMethods 类装饰器遍历了 MathCalculator 类的所有方法,并在每个方法执行前后添加了日志记录,输出方法名、参数以及返回值。

2.2 权限控制

在企业级应用中,权限控制是至关重要的。我们可以使用类装饰器来实现对类实例方法调用的权限验证。

interface User {
    role: string;
}

function requireRole(role: string) {
    return function(target: Function) {
        const originalPrototype = target.prototype;
        const methodNames = Object.getOwnPropertyNames(originalPrototype).filter(name => typeof originalPrototype[name] === 'function' && name!== 'constructor');
        methodNames.forEach(methodName => {
            const originalMethod = originalPrototype[methodName];
            originalPrototype[methodName] = function(this: { user: User },...args: any[]) {
                if (this.user.role === role) {
                    return originalMethod.apply(this, args);
                } else {
                    throw new Error('Access denied. Insufficient permissions.');
                }
            };
        });
        return target;
    };
}

@requireRole('admin')
class AdminPanel {
    constructor(public user: User) {}

    deleteUser(userId: string) {
        console.log(`Deleting user with ID ${userId}`);
    }
}

const adminUser: User = { role: 'admin' };
const normalUser: User = { role: 'user' };

const adminPanel = new AdminPanel(adminUser);
adminPanel.deleteUser('123'); // 输出: Deleting user with ID 123

const normalPanel = new AdminPanel(normalUser);
try {
    normalPanel.deleteUser('456');
} catch (error) {
    console.error(error.message); // 输出: Access denied. Insufficient permissions.
}

在这个例子中,requireRole 装饰器接收一个角色名作为参数。它为 AdminPanel 类的所有方法添加了权限验证,只有当用户的角色与指定角色匹配时,方法才能被调用。

2.3 性能监控

性能监控对于优化应用程序性能非常关键。我们可以通过类装饰器来测量类方法的执行时间。

function measurePerformance(target: Function) {
    const originalPrototype = target.prototype;
    const methodNames = Object.getOwnPropertyNames(originalPrototype).filter(name => typeof originalPrototype[name] === 'function' && name!== 'constructor');
    methodNames.forEach(methodName => {
        const originalMethod = originalPrototype[methodName];
        originalPrototype[methodName] = function(...args: any[]) {
            const start = Date.now();
            const result = originalMethod.apply(this, args);
            const end = Date.now();
            console.log(`Method ${methodName} took ${end - start} ms to execute`);
            return result;
        };
    });
    return target;
}

@measurePerformance
class FileProcessor {
    processFile(filePath: string) {
        // 模拟文件处理操作
        for (let i = 0; i < 1000000; i++);
        return `File ${filePath} processed`;
    }
}

const fileProcessor = new FileProcessor();
console.log(fileProcessor.processFile('example.txt')); 
// 输出: 
// Method processFile took [执行时间] ms to execute
// File example.txt processed

measurePerformance 类装饰器为 FileProcessor 类的方法添加了性能测量逻辑,在方法执行前后记录时间,并输出方法的执行时长。

三、类装饰器在框架中的应用

3.1 在 Angular 中的应用

Angular 是一个流行的前端框架,它广泛使用装饰器来定义组件、服务等。虽然 Angular 本身对装饰器的使用更多是基于元数据的定义,但我们可以类比一些自定义装饰器的应用。例如,我们可以创建一个自定义的类装饰器来为 Angular 组件添加一些通用的行为。 假设我们有一个需求,为所有的 Angular 组件添加一个加载指示器。我们可以这样实现:

import { Component } from '@angular/core';

function withLoadingIndicator(target: Function) {
    const originalPrototype = target.prototype;
    const methodNames = Object.getOwnPropertyNames(originalPrototype).filter(name => typeof originalPrototype[name] === 'function' && name!== 'constructor');
    methodNames.forEach(methodName => {
        const originalMethod = originalPrototype[methodName];
        originalPrototype[methodName] = function(...args: any[]) {
            this.isLoading = true;
            const result = originalMethod.apply(this, args);
            this.isLoading = false;
            return result;
        };
    });
    return target;
}

@withLoadingIndicator
@Component({
    selector: 'app-example',
    templateUrl: './example.component.html',
    styleUrls: ['./example.component.css']
})
export class ExampleComponent {
    isLoading: boolean = false;

    fetchData() {
        // 模拟数据获取操作
        return 'Data fetched';
    }
}

example.component.html 中,我们可以根据 isLoading 的值来显示或隐藏加载指示器。这样,通过类装饰器,我们为 ExampleComponent 的所有方法添加了加载指示器的逻辑。

3.2 在 Vue 3 中的应用

在 Vue 3 中,虽然没有像 Angular 那样大规模地使用装饰器,但在组合式 API 中,我们可以利用 TypeScript 装饰器来实现一些特定的功能。例如,我们可以创建一个类装饰器来为 Vue 组件的生命周期钩子添加额外的逻辑。

import { defineComponent } from 'vue';

function lifecycleLogger(target: Function) {
    const originalPrototype = target.prototype;
    const lifecycleHooks = ['created', 'beforeMount', 'mounted', 'beforeUpdate', 'updated', 'beforeUnmount', 'unmounted'];
    lifecycleHooks.forEach(hook => {
        if (originalPrototype[hook]) {
            const originalHook = originalPrototype[hook];
            originalPrototype[hook] = function(...args: any[]) {
                console.log(`Entering ${hook}`);
                const result = originalHook.apply(this, args);
                console.log(`Exiting ${hook}`);
                return result;
            };
        }
    });
    return target;
}

@lifecycleLogger
export default defineComponent({
    name: 'MyVueComponent',
    created() {
        console.log('Component created');
    },
    mounted() {
        console.log('Component mounted');
    }
});

上述代码中,lifecycleLogger 类装饰器为 MyVueComponent 的生命周期钩子添加了日志记录,在进入和离开每个钩子时输出日志。

四、实现复杂的类装饰器模式

4.1 装饰器组合

在实际项目中,我们可能需要同时应用多个装饰器来实现复杂的功能。例如,我们可以将日志记录和权限控制装饰器组合使用。

interface User {
    role: string;
}

function requireRole(role: string) {
    return function(target: Function) {
        const originalPrototype = target.prototype;
        const methodNames = Object.getOwnPropertyNames(originalPrototype).filter(name => typeof originalPrototype[name] === 'function' && name!== 'constructor');
        methodNames.forEach(methodName => {
            const originalMethod = originalPrototype[methodName];
            originalPrototype[methodName] = function(this: { user: User },...args: any[]) {
                if (this.user.role === role) {
                    return originalMethod.apply(this, args);
                } else {
                    throw new Error('Access denied. Insufficient permissions.');
                }
            };
        });
        return target;
    };
}

function logClassMethods(target: Function) {
    const originalPrototype = target.prototype;
    const methodNames = Object.getOwnPropertyNames(originalPrototype).filter(name => typeof originalPrototype[name] === 'function' && name!== 'constructor');
    methodNames.forEach(methodName => {
        const originalMethod = originalPrototype[methodName];
        originalPrototype[methodName] = function(...args: any[]) {
            console.log(`Calling method ${methodName} with arguments:`, args);
            const result = originalMethod.apply(this, args);
            console.log(`Method ${methodName} returned:`, result);
            return result;
        };
    });
    return target;
}

@requireRole('admin')
@logClassMethods
class AdminService {
    constructor(public user: User) {}

    deleteUser(userId: string) {
        console.log(`Deleting user with ID ${userId}`);
    }
}

const adminUser: User = { role: 'admin' };
const normalUser: User = { role: 'user' };

const adminService = new AdminService(adminUser);
adminService.deleteUser('123'); 
// 输出: 
// Calling method deleteUser with arguments: [ '123' ]
// Deleting user with ID 123
// Method deleteUser returned: undefined

const normalService = new AdminService(normalUser);
try {
    normalService.deleteUser('456');
} catch (error) {
    console.error(error.message); // 输出: Access denied. Insufficient permissions.
}

在这个例子中,AdminService 类同时应用了 requireRolelogClassMethods 装饰器。首先进行权限验证,只有权限通过后才会执行日志记录的逻辑。

4.2 装饰器工厂模式

装饰器工厂模式允许我们创建参数化的装饰器。例如,我们可以创建一个装饰器工厂,根据不同的配置来决定是否启用日志记录。

function logClassMethodsFactory(enableLogging: boolean) {
    return function(target: Function) {
        if (!enableLogging) {
            return target;
        }
        const originalPrototype = target.prototype;
        const methodNames = Object.getOwnPropertyNames(originalPrototype).filter(name => typeof originalPrototype[name] === 'function' && name!== 'constructor');
        methodNames.forEach(methodName => {
            const originalMethod = originalPrototype[methodName];
            originalPrototype[methodName] = function(...args: any[]) {
                console.log(`Calling method ${methodName} with arguments:`, args);
                const result = originalMethod.apply(this, args);
                console.log(`Method ${methodName} returned:`, result);
                return result;
            };
        });
        return target;
    };
}

@logClassMethodsFactory(true)
class EnabledLoggingClass {
    myMethod() {
        return 'Method result';
    }
}

@logClassMethodsFactory(false)
class DisabledLoggingClass {
    myMethod() {
        return 'Method result';
    }
}

const enabledObj = new EnabledLoggingClass();
enabledObj.myMethod(); 
// 输出: 
// Calling method myMethod with arguments: []
// Method myMethod returned: Method result

const disabledObj = new DisabledLoggingClass();
disabledObj.myMethod(); 
// 没有日志输出

在上述代码中,logClassMethodsFactory 是一个装饰器工厂,它接收一个布尔值 enableLogging。根据这个值,决定是否为类的方法添加日志记录功能。

五、类装饰器实践中的注意事项

5.1 装饰器的执行顺序

当多个装饰器应用于一个类时,装饰器的执行顺序是从下往上的。例如:

function decorator1(target: Function) {
    console.log('Decorator 1 executed');
    return target;
}

function decorator2(target: Function) {
    console.log('Decorator 2 executed');
    return target;
}

@decorator1
@decorator2
class MyClass {
    // 类的内容
}

在上述代码中,会先输出 Decorator 2 executed,然后输出 Decorator 1 executed。理解这个执行顺序对于正确实现复杂的装饰器逻辑非常重要。

5.2 与 ES6 类继承的兼容性

在使用类装饰器时,需要注意与 ES6 类继承的兼容性。当一个被装饰的类被继承时,装饰器的行为可能会受到影响。例如,如果一个类装饰器修改了类的原型,子类可能会继承这些修改,但也可能会出现一些意外的行为。

function classDecorator(target: Function) {
    target.prototype.newMethod = function() {
        console.log('This is a new method added by the class decorator');
    };
    return target;
}

@classDecorator
class ParentClass {
    // 类的原有内容
}

class ChildClass extends ParentClass {
    // 子类内容
}

const childObj = new ChildClass();
(childObj as any).newMethod(); 
// 输出: This is a new method added by the class decorator

在这个例子中,子类 ChildClass 继承了父类 ParentClass 被装饰后添加的 newMethod。但如果装饰器的逻辑更为复杂,比如修改了类的构造函数等,可能会导致子类出现兼容性问题。

5.3 装饰器的性能影响

虽然装饰器为代码带来了很大的灵活性,但过度使用装饰器可能会对性能产生一定的影响。尤其是在一些性能敏感的场景中,如频繁调用的方法上添加复杂的装饰器逻辑,可能会增加方法的执行时间。因此,在使用装饰器时,需要权衡功能的便利性和性能的影响。

在实际项目中,我们可以通过性能测试工具来评估装饰器对应用程序性能的具体影响,并根据测试结果进行优化。例如,可以使用 jest 结合 benchmark 库来进行性能测试。

import Benchmark from 'benchmark';

function simpleMethod() {
    // 简单的方法逻辑
    return 1 + 1;
}

function decoratedSimpleMethod() {
    console.log('Before method');
    const result = simpleMethod();
    console.log('After method');
    return result;
}

const suite = new Benchmark.Suite;

suite
  .add('Simple method', simpleMethod)
  .add('Decorated simple method', decoratedSimpleMethod)
  .on('cycle', function(event: any) {
        console.log(String(event.target));
    })
  .on('complete', function(this: any) {
        console.log('Fastest is'+ this.filter('fastest').map('name'));
    })
  .run({ 'async': true });

通过上述代码,可以比较普通方法和添加了装饰器逻辑的方法的执行性能,从而为是否使用装饰器以及如何优化装饰器逻辑提供依据。

通过以上内容,我们详细探讨了 TypeScript 类装饰器在实际项目中的各种应用场景、实现模式以及注意事项。类装饰器为我们提供了一种强大的代码增强方式,合理地运用它可以使我们的代码更加简洁、可维护和可扩展。在实际开发中,需要根据项目的具体需求和场景,谨慎地选择和实现类装饰器,以达到最佳的开发效果。