1. 从基础GET装饰器到可复用封装
还记得上一章我们实现的简单GET请求装饰器吗?那个只能算是个"玩具"。在实际项目中,我们需要处理各种复杂场景:不同的请求头、错误重试机制、日志记录、性能监控...如果每个控制器都写一遍这些逻辑,代码很快就会变得难以维护。
我刚开始用NestJS做电商后台时,就遇到过这种问题。系统需要对接十几个外部API,每个接口都要处理超时、鉴权、日志。最初的做法是复制粘贴代码,结果某天需要修改请求头格式时,我不得不修改二十多个文件——这简直是一场噩梦。
后来发现装饰器才是解决这类问题的银弹。通过将通用逻辑封装到装饰器中,不仅能保持代码整洁,还能实现跨项目的复用。比如我们现在要封装的这个GET装饰器,经过多次迭代后已经在我们团队三个不同项目中稳定运行了半年多。
2. 可配置装饰器的核心设计
2.1 装饰器工厂模式进阶
基础版的装饰器工厂只能接收URL参数,而我们的增强版需要支持完整配置项:
interface GetOptions { url: string; headers?: Record<string, string>; timeout?: number; retry?: number; logger?: boolean; }这个配置接口的设计有几点值得注意:
- 必填的只有url字段,其他都是可选配置
- headers使用键值对结构,方便扩展各种认证信息
- timeout单位是毫秒,默认可以设为3000
- retry表示重试次数,对于重要接口建议设为2-3次
- logger开关控制是否记录请求日志
2.2 实现配置合并逻辑
实际项目中我们通常会有全局配置和局部配置:
const DEFAULT_OPTIONS = { timeout: 3000, retry: 0, logger: true }; function mergeOptions(options: GetOptions) { return { ...DEFAULT_OPTIONS, ...options }; }这种合并策略让我们的装饰器既灵活又统一。全局配置放在DEFAULT_OPTIONS中,单个装饰器调用时可以覆盖特定配置。
3. 增强型GET装饰器实现
3.1 核心功能实现
完整版的装饰器需要处理以下场景:
const Get = (options: GetOptions | string): MethodDecorator => { return (target, key, descriptor) => { const originalMethod = descriptor.value; const config = typeof options === 'string' ? mergeOptions({ url: options }) : mergeOptions(options); descriptor.value = async function (...args: any[]) { try { const response = await axios.get(config.url, { headers: config.headers, timeout: config.timeout }); return originalMethod.apply(this, [response.data, ...args]); } catch (error) { if (config.retry > 0) { // 重试逻辑实现 } throw error; } }; }; };这个版本有几个关键改进:
- 支持字符串参数快捷调用(保持向下兼容)
- 使用async/await替代Promise链式调用
- 保留了原方法的this绑定
- 响应数据自动解构
3.2 错误处理增强
在实际项目中,简单的错误捕获远远不够。我们需要区分网络错误、超时错误、业务错误等不同情况:
enum ErrorType { NETWORK, TIMEOUT, BUSINESS } // 在catch块中添加错误分类 const classifiedError = classifyError(error); if (config.logger) { logError(classifiedError, config); }完整的错误分类函数需要考虑axios错误码、响应状态码等多种因素。这部分代码虽然繁琐,但对后续的错误监控非常重要。
4. 装饰器的高级用法
4.1 组合装饰器模式
在真实项目中,我们经常需要组合多个装饰器。比如同时需要缓存和请求:
@Cache({ ttl: 60 }) @Get('/api/products') async getProducts() { // 方法体会被两个装饰器增强 }实现这种效果需要注意装饰器的执行顺序(从下到上,从右到左)。我们的GET装饰器需要确保与其他装饰器兼容。
4.2 元数据集成
配合reflect-metadata可以实现更强大的功能:
import 'reflect-metadata'; const GetWithMeta = (options: GetOptions): MethodDecorator => { return (target, key) => { Reflect.defineMetadata('api:endpoint', options, target, key); // 其他装饰逻辑... }; };这样我们就能在全局统一获取所有API端点信息,非常适合用来生成API文档或做接口监控。
5. 实战中的性能优化
5.1 请求拦截器优化
当装饰器被大量使用时,每个请求都创建新的axios实例会导致性能问题。解决方案是使用单例的axios实例:
const apiClient = axios.create({ timeout: 3000 }); // 在装饰器内部复用这个实例 apiClient.get(config.url);5.2 缓存策略实现
对于高频调用的接口,可以在装饰器层面实现缓存:
const cacheStore = new Map(); descriptor.value = async function(...args) { const cacheKey = generateCacheKey(config.url, args); if (cacheStore.has(cacheKey)) { return cacheStore.get(cacheKey); } // ...正常请求逻辑 cacheStore.set(cacheKey, result); return result; };注意要设置合理的缓存过期时间,避免内存泄漏。
6. 测试与调试技巧
6.1 单元测试方案
测试装饰器需要特殊技巧,因为装饰器本质上是修改类行为。推荐使用以下测试结构:
describe('Get Decorator', () => { let testInstance: TestClass; beforeAll(() => { @Controller() class TestClass { @Get('https://api.example.com') testMethod() {} } testInstance = new TestClass(); }); it('should modify method behavior', () => { // 测试方法是否被正确增强 }); });6.2 调试技巧
调试装饰器时最头疼的问题是代码执行顺序。我常用的方法是:
- 在装饰器开始和结束处打日志
- 使用VS Code的调试器,在装饰器函数内设置断点
- 检查descriptor.value.toString()查看方法是否被正确修改
7. 在NestJS中的最佳实践
虽然我们实现的装饰器可以在任何TS项目中使用,但在NestJS中有更优雅的集成方式:
7.1 作为Provider使用
将装饰器封装成可注入的服务:
@Injectable() export class ApiClient { async get(config: GetOptions) { // 实现核心逻辑 } } // 然后在装饰器中使用 constructor(private readonly apiClient: ApiClient) {} @GetDecorator('/api') async getData() { // ... }7.2 结合NestJS拦截器
对于需要全局处理的逻辑(如认证),可以结合拦截器使用:
@UseInterceptors(LoggingInterceptor) @GetDecorator('/api') async getData() { // ... }这种分层设计让代码更加清晰可维护。
8. 从装饰器到自定义装饰器工厂
当项目规模扩大时,我们需要更高级的抽象——装饰器工厂函数:
function createApiDecorator(baseOptions: GetOptions) { return (overrideOptions: Partial<GetOptions>) => { return Get({ ...baseOptions, ...overrideOptions }); }; } // 项目初始化时配置 const ProjectGet = createApiDecorator({ timeout: 5000, headers: { 'X-App-Version': '1.0.0' } }); // 控制器中使用 @Controller() class UserController { @ProjectGet({ url: '/api/users' }) getUsers() {} }这种模式特别适合大型项目,可以确保所有API调用遵循统一的配置标准。