Skip to content

参数装饰器原理:@Body()@Param()@Query() 如何工作?

在 NestJS 中,参数装饰器是连接 HTTP 请求数据与处理函数参数的桥梁。通过 @Body()@Param()@Query() 等装饰器,我们可以轻松地从请求中提取所需的数据。但这些装饰器是如何工作的呢?它们如何通过元数据告诉框架"这个参数从哪来"?本文将深入探讨参数装饰器的内部实现机制。

1. 参数装饰器基础概念

1.1 什么是参数装饰器?

参数装饰器是 TypeScript 提供的一种特殊装饰器,用于装饰函数参数:

typescript
// 参数装饰器的基本形式
function ParameterDecorator(target: any, propertyKey: string, parameterIndex: number) {
  // target: 对于静态方法是类构造函数,对于实例方法是类的原型
  // propertyKey: 方法名
  // parameterIndex: 参数在参数列表中的索引
}

// 使用示例
class MyClass {
  method(@ParameterDecorator param1: string, param2: number) {
    // ...
  }
}

1.2 NestJS 中的参数装饰器

typescript
@Controller('users')
export class UserController {
  @Post(':id')
  createUser(
    @Param('id') id: string,           // 从路径参数中提取
    @Query('page') page: number,        // 从查询参数中提取
    @Body() createUserDto: CreateUserDto, // 从请求体中提取
    @Headers('authorization') auth: string, // 从请求头中提取
    @Request() req: Request,            // 获取整个请求对象
  ) {
    return { id, page, createUserDto, auth, req };
  }
}

2. 参数装饰器的实现机制

2.1 装饰器工厂函数

NestJS 的参数装饰器都是通过工厂函数创建的:

typescript
// nestjs/common/decorators/http/route-params.decorator.ts(简化版)
export function createRouteParamDecorator(paramtype: RouteParamtypes) {
  return (data?: any): ParameterDecorator => (target, key, index) => {
    // 获取现有的路由参数元数据
    const args = Reflect.getMetadata(ROUTE_ARGS_METADATA, target.constructor, key) || {};
    
    // 存储参数信息
    args[index] = {
      index,
      factory: (data: any, ctx: ExecutionContext) => 
        ctx.switchToHttp().getRequest()[paramtype],
      data,
      pipes: [],
    };
    
    // 将元数据存储在类的方法上
    Reflect.defineMetadata(ROUTE_ARGS_METADATA, args, target.constructor, key);
  };
}

// 具体的装饰器实现
export const Request: () => ParameterDecorator = createRouteParamDecorator(RouteParamtypes.REQUEST);
export const Response: () => ParameterDecorator = createRouteParamDecorator(RouteParamtypes.RESPONSE);
export const Next: () => ParameterDecorator = createRouteParamDecorator(RouteParamtypes.NEXT);
export const Body: (property?: string) => ParameterDecorator = createRouteParamDecorator(RouteParamtypes.BODY);
export const Query: (property?: string) => ParameterDecorator = createRouteParamDecorator(RouteParamtypes.QUERY);
export const Param: (property?: string) => ParameterDecorator = createRouteParamDecorator(RouteParamtypes.PARAM);

2.2 元数据存储结构

typescript
// 存储的元数据结构示例
{
  0: {  // 第一个参数 (@Param('id') id: string)
    index: 0,
    factory: Function,  // 用于提取参数值的工厂函数
    data: 'id',         // 装饰器传递的数据
    pipes: [],          // 应用于该参数的管道
  },
  1: {  // 第二个参数 (@Query('page') page: number)
    index: 1,
    factory: Function,
    data: 'page',
    pipes: [],
  }
}

3. 参数提取过程

3.1 路由处理函数的执行

当请求到达时,NestJS 需要执行路由处理函数并为其提供正确的参数:

typescript
class RouterExecutionContext {
  async getParamValue<T>(
    req: TRequest,
    res: TResponse,
    next: Function,
    param: ParamData,
  ): Promise<any> {
    // 获取工厂函数
    const { factory, data, pipes } = param;
    
    // 使用工厂函数提取原始值
    const value = await factory(data, { 
      switchToHttp: () => ({ getRequest: () => req, getResponse: () => res }),
      // ... 其他上下文方法
    });
    
    // 应用管道进行转换和验证
    return this.applyPipes(value, { metatype: param.metatype, type: param.type, data, pipes });
  }
}

3.2 工厂函数的具体实现

每个参数类型都有对应的工厂函数:

typescript
// Body 参数工厂函数
function bodyFactory(data: string | undefined, ctx: ExecutionContext) {
  const req = ctx.switchToHttp().getRequest();
  return data ? req.body?.[data] : req.body;
}

// Query 参数工厂函数
function queryFactory(data: string | undefined, ctx: ExecutionContext) {
  const req = ctx.switchToHttp().getRequest();
  return data ? req.query?.[data] : req.query;
}

// Param 参数工厂函数
function paramFactory(data: string | undefined, ctx: ExecutionContext) {
  const req = ctx.switchToHttp().getRequest();
  return data ? req.params?.[data] : req.params;
}

// Headers 参数工厂函数
function headersFactory(data: string | undefined, ctx: ExecutionContext) {
  const req = ctx.switchToHttp().getRequest();
  return data ? req.headers?.[data] : req.headers;
}

4. 管道在参数处理中的应用

4.1 参数级别的管道

参数装饰器支持为特定参数应用管道:

typescript
@Get(':id')
findOne(
  @Param('id', ParseIntPipe) id: number,           // 将 id 转换为数字
  @Query('page', ParseIntPipe) page: number,       // 将 page 转换为数字
  @Body(ValidationPipe) createUserDto: CreateUserDto, // 验证请求体
) {
  return this.userService.findOne(id, page, createUserDto);
}

4.2 管道执行过程

typescript
class RouterExecutionContext {
  async applyPipes<T>(
    value: T,
    { metatype, type, data, pipes }: { 
      metatype: Type<unknown>; 
      type: RouteParamtypes; 
      data: unknown; 
      pipes: PipeTransform[]; 
    },
  ) {
    // 合并方法级别的管道和参数级别的管道
    const combinedPipes = this.contextUtils.mergePipes(
      this.paramsContext.get(type).pipes,  // 方法级别的管道
      pipes,                               // 参数级别的管道
    );
    
    // 依次执行所有管道
    return combinedPipes.reduce(async (acc, pipe) => {
      const val = await acc;
      return pipe.transform(val, { metatype, type, data });
    }, Promise.resolve(value));
  }
}

5. 自定义参数装饰器

5.1 创建自定义参数装饰器

我们可以创建自定义参数装饰器来满足特定需求:

typescript
// decorators/current-user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const CurrentUser = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return request.user; // 假设用户信息存储在 request.user 中
  },
);

// 使用自定义装饰器
@Controller('users')
export class UserController {
  @Get('profile')
  getProfile(@CurrentUser() user: User) {
    return user;
  }
}

5.2 带配置的自定义参数装饰器

typescript
// decorators/cookie.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const Cookie = createParamDecorator(
  (data: string, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return data ? request.cookies?.[data] : request.cookies;
  },
);

// 使用带配置的自定义装饰器
@Controller('auth')
export class AuthController {
  @Get('me')
  getMe(@Cookie('sessionId') sessionId: string) {
    return this.authService.validateSession(sessionId);
  }
}

6. 参数装饰器的高级用法

6.1 组合多个装饰器

typescript
// decorators/auth-user.decorator.ts
import { createParamDecorator, ExecutionContext, UnauthorizedException } from '@nestjs/common';

export const AuthUser = createParamDecorator(
  async (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    
    // 检查用户是否已认证
    if (!request.user) {
      throw new UnauthorizedException('用户未认证');
    }
    
    // 可以在这里添加额外的权限检查
    if (data === 'admin' && request.user.role !== 'admin') {
      throw new UnauthorizedException('需要管理员权限');
    }
    
    return request.user;
  },
);

// 使用
@Controller('admin')
export class AdminController {
  @Get('users')
  getAllUsers(@AuthUser('admin') user: User) {
    return this.userService.findAll();
  }
}

6.2 与守卫的配合使用

typescript
// guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [
      context.getHandler(),
      context.getClass(),
    ]);
    
    if (!requiredRoles) {
      return true;
    }
    
    const { user } = context.switchToHttp().getRequest();
    return requiredRoles.some((role) => user.roles?.includes(role));
  }
}

// 使用
@Controller('users')
export class UserController {
  @Get(':id')
  @UseGuards(RolesGuard)
  @SetMetadata('roles', ['admin', 'user'])
  findOne(@Param('id') id: string, @CurrentUser() user: User) {
    // 如果是用户自己查看自己的信息,或者管理员查看任意用户信息
    if (user.id === id || user.roles.includes('admin')) {
      return this.userService.findOne(id);
    }
    throw new ForbiddenException('无权访问');
  }
}

7. 性能考虑和最佳实践

7.1 避免重复的参数提取

typescript
// 不推荐:重复提取相同参数
@Get(':id')
findOne(
  @Param('id') id: string,
  @Param('id', ParseUUIDPipe) uuid: string,
) {
  // ...
}

// 推荐:使用管道进行转换
@Get(':id')
findOne(
  @Param('id', ParseUUIDPipe) id: string,
) {
  // ...
}

7.2 合理使用参数装饰器

typescript
// 推荐:明确指定需要的参数
@Get()
findAll(
  @Query('page', ParseIntPipe) page: number = 1,
  @Query('limit', ParseIntPipe) limit: number = 10,
) {
  return this.userService.findAll({ page, limit });
}

// 不推荐:获取整个查询对象
@Get()
findAll(@Query() query: any) {
  // 这样做会失去类型安全和验证
  const { page = 1, limit = 10 } = query;
  return this.userService.findAll({ page, limit });
}

8. 错误处理

8.1 参数验证失败

当参数验证失败时,NestJS 会抛出相应的异常:

typescript
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
  // 如果 id 不是有效的数字,ParseIntPipe 会抛出 BadRequestException
  return this.userService.findOne(id);
}

8.2 自定义错误处理

typescript
// 自定义管道处理错误
@Injectable()
export class CustomParseIntPipe implements PipeTransform<string, number> {
  transform(value: string): number {
    const val = parseInt(value, 10);
    if (isNaN(val)) {
      throw new BadRequestException('参数必须是有效的数字');
    }
    return val;
  }
}

9. 内部实现细节

9.1 参数解析流程

typescript
// 完整的参数解析流程
class RouterExecutionContext {
  async getArguments(
    instance: Controller,
    methodName: string,
    context: ExecutionContext,
  ): Promise<any[]> {
    // 1. 获取方法参数元数据
    const metadata = Reflect.getMetadata(ROUTE_ARGS_METADATA, instance.constructor, methodName) || {};
    
    // 2. 按索引排序参数
    const keys = Object.keys(metadata)
      .map(Number)
      .sort((a, b) => a - b);
    
    // 3. 解析每个参数
    const args = [];
    for (const index of keys) {
      const param = metadata[index];
      const value = await this.getParamValue(context, param);
      args.push(value);
    }
    
    return args;
  }
  
  private async getParamValue(context: ExecutionContext, param: ParamMetadata) {
    // 1. 使用工厂函数提取原始值
    const rawValue = await param.factory(param.data, context);
    
    // 2. 应用管道转换和验证
    const transformedValue = await this.applyPipes(rawValue, param);
    
    return transformedValue;
  }
}

9.2 元数据管理

typescript
// 元数据管理机制
class MetadataScanner {
  private readonly metadataCache = new Map<string, any>();
  
  getMetadata(target: any, propertyKey: string, metadataKey: string) {
    const cacheKey = `${target.name}:${propertyKey}:${metadataKey}`;
    
    if (this.metadataCache.has(cacheKey)) {
      return this.metadataCache.get(cacheKey);
    }
    
    const metadata = Reflect.getMetadata(metadataKey, target, propertyKey);
    this.metadataCache.set(cacheKey, metadata);
    
    return metadata;
  }
}

10. 总结

参数装饰器通过以下机制工作:

  1. 元数据定义:装饰器将参数信息存储为方法的元数据
  2. 参数提取:在运行时,框架读取元数据并使用工厂函数提取参数值
  3. 管道处理:提取的值通过管道进行转换和验证
  4. 参数注入:处理后的值作为参数传递给路由处理函数

通过理解这些机制,我们可以:

  1. 更好地使用内置参数装饰器
  2. 创建自定义参数装饰器满足特定需求
  3. 优化参数处理性能
  4. 实现更复杂的参数验证和转换逻辑

在下一篇文章中,我们将探讨管道(Pipe)的执行时机:在 handler 之前拦截,深入了解 ValidationPipe 如何结合 class-validator 实现自动校验。