深入理解 NestJS @Query 装饰器的实现原理

前言

在 NestJS 中,@Query 装饰器让我们能够优雅地从请求中提取查询参数。本文将通过一个简单的 demo 来深入理解它的实现原理。

一、基本概念

1.1 装饰器的作用

在这个实现中,我们使用了两个关键的装饰器:

  • @Query:参数装饰器,负责收集和存储参数信息
  • @LogMethod:方法装饰器,负责处理和注入参数

1.2 元数据的作用

使用 reflect-metadata 来存储和读取装饰器的元数据,这是实现依赖注入的关键。

二、核心实现

2.1 参数装饰器 @Query

typescript 复制代码
function Query(paramName: string) {
  return function (target: Object, methodKey: string, parameterIndex: number) {
    const existingParams = Reflect.getMetadata(QUERY_METADATA_KEY, target, methodKey) || [];
    
    existingParams.push({
      index: parameterIndex,
      name: paramName
    });
    
    Reflect.defineMetadata(QUERY_METADATA_KEY, existingParams, target, methodKey);
  };
}

这个装饰器的职责是:

  1. 收集参数信息(参数名和位置)
  2. 将信息存储到元数据中

2.2 方法装饰器 @LogMethod

typescript 复制代码
function LogMethod() {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = function (req: Request) {
      const queryParams = Reflect.getMetadata(QUERY_METADATA_KEY, target, propertyKey) || [];
      
      const args = queryParams
        .sort((a, b) => a.index - b.index)
        .map(param => req.query[param.name]);

      return originalMethod.apply(this, args);
    };
  };
}

这个装饰器的职责是:

  1. 读取存储的参数信息
  2. 从请求中提取参数值
  3. 按正确顺序组装参数
  4. 调用原始方法

三、使用示例

typescript 复制代码
class TestController {
  @LogMethod()
  getUsers(@Query('page') page: string, @Query('size') size: string) {
    return `Getting users from page ${page} with size ${size}`;
  }
}

// 使用
const controller = new TestController();
const request = {
  query: {
    page: '1',
    size: '10'
  }
};

const result = controller.getUsers(request);

四、工作流程

  1. 参数装饰器执行:

    • @Query('page') 存储第一个参数信息
    • @Query('size') 存储第二个参数信息
  2. 方法装饰器执行:

    • 读取存储的参数信息
    • 从请求中提取对应的值
    • 按顺序组装参数数组
    • 调用原始方法
  3. 最终效果:

    • 输入:{ query: { page: '1', size: '10' } }
    • 转换为:getUsers('1', '10')

五、实现原理总结

  1. 参数装饰器 @Query 的作用:

    • 只负责解析和保存参数信息
    • 将参数位置和名称存储到元数据中
  2. 方法装饰器 @LogMethod 的作用:

    • 读取存储的参数信息
    • 从请求中提取实际值
    • 重新组装参数并调用方法

这种设计模式让我们能够:

  • 优雅地处理请求参数
  • 实现依赖注入
  • 分离关注点
  • 提高代码可维护性

六、应用场景

  1. 请求参数处理
  2. 依赖注入实现
  3. 参数转换和验证
  4. API 接口开发

完整代码

js 复制代码
import 'reflect-metadata';

// 定义装饰器元数据的 key
const QUERY_METADATA_KEY = 'query_params';

// 创建参数装饰器
function Query(paramName: string) {
  return function (target: Object, methodKey: string | symbol, parameterIndex: number) {
    console.log('Query target====>', target);
    console.log('Query methodKey====>', methodKey);
    console.log('Query parameterIndex====>', parameterIndex);
    // 获取现有的参数元数据或初始化新的
    const existingParams: Array<{ index: number; name: string }> = 
      Reflect.getMetadata(QUERY_METADATA_KEY, target, methodKey) || [];
    
    // 添加新的参数信息
    existingParams.push({
      index: parameterIndex,
      name: paramName
    });

    console.log('existingParams====>', existingParams);
    
    // 保存更新后的元数据
    Reflect.defineMetadata(QUERY_METADATA_KEY, existingParams, target, methodKey);
  };
}

// 模拟请求对象
interface Request {
  query: Record<string, string>;
}

// 模拟控制器
class TestController {
  @LogMethod()
  getUsers(@Query('page') page: string, @Query('size') size: string) {
    return `Getting users from page ${page} with size ${size}`;
  }
}

// 方法装饰器 - 用于处理请求
function LogMethod() {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    console.log('target====>', target);
    console.log('propertyKey====>', propertyKey);
    console.log('descriptor====>', descriptor);
    const originalMethod = descriptor.value;

    descriptor.value = function (req: Request) {
      // 获取查询参数的元数据
      const queryParams: Array<{ index: number; name: string }> = 
        Reflect.getMetadata(QUERY_METADATA_KEY, target, propertyKey) || [];
      
      // 根据元数据构建方法参数
      const args = queryParams
        .sort((a, b) => a.index - b.index)
        .map(param => req.query[param.name]);

      console.log('args====>', args);

      // 调用原始方法
      const result = originalMethod.apply(this, args);
      
      console.log(`====>Method ${propertyKey} called with query params:`, req.query);
      return result;
    };

    return descriptor;
  };
}

// 使用示例
const controller = new TestController();

// 模拟请求
const request: Request = {
  query: {
    page: '1',
    size: '10'
  }
};

// 测试调用
const result = (controller.getUsers as any)(request);
console.log('Result=====>', result);
相关推荐
Eric_见嘉1 天前
NestJS 🧑‍🍳 厨子必修课(九):API 文档 Swagger
前端·后端·nestjs
XiaoYu20029 天前
第3章 Nest.js拦截器
前端·ai编程·nestjs
XiaoYu200211 天前
第2章 Nest.js入门
前端·ai编程·nestjs
实习生小黄11 天前
NestJS 调试方案
后端·nestjs
当时只道寻常14 天前
NestJS 如何配置环境变量
nestjs
濮水大叔1 个月前
VonaJS是如何做到文件级别精确HMR(热更新)的?
typescript·node.js·nestjs
ovensi1 个月前
告别笨重的 ELK,拥抱轻量级 PLG:NestJS 日志监控实战指南
nestjs
ovensi1 个月前
Docker+NestJS+ELK:从零搭建全链路日志监控系统
后端·nestjs
Gogo8161 个月前
nestjs 的项目启动
nestjs