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装饰器。它应该具备以下特性:
- 支持参数传递:能够接收URL、查询参数、请求头等配置
- 错误处理:优雅地处理网络错误、超时、状态码异常
- 类型安全:完整的TypeScript类型支持
- 可测试性:便于单元测试和集成测试
- 可配置性:支持超时、重试、缓存等高级特性
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

&spm=1001.2101.3001.5002&articleId=152651737&d=1&t=3&u=839fdc44c2d5483bb47bedeee6191ab9)
1260

被折叠的 条评论
为什么被折叠?



