参数装饰器原理:@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. 总结
参数装饰器通过以下机制工作:
- 元数据定义:装饰器将参数信息存储为方法的元数据
- 参数提取:在运行时,框架读取元数据并使用工厂函数提取参数值
- 管道处理:提取的值通过管道进行转换和验证
- 参数注入:处理后的值作为参数传递给路由处理函数
通过理解这些机制,我们可以:
- 更好地使用内置参数装饰器
- 创建自定义参数装饰器满足特定需求
- 优化参数处理性能
- 实现更复杂的参数验证和转换逻辑
在下一篇文章中,我们将探讨管道(Pipe)的执行时机:在 handler 之前拦截,深入了解 ValidationPipe 如何结合 class-validator 实现自动校验。