深入理解 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);
相关推荐
__zRainy__10 小时前
Node.js Web 框架选型指南:从 Express 到 Hono 的全景对比
前端·node.js·express·koa·nestjs·egg·fastify
Darling噜啦啦20 小时前
NestJS 全栈 CRUD 实战:从 Module 拆分到 RESTful 七大装饰器,彻底搞懂后端接口工程
nestjs
嘟嘟07171 天前
从入口到 CRUD:用 NestJS 的模块化与装饰器思想读懂一个后端应用
后端·typescript·nestjs
想要成为糕糕手1 天前
🚀 NestJS 完全入门指南: Module/Controller/Service讲解 + 实战 CRUD
后端·nestjs
想要成为糕糕手1 天前
🏭 设计模式之工厂模式:从蜜雪冰城到 NestJS,把「new」外包出去
后端·nestjs
小月土星1 天前
用设计哲学的眼光重新审视 NestJS——一个 AI 应用后端的思考
nestjs
触底反弹1 天前
🔥 NestJS 从零到实战:一个 Todos CRUD 搞懂企业级后端框架的核心设计
后端·typescript·nestjs
只一1 天前
吃透 NestJS 核心|工厂模式 + 依赖注入 + Todo CRUD 实战,后端入门到上手
nestjs
Asize1 天前
3 种设计模式 + DI:Nest.js 后端第一课
后端·设计模式·nestjs
moMo2 天前
深入 NestJS:从工厂模式到装饰器模式的架构之美
typescript·nestjs