自定义装饰器:结合 Reflector 和 ExecutionContext
NestJS 里的一个进阶玩法——自定义装饰器。别看装饰器平时就是 @Get()、@Body() 这些现成的,其实你也可以自己造,而且造出来的装饰器往往能让代码更加简洁、语义更清晰。比如你可以封装一个 @User() 装饰器,直接获取当前登录用户,而不需要在每个控制器里写一堆重复的代码。
要实现自定义装饰器,我们得先了解两个核心工具:Reflector 和 ExecutionContext。它们就像是装饰器的“后台助手”,帮你读取元数据、获取当前请求的上下文。
先理解两个关键角色
Reflector:元数据读写器
NestJS 内部大量使用了反射机制,说白了就是在类、方法、参数上附加一些“隐藏信息”,然后在需要的时候读取出来。Reflector 就是一个用来读取这些元数据的工具类。比如我们用 @SetMetadata() 往某个处理函数上贴了一个 roles 数组,就可以在守卫或拦截器里用 Reflector 把它取出来。
@SetMetadata('roles', ['admin'])
@Get('admin')
getAdminData() {}然后在守卫里:
const roles = this.reflector.get<string[]>('roles', context.getHandler());ExecutionContext:当前执行环境的万能钥匙
ExecutionContext 是 Nest 对当前请求处理上下文的封装,它继承自 ArgumentsHost。它能告诉你当前是在处理 HTTP 请求、WebSocket 消息还是 RPC 调用,还能拿到当前控制器类和处理函数。对于自定义装饰器来说,它可以帮助我们获取请求对象、响应对象等。
例如,在自定义装饰器里,我们可能会这样获取请求:
const request = ctx.switchToHttp().getRequest();自己动手写一个自定义装饰器
假设我们经常需要从请求里取出用户信息(比如从 token 解析出来的用户),每次都在控制器里写 const user = req.user; 太麻烦。我们可以封装一个 @CurrentUser() 参数装饰器。
第一步:创建装饰器文件
Nest 提供了 createParamDecorator 函数,专门用来创建参数级别的自定义装饰器。
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const CurrentUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user; // 假设你已经在中间件或守卫里把用户挂到了 request 上
},
);就这么简单!然后在控制器里直接用:
@Get('profile')
getProfile(@CurrentUser() user: User) {
return user;
}第二步:带上参数让装饰器更灵活
有时候我们只想取用户的某个字段,比如 id,可以给装饰器传参:
export const CurrentUser = createParamDecorator(
(data: string, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user;
return data ? user?.[data] : user;
},
);用法:
@Get('profile')
getProfile(@CurrentUser('id') userId: string) {
return userId;
}结合 Reflector 读取元数据的装饰器
有时候自定义装饰器不只是取数据,还要结合元数据做一些逻辑。比如我们想写一个 @Permissions() 装饰器,给每个接口打上需要的权限标记,然后在守卫里统一校验。
先定义装饰器:
import { SetMetadata } from '@nestjs/common';
export const Permissions = (...permissions: string[]) =>
SetMetadata('permissions', permissions);然后在控制器里用:
@Post()
@Permissions('create:user')
createUser() {}接着在守卫里用 Reflector 读取元数据:
@Injectable()
export class PermissionsGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredPermissions = this.reflector.get<string[]>('permissions', context.getHandler());
if (!requiredPermissions) {
return true; // 没有设置权限标记,直接放行
}
const request = context.switchToHttp().getRequest();
const userPermissions = request.user.permissions; // 假设从 token 中拿到用户权限
return requiredPermissions.every(p => userPermissions.includes(p));
}
}这样就把权限校验和业务代码完全分离开了,清爽!
装饰器的几种类型
Nest 支持三种类型的自定义装饰器:
- 类装饰器:用
@Injectable()那种,很少自己写。 - 方法装饰器:用
@Get()那种,可以通过SetMetadata附加元数据。 - 参数装饰器:用
@Body()那种,上面我们写的@CurrentUser()就是这种。
实际开发中,参数装饰器最常见,因为它能直接注入请求相关的数据,减少重复代码。
注意事项
- 不要在装饰器里做复杂逻辑:装饰器应该尽量简单,只负责提取数据或附加元数据。复杂的业务逻辑应该放在服务层或守卫/拦截器里。
- 结合全局 Guards 使用:像上面的权限装饰器,配合全局守卫,就能实现声明式的权限控制。
- 利用 ExecutionContext 获取更多信息:比如想拿响应对象、获取 WebSocket 客户端等,都可以通过
switchToWs()或switchToRpc()来切换上下文。
总结
自定义装饰器是 NestJS 提供的一种非常灵活的扩展方式,它让我们能用声明式的写法替代重复的命令式代码。结合 Reflector 和 ExecutionContext,我们可以读取元数据、获取当前请求的各种信息,从而实现如用户注入、权限标记、日志记录等功能。下次当你发现某个数据在多个控制器里反复手动提取时,不妨考虑封装一个自定义装饰器,让代码更优雅。

Comments | NOTHING