自定义装饰器:结合 Reflector 和 ExecutionContext


自定义装饰器:结合 Reflector 和 ExecutionContext

NestJS 里的一个进阶玩法——自定义装饰器。别看装饰器平时就是 @Get()@Body() 这些现成的,其实你也可以自己造,而且造出来的装饰器往往能让代码更加简洁、语义更清晰。比如你可以封装一个 @User() 装饰器,直接获取当前登录用户,而不需要在每个控制器里写一堆重复的代码。

要实现自定义装饰器,我们得先了解两个核心工具:ReflectorExecutionContext。它们就像是装饰器的“后台助手”,帮你读取元数据、获取当前请求的上下文。


先理解两个关键角色

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() 就是这种。

实际开发中,参数装饰器最常见,因为它能直接注入请求相关的数据,减少重复代码。


注意事项

  1. 不要在装饰器里做复杂逻辑:装饰器应该尽量简单,只负责提取数据或附加元数据。复杂的业务逻辑应该放在服务层或守卫/拦截器里。
  2. 结合全局 Guards 使用:像上面的权限装饰器,配合全局守卫,就能实现声明式的权限控制。
  3. 利用 ExecutionContext 获取更多信息:比如想拿响应对象、获取 WebSocket 客户端等,都可以通过 switchToWs()switchToRpc() 来切换上下文。

总结

自定义装饰器是 NestJS 提供的一种非常灵活的扩展方式,它让我们能用声明式的写法替代重复的命令式代码。结合 ReflectorExecutionContext,我们可以读取元数据、获取当前请求的各种信息,从而实现如用户注入、权限标记、日志记录等功能。下次当你发现某个数据在多个控制器里反复手动提取时,不妨考虑封装一个自定义装饰器,让代码更优雅。

声明:麋鹿与鲸鱼|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - 自定义装饰器:结合 Reflector 和 ExecutionContext


Carpe Diem and Do what I like