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

TypeScript装饰器实战:类装饰器的高级应用

2021-03-293.9k 阅读

TypeScript装饰器基础回顾

在深入探讨类装饰器的高级应用之前,我们先来简单回顾一下TypeScript装饰器的基础知识。装饰器是一种特殊类型的声明,它能够附加到类声明、方法、访问器、属性或参数上,为它们添加额外的行为或元数据。

装饰器工厂函数

装饰器本质上是一个函数,它可以接受参数并返回一个实际的装饰器函数,这种形式被称为装饰器工厂函数。例如,我们定义一个简单的日志装饰器工厂函数:

function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function(...args: any[]) {
        console.log(`调用方法 ${propertyKey},参数为:`, args);
        const result = originalMethod.apply(this, args);
        console.log(`方法 ${propertyKey} 返回结果:`, result);
        return result;
    };
    return descriptor;
}
class ExampleClass {
    @log
    add(a: number, b: number) {
        return a + b;
    }
}
const example = new ExampleClass();
example.add(2, 3);

在上述代码中,log 装饰器在方法调用前后打印日志信息,从而增强了方法的行为。

类装饰器的基本使用

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

简单示例

function classDecorator<T extends { new(...args: any[]): {} }>(constructor: T) {
    return class extends constructor {
        newProperty = "新属性";
        log() {
            console.log("这是一个新方法");
        }
    };
}
@classDecorator
class MyClass { }
const myInstance = new MyClass();
console.log(myInstance.newProperty); 
myInstance.log(); 

在这个例子中,classDecorator 装饰器接收类的构造函数作为参数,并返回一个新的类,这个新类继承自原始类,并且添加了新的属性 newProperty 和方法 log

类装饰器的高级应用 - 依赖注入

依赖注入(Dependency Injection,简称DI)是一种软件设计模式,它允许我们将依赖对象传递给一个类,而不是在类内部创建它们。类装饰器在实现依赖注入方面非常有用。

依赖注入的实现原理

假设我们有一个 UserService 类,它依赖于 HttpClient 类来进行网络请求。传统方式下,UserService 可能会在内部实例化 HttpClient。但是通过依赖注入,我们可以将 HttpClient 作为参数传递给 UserService

class HttpClient {
    get(url: string) {
        console.log(`从 ${url} 获取数据`);
    }
}
class UserService {
    constructor(private http: HttpClient) { }
    getUser() {
        this.http.get('/user');
    }
}

现在,我们使用类装饰器来自动注入依赖。

使用类装饰器实现依赖注入

interface Injectable {
    provide: string;
}
function Inject(target: any) {
    const providers: { [key: string]: any } = {};
    return function <T extends { new(...args: any[]): {} }>(constructor: T) {
        return class extends constructor {
            constructor(...args: any[]) {
                const resolvedArgs: any[] = [];
                for (const arg of Reflect.getMetadata('design:paramtypes', constructor) || []) {
                    const provide = (arg as Injectable).provide;
                    resolvedArgs.push(providers[provide]);
                }
                super(...resolvedArgs);
            }
        };
    };
}
@Inject()
class HttpClient {
    static provide = 'httpClient';
    get(url: string) {
        console.log(`从 ${url} 获取数据`);
    }
}
@Inject()
class UserService {
    constructor(private http: HttpClient) { }
    getUser() {
        this.http.get('/user');
    }
}
const httpClient = new HttpClient();
const providers: { [key: string]: any } = {
    [HttpClient.provide]: httpClient
};
const userService = new UserService();
userService.getUser();

在上述代码中,Inject 装饰器通过 Reflect.getMetadata 获取类构造函数参数的类型信息,并根据 provide 属性从 providers 中解析出对应的依赖实例,实现了依赖的自动注入。

类装饰器的高级应用 - AOP(面向切面编程)

面向切面编程(Aspect - Oriented Programming,简称AOP)是一种编程范式,它允许开发者将横切关注点(如日志记录、性能监控等)从业务逻辑中分离出来,以提高代码的可维护性和可复用性。类装饰器为在TypeScript中实现AOP提供了便利。

AOP的基本概念

在AOP中,横切关注点被称为“切面”,比如日志记录、事务管理等。“切点”定义了切面应用的具体位置,例如某个类的特定方法。“通知”则是在切点处执行的具体操作,如方法调用前的日志记录。

使用类装饰器实现AOP

function aspect(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function(...args: any[]) {
        console.log(`切面逻辑:方法 ${propertyKey} 调用前`);
        const result = originalMethod.apply(this, args);
        console.log(`切面逻辑:方法 ${propertyKey} 调用后`);
        return result;
    };
    return descriptor;
}
class MathService {
    @aspect
    add(a: number, b: number) {
        return a + b;
    }
}
const mathService = new MathService();
mathService.add(2, 3);

在这个例子中,aspect 装饰器作为一个切面,在 MathServiceadd 方法调用前后添加了日志记录的通知,而 add 方法就是切点。通过这种方式,我们将日志记录这种横切关注点从业务逻辑中分离出来,实现了AOP。

类装饰器与元数据

TypeScript的 reflect - metadata 库与类装饰器结合,可以为类及其成员添加和读取元数据,这在很多场景下非常有用,比如验证、路由等。

元数据的定义与读取

首先,我们需要安装 reflect - metadata 库,并在项目中引入它。

npm install reflect - metadata

然后在代码中使用:

import 'reflect - metadata';
const metadataKey = 'custom:metadata';
function addMetadata(target: any, propertyKey: string) {
    Reflect.defineMetadata(metadataKey, '元数据值', target, propertyKey);
}
class MyClass {
    @addMetadata
    myMethod() { }
}
const metadata = Reflect.getMetadata(metadataKey, MyClass.prototype,'myMethod');
console.log(metadata); 

在上述代码中,addMetadata 装饰器使用 Reflect.defineMetadataMyClassmyMethod 方法添加了元数据,然后通过 Reflect.getMetadata 读取该元数据。

基于元数据的验证

我们可以利用元数据实现方法参数和返回值的验证。

import 'reflect - metadata';
const validationMetadataKey = 'validation:rules';
function validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function(...args: any[]) {
        const rules = Reflect.getMetadata(validationMetadataKey, target, propertyKey);
        if (rules) {
            for (let i = 0; i < args.length; i++) {
                if (!rules[i](args[i])) {
                    throw new Error(`参数 ${i} 验证失败`);
                }
            }
        }
        const result = originalMethod.apply(this, args);
        if (rules && rules.length > args.length) {
            const returnRule = rules[rules.length - 1];
            if (!returnRule(result)) {
                throw new Error('返回值验证失败');
            }
        }
        return result;
    };
    return descriptor;
}
function addValidationRules(...rules: ((value: any) => boolean)[]) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        Reflect.defineMetadata(validationMetadataKey, rules, target, propertyKey);
    };
}
class Calculator {
    @validate
    @addValidationRules(
        (value: number) => typeof value === 'number' && value > 0,
        (value: number) => typeof value === 'number' && value > 0,
        (result: number) => result > 0
    )
    divide(a: number, b: number) {
        return a / b;
    }
}
const calculator = new Calculator();
try {
    calculator.divide(10, 2);
} catch (error) {
    console.error(error.message);
}

在这个例子中,addValidationRules 装饰器为 Calculatordivide 方法添加了参数和返回值的验证规则元数据。validate 装饰器在方法调用前后读取这些元数据并进行验证,确保方法的输入和输出符合预期。

类装饰器在框架中的应用

许多流行的前端框架,如Angular、Nest.js等,都广泛使用了类装饰器来实现各种功能。

Angular中的类装饰器

在Angular中,类装饰器用于定义组件、服务、模块等。例如,@Component 装饰器用于定义一个Angular组件:

import { Component } from '@angular/core';
@Component({
    selector: 'app - my - component',
    templateUrl: './my - component.html',
    styleUrls: ['./my - component.css']
})
export class MyComponent {
    message = 'Hello, Angular!';
}

@Component 装饰器为类添加了组件相关的元数据,如选择器、模板和样式等,使得该类成为一个可复用的Angular组件。

Nest.js中的类装饰器

Nest.js是一个用于构建高效、可扩展的Node.js服务器端应用程序的框架。它使用类装饰器来定义控制器、服务、模块等。例如,@Controller 装饰器用于定义一个控制器:

import { Controller, Get } from '@nestjs/common';
@Controller('users')
export class UsersController {
    @Get()
    getUsers() {
        return '获取用户列表';
    }
}

@Controller 装饰器为类定义了路由前缀,@Get 装饰器为方法定义了HTTP GET请求的路由,通过这些装饰器,Nest.js能够清晰地组织和管理应用程序的API。

类装饰器的性能考虑

虽然类装饰器为我们带来了很多便利,但在使用时也需要考虑性能问题。

装饰器执行时机

装饰器在类定义时就会执行,而不是在类实例化时。这意味着如果有大量的类装饰器,特别是那些执行复杂操作的装饰器,可能会影响应用程序的启动性能。

避免过度使用

过度使用类装饰器可能会导致代码难以理解和维护。每个装饰器都会对类的定义进行修改,过多的装饰器会使类的实际行为变得复杂和难以追踪。因此,在使用类装饰器时,要确保它们是必要的,并且保持装饰器逻辑的简洁性。

类装饰器的兼容性与未来发展

TypeScript装饰器目前处于实验阶段,不同的运行环境对其支持程度可能有所不同。

兼容性

在现代的JavaScript运行环境,如Node.js和最新版本的浏览器中,对TypeScript装饰器的支持相对较好。但在一些较旧的环境中,可能需要使用转译工具(如Babel)来确保装饰器能够正常工作。

未来发展

随着JavaScript和TypeScript的不断发展,装饰器可能会成为更稳定和标准的特性。未来,我们可能会看到更多的高级应用场景和更简洁的语法,进一步提升开发效率。

在实际项目中,我们应该根据项目的需求和目标运行环境,合理地使用类装饰器,充分发挥其优势,同时避免潜在的问题。通过深入理解和掌握类装饰器的高级应用,我们能够编写出更加优雅、可维护和高效的前端代码。