深入理解 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);
相关推荐
濮水大叔4 天前
你认为Vonajs提供的这些特性会比Nestjs更好用吗?
nodejs·nestjs
前端卧龙人5 天前
你的nest项目不要再使用console.log
nestjs
plusone6 天前
【Nest指北系列-源码】(四)NestContainer
nestjs
zhuyasen9 天前
从Node.js到Go:如何从NestJS丝滑切换并拥抱Sponge框架
node.js·nestjs
前端杂货铺13 天前
NestJS——重构日志、数据库、配置
数据库·重构·nestjs
前端杂货铺21 天前
NestJS——日志、NestJS-logger、pino、winston、全局异常过滤器
nestjs·日志
林太白1 个月前
NestJS用户模块CRUD和分页实现
前端·javascript·nestjs
plusone1 个月前
【Nest指北系列-源码】(一)目录结构
nestjs
前端笨鸟2 个月前
NestJS+MongoDB高效CRUD接口开发全景指南
前端·mongodb·nestjs
前端实习生鲸落2 个月前
nest 静态文件打包
前端·node.js·nestjs