NestJS装饰器实战:手把手教你自定义GET请求装饰器(附完整代码)

NestJS装饰器实战:从零构建一个功能完备的自定义GET请求装饰器

如果你已经用了一段时间NestJS,可能会觉得框架内置的装饰器已经足够强大——@Get()@Post()@Param()这些装饰器确实让路由定义变得异常简洁。但真正深入NestJS开发后,你会发现自定义装饰器才是这个框架的灵魂所在。它不仅仅是语法糖,更是一种强大的元编程工具,能够将复杂的业务逻辑封装成简洁的API。

今天我要分享的,不是简单的装饰器语法教程,而是如何从零开始构建一个真正实用的自定义GET请求装饰器。这个装饰器不仅要能发起HTTP请求,还要处理错误、支持参数传递、易于测试,甚至能在生产环境中稳定运行。我会带你一步步实现,并分享我在实际项目中踩过的坑和解决方案。

1. 理解装饰器的本质:不只是语法糖

很多人把TypeScript装饰器看作是一种"美化代码"的工具,这种理解太浅了。在NestJS的架构中,装饰器实际上是元数据注入器。它们的作用是在类、方法、属性或参数上附加额外的信息,这些信息在运行时可以被框架读取和处理。

1.1 装饰器的执行时机

理解装饰器的执行时机至关重要。看这个简单的例子:

function LogExecutionTime() {
  console.log('装饰器工厂执行');
  
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log('装饰器函数执行');
    
    const originalMethod = descriptor.value;
    descriptor.value = function(...args: any[]) {
      console.log('方法实际调用');
      return originalMethod.apply(this, args);
    };
  };
}

class ExampleController {
  @LogExecutionTime()
  getData() {
    console.log('业务逻辑执行');
  }
}

// 输出顺序:
// 装饰器工厂执行
// 装饰器函数执行
// (此时getData方法还没有被调用)

这里的关键点:装饰器在类定义时立即执行,而不是在方法调用时。这意味着装饰器内部的逻辑应该专注于修改方法描述符,而不是执行具体的业务逻辑。

1.2 方法装饰器的核心参数

方法装饰器接收三个关键参数:

参数 类型 描述
target any 对于静态方法是类的构造函数,对于实例方法是类的原型
propertyKey string | symbol 方法名称
descriptor PropertyDescriptor 方法的属性描述符

PropertyDescriptor是JavaScript的原生对象,包含以下重要属性:

interface PropertyDescriptor {
  configurable?: boolean;    // 是否可配置
  enumerable?: boolean;      // 是否可枚举
  writable?: boolean;        // 是否可写
  value?: any;               // 方法本身
  get?(): any;              // getter函数
  set?(v: any): void;       // setter函数
}

对于方法装饰器,我们主要操作descriptor.value,这是原始方法的引用。

2. 设计一个实用的GET请求装饰器

现在我们来设计一个真正有用的自定义GET装饰器。它应该具备以下特性:

  1. 支持参数传递:能够接收URL、查询参数、请求头等配置
  2. 错误处理:优雅地处理网络错误、超时、状态码异常
  3. 类型安全:完整的TypeScript类型支持
  4. 可测试性:便于单元测试和集成测试
  5. 可配置性:支持超时、重试、缓存等高级特性

2.1 基础版本:最简单的GET装饰器

我们先从最简单的版本开始,理解核心原理:

import axios, { AxiosResponse } from 'axios';

export function SimpleGet(url: string): MethodDecorator {
  return (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
    const originalMethod = descriptor.value;
    
    // 重写原方法
    descriptor.value = async function(...args: any[]) {
      try {
        const response: AxiosResponse = await axios.get(url);
        // 调用原始方法,并传入响应数据
        return originalMethod.apply(this, [response.data, response.status]);
      } catch (error) {
        // 错误处理
        return originalMethod.apply(this, [error, 500]);
      }
    };
    
    return descriptor;
  };
}

使用方式:

class ApiController {
  @SimpleGet('https://api.example.com/data')
  async fetchData(data: any, status: number) {
    console.log('获取到的数据:', data);
    console.log('状态码:', status);
    return data;
  }
}

这个版本虽然简单,但有几个明显问题:

  • 硬编码了错误状态码500
  • 没有处理请求配置
  • 错误信息不够详细
  • 缺乏类型安全

2.2 进阶版本:支持完整配置

让我们改进这个装饰器,支持更多的配置选项:

import axios, { AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';

export interface GetDecoratorOptions {
  url: string;
  params?: Record<string, any>;
  headers?: Record<string, string>;
  timeout?: number;
  retry?: number;
  retryDelay?: number;
  cache?: boolean;
  cacheTTL?: number; // 缓存生存时间(毫秒)
}

export function AdvancedGet(options: GetDecoratorOptions): MethodDecorator {
  return (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
    const originalMethod = descriptor.value;
    const cache = new Map<string, { data: any; timestamp: number }>();
    
    descriptor.value = async function(...args: any[]) {
      const {
        url,
        params = {},
        headers = {},
        timeout = 5000,
        retry = 0,
        retryDelay = 1000,
        cache: useCache = false,
        cacheTTL = 300000, // 默认5分钟
      } = options;
      
      // 缓存键生成
      const cacheKey = `${url}?${new URLSearchParams(params).toString()}`;
      
      // 检查缓存
      if (useCache) {
        const cached = cache.get(cacheKey);
        if (cached && Date.now() - cached.timestamp < cacheTTL) {
          return originalMethod.apply(this, [cached.data, 200, true]); // 第三个参数表示来自缓存
        }
      }
      
      const config: AxiosRequestConfig = {
        url,
        method: 'GET',
        params,
        headers,
        timeout,
      };
      
      let lastError: AxiosError | null = null;
      
      for (let attempt = 0; attempt <= retry; attempt++) {
        if (attempt > 0) {
          console.log(`第${attempt}次重试...`);
          await new Promise(resolve => setTimeout(resolve, retryDelay));
        }
        
        try {
          const response: AxiosResponse = await axios(config);
          
          // 更新缓存
          if (useCache) {
            cache.set(cacheKey, {
              data: response.data,
              timestamp: Date.now(),
            });
          }
          
          return originalMethod.apply(this, [
            response.data,
            response.status,
            response.headers,
            false, // 不是缓存数据
          ]);
          
        } catch (error) {
          lastError = error as AxiosError;
          
          // 如果是最后一次尝试,或者错误不是网络错误,则抛出
          if (attempt === retry || !error.isAxiosError) {
            break;
          }
          
          // 如果是超时或网络错误,继续重试
          if (error.code === 'ECONNABORTED' || error.code === 'ENETUNREACH') {
            continue;
          }
          
          break;
        }
      }
      
      // 所有重试都失败
      const errorResponse = {
        message: lastError?.message || '请求失败',
        code: lastError?.code,
        config: lastError?.config,
      };
      
      return originalMethod.apply(this, [
        errorResponse,
        lastError?.response?.status || 500,
        null,
        false,
      ]);
    };
    
    return descriptor;
  };
}

这个版本增加了:

  • 请求参数支持
  • 自定义请求头
  • 超时设置
  • 自动重试机制
  • 简单的内存缓存

注意:生产环境中的缓存应该使用Redis等外部存储,这里的内存缓存仅用于演示原理。

3. 类型安全的装饰器实现

对于TypeScript项目,类型安全至关重要。我们需要确保装饰器返回的数据类型是明确的。

3.1 使用泛型增强类型安全

import axios, { AxiosRequestC
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值