自定义装饰器
一、为什么需要自定义装饰器
NestJS 的内置装饰器(@Param、@Body、@Headers 等)已经很强大,但实际项目中常有重复的提取逻辑:
typescript
// ❌ 每个接口都重复这段代码
@Get('profile')
getProfile(@Req() req: Request) {
const user = req.user; // 到处重复
return user;
}
// ✅ 封装为自定义装饰器
@Get('profile')
getProfile(@CurrentUser() user: User) {
return user;
}自定义装饰器让代码更语义化、复用性更高、测试更简单。
二、参数装饰器(createParamDecorator)
createParamDecorator 用于创建提取请求数据的装饰器,取代 @Req() 后的手动提取。
基础用法
typescript
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
// 提取当前登录用户
export const CurrentUser = createParamDecorator(
(data: string | undefined, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user; // 由 JwtAuthGuard 注入
// data 是传入装饰器的参数
return data ? user?.[data] : user;
},
);
// 使用方式一:获取完整 user 对象
@Get('profile')
getProfile(@CurrentUser() user: User) {
return user;
}
// 使用方式二:获取 user 的某个字段
@Get('me')
getMyId(@CurrentUser('id') id: number) {
return { id };
}提取请求 IP
typescript
export const ClientIp = createParamDecorator(
(_data: unknown, ctx: ExecutionContext): string => {
const request = ctx.switchToHttp().getRequest<Request>();
// 支持代理(nginx、CDN)透传的真实 IP
return (
(request.headers['x-forwarded-for'] as string)?.split(',')[0]?.trim() ||
request.headers['x-real-ip'] as string ||
request.socket.remoteAddress ||
'0.0.0.0'
);
},
);
@Post('login')
login(@ClientIp() ip: string, @Body() dto: LoginDto) {
this.authService.recordLoginAttempt(ip, dto.email);
}提取 traceId
typescript
export const TraceId = createParamDecorator(
(_data: unknown, ctx: ExecutionContext): string => {
const request = ctx.switchToHttp().getRequest<
Request & { traceId?: string }
>();
return request.traceId ?? '';
},
);
@Get('data')
getData(@TraceId() traceId: string) {
this.logger.log(`Processing request ${traceId}`);
}带类型安全的参数装饰器
typescript
// 声明 data 参数的类型,提供 IDE 提示
export const CurrentUser = createParamDecorator(
(data: keyof User | undefined, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user: User = request.user;
return data ? user[data] : user;
},
) as {
(): ParameterDecorator;
(data: keyof User): ParameterDecorator;
};三、元数据装饰器(SetMetadata)
用于向路由处理方法或控制器类附加元数据,配合 Guard/Interceptor 中的 Reflector 读取。
基础模式
typescript
import { SetMetadata } from '@nestjs/common';
// 简单版本
export const Roles = (...roles: Role[]) => SetMetadata('roles', roles);
// 带类型安全的工厂函数版本(推荐)
export const ROLES_KEY = 'roles';
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);公开接口装饰器
typescript
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
// 在 JwtAuthGuard 中读取
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) return true;操作权限装饰器
typescript
export enum Action { Read = 'read', Create = 'create', Update = 'update', Delete = 'delete' }
export enum Resource { User = 'user', Post = 'post', Comment = 'comment' }
export interface Permission {
action: Action;
resource: Resource;
}
export const RequirePermission = (action: Action, resource: Resource) =>
SetMetadata('permission', { action, resource });
// 使用
@RequirePermission(Action.Delete, Resource.Post)
@Delete(':id')
deletePost(@Param('id') id: string) {}四、组合装饰器(applyDecorators)
applyDecorators 将多个装饰器合并为一个,消除重复代码:
认证守护装饰器
typescript
import { applyDecorators, UseGuards, SetMetadata, HttpCode, HttpStatus } from '@nestjs/common';
import { ApiBearerAuth, ApiUnauthorizedResponse } from '@nestjs/swagger';
export function Auth(...roles: Role[]) {
return applyDecorators(
SetMetadata('roles', roles),
UseGuards(JwtAuthGuard, RolesGuard),
ApiBearerAuth(), // Swagger 文档
ApiUnauthorizedResponse({ description: '未授权' }), // Swagger 文档
);
}
// 使用:替代 4 行代码
@Auth(Role.ADMIN)
@Delete(':id')
remove(@Param('id') id: string) {}API 路由标准套件
typescript
export function ApiRoute(options: {
summary: string;
roles?: Role[];
isPublic?: boolean;
}) {
const decorators: MethodDecorator[] = [
ApiOperation({ summary: options.summary }),
];
if (options.isPublic) {
decorators.push(Public());
} else {
decorators.push(
SetMetadata('roles', options.roles ?? []),
UseGuards(JwtAuthGuard, RolesGuard),
ApiBearerAuth(),
);
}
return applyDecorators(...decorators);
}
// 使用
@Get()
@ApiRoute({ summary: '获取用户列表', roles: [Role.ADMIN] })
findAll() {}
@Get('health')
@ApiRoute({ summary: '健康检查', isPublic: true })
health() {}五、属性装饰器
用于 DTO 类的属性标注(class-validator 中大量使用此模式):
typescript
import { registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator';
// 自定义:两个密码字段必须一致
export function IsEqualTo(property: string, validationOptions?: ValidationOptions) {
return function (object: object, propertyName: string) {
registerDecorator({
name: 'isEqualTo',
target: object.constructor,
propertyName,
constraints: [property],
options: validationOptions,
validator: {
validate(value: any, args: ValidationArguments) {
const [relatedPropertyName] = args.constraints;
const relatedValue = (args.object as any)[relatedPropertyName];
return value === relatedValue;
},
defaultMessage(args: ValidationArguments) {
return `${args.property} 与 ${args.constraints[0]} 不一致`;
},
},
});
};
}
// 使用
export class RegisterDto {
@IsString()
@MinLength(8)
password: string;
@IsEqualTo('password')
confirmPassword: string;
}六、在自定义装饰器中使用管道
参数装饰器可以组合管道进行数据转换:
typescript
// 自动将参数转换为 ParsedQuery 对象并验证
export const PaginationQuery = () =>
Query(
new ValidationPipe({
transform: true,
transformOptions: { enableImplicitConversion: true },
whitelist: true,
}),
);
export class PaginationDto {
@IsOptional()
@IsInt()
@Min(1)
@Type(() => Number)
page?: number = 1;
@IsOptional()
@IsInt()
@Min(1)
@Max(100)
@Type(() => Number)
limit?: number = 10;
}
// 使用
@Get()
findAll(@PaginationQuery() pagination: PaginationDto) {
return this.service.findPaginated(pagination.page, pagination.limit);
}七、最佳实践
- 集中管理装饰器:在
src/common/decorators/目录统一存放 - 导出常量 Key:避免字符串字面量散落各处typescript
export const ROLES_KEY = 'roles'; export const IS_PUBLIC_KEY = 'isPublic'; - 优先用组合装饰器:将常见的 Guard + Roles + Swagger 组合封装为
@Auth() - 类型安全:
createParamDecorator中声明data参数的类型 - 测试装饰器行为:通过
Reflector.get()在单元测试中验证元数据是否正确设置
可运行 Demo:
practice/04-auth-system— @CurrentUser() 和 @Roles() 自定义装饰器实现
常见错误
| 错误 | 原因 | 解决 |
|---|---|---|
createParamDecorator 无法注入 Service | 参数装饰器无法使用 DI | 改用 Guard + @SetMetadata 组合,或用 ModuleRef 手动解析 |
| 组合装饰器中 Guard 顺序错误 | applyDecorators 顺序与执行顺序不同 | Guard 先于 Interceptor 执行;装饰器应用顺序从下到上 |
| 元数据 key 冲突 | 多个装饰器用了同名 string key | 用 Symbol 作为 metadata key 避免命名冲突 |