深入理解 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);
相关推荐
汪小成8 小时前
NestJS学习笔记-02-模块、控制器与服务,手把手构建你的第一个CRUD API!🚀
后端·nestjs
汪小成1 天前
NestJS学习笔记-01-第一个Nest应用诞生记 🚀
后端·nestjs
plusone1 天前
【Nest指北系列】守卫
nestjs
用户11481867894842 天前
大文件下载、断点续传功能
前端·nestjs
hezf2 天前
初识 Prisma-结合NestJS
数据库·后端·nestjs
Kagol2 天前
🎉TinyPro v1.2.0 正式发布,趁着 TinyPro 项目刚创建不久,快来参与贡献(蹭 PR)吧!
前端·vue.js·nestjs
你不会困5 天前
什么?每天早上准时9点给你发送github项目推荐邮件
前端·javascript·nestjs
林太白5 天前
学到了,强大的企业级NestJS必须了解一下
前端·后端·nestjs
HxY6 天前
使用nest-mcp-sse快速开发MCP Server
nestjs·mcp