返回文章列表 →

Note

自定义装饰器

nestjs的自定义装饰器介绍

发布于 更新于

自定义装饰器

根据需求,封装自己的装饰器 @xxx() ,给类、方法或参数附加自定义元数据。NestJS根据作用把自定义装饰器分成了三类。自定义装饰器根据其职责创建在对应的目录下,且会创建 decorators 目录来放置,通常命名为 xxx.decorator.ts

分类

类型 例子 作用
自定义参数装饰器 @CurrentUser() 从请求中取出数据,传给控制器参数
自定义元数据装饰器 @Roles('admin') 给接口添加配置或标记
组合装饰器 @Auth('admin') 把多个装饰器合并成一个

自定义参数装饰器

通过 createParamDecorator() 创建,用于处理请求中的数据传给Controller

创建

current-user.decorator.ts

import { createParamDecorator, ExecutionContext } from "@nestjs/common";

export const CurrentUser = createParamDecorator(
  (data: unknown, context: ExecutionContext) => {
    const request = context.switchToHttp().getRequest();
    // 返回控给制器参数的值
    return request.user;
  },
);
createParamDecorator()的两个参数
  • data
    表示使用装饰器时传入的数据。如 @CurrentUser('id') ,那么data就是id
  • context
    表示当前请求的执行上下文,也就是 ExecutionContext

使用

@Get('profile')
getProfile(@CurrentUser() user: AuthUser) {
  return user;
}

详细说明

@CurrentUser() user 相当于让 NestJS 执行装饰器中的函数: const request = context.switchToHttp().getRequest(); return request.user; 概念上可以理解为控制器的参数user就是 request.user

这里这么做的意义是什么(这个需求里自定义装饰器具体作用)

  • 减少重复代码。如果很多个地方都要使用 request 中的 user ,每次都要 request.user ,这样很冗余,直接使用装饰器可以避免
  • 可以根据条件不同,返回不同的数据给控制器 比如用户传递单个信息,只返回单个信息给控制器,如果用户传递完整用户信息则返回完整用户信息给控制器:
interface AuthUser {
  id: number;
  username: string;
  email: string;
  roles: string[];
}

type RequestWithUser = {
  user?: AuthUser;
};

export const CurrentUser = createParamDecorator<keyof AuthUser | undefined>(
  (property: keyof AuthUser | undefined, context: ExecutionContext) => {
    const request = context.switchToHttp().getRequest<RequestWithUser>();

    const user = request.user;
    // 返回完整用户信息
    if (!property) {
      return user;
    }

    // 返回单个信息
    return user?.[property];
  },
);
// 获取完整信息
@Get('profile')
getProfile(@CurrentUser() user: AuthUser) {
  return user;
}

// 只获取用户ID
@Get('profile')
getProfile(@CurrentUser('id') userId: number) {
  return userId;
}

和管道配合使用

NestJS 会把自定义参数装饰器和 @Body()、@Param()、@Query() 等内置参数装饰器结合处理,因此可以配合Pipe

获取用户ID后转换成数字
@Get('profile')
getProfile(
  @CurrentUser('id', ParseIntPipe)
  userId: number,
) {
  return userId;
}
直接使用 ValidationPipe
@Get('profile')
getProfile(
  @CurrentUser(
    new ValidationPipe({
      validateCustomDecorators: true,
    }),
  )
  user: AuthUserDto,
) {
  return user;
}

// ValidationPipe 默认不会验证自定义参数装饰器产生的参数,需要配置validateCustomDecorators: true

自定义元数据装饰器

元数据装饰器用于给类/方法添加元数据。涉及到元数据,就必须介绍Reflector类和SetMetadata()装饰器

SetMetadata()

创建自定义元数据装饰器,需显示指定key

Reflector

是 NestJS 提供的元数据读取与装饰器创建工具类。它可以读取附加在类、方法上的元数据,也可以创建类型安全的元数据装饰。从 @nestjs/core 中引入

Reflector.createDecorator()

用于创建自定义元数据装饰器

创建装饰器
import { Reflector } from "@nestjs/core";

export enum Role {
  User = "user",
  Admin = "admin",
  Visitor = "visitor",
}

export const Roles =
  // 会返回元数据装饰器,这里是@Roles(['admin', 'user'])。没有显示指定key,内部会设置一个key:Role.KEY。createDecorator不推荐显示指定key
  Reflector.createDecorator<Role[]>();
使用装饰器
// 它会在后台将传入的参数(这里是包含角色的数组)作为元数据存储起来。后续可获取元数据用于守卫、拦截器
// 给类/方法添加元数据(键值对):"key:Role.KEY,value:['admin']"
@Roles([Role.Admin])
reflector.get()

在目标中根据传入装饰器(内部也是通过key查找)找到对应元数据并返回。如果目标中不存在对应的元数据则返回 undefined

this.reflector.get<返回值类型>(
  元数据key, // 使用createDecorator创建的元数据装饰器,且没有显示指定key,直接传入装饰器即可,Reflector 会从 Roles.KEY 中自动取得真正的 key
  目标, // 类/方法。get方法只能读取一个目标
);
绑定元数据
@Roles([Role.Admin])
@Controller("users")
export class UsersController {
  @Post()
  createUser() {}
}
获取元数据
// 返回值['admin']
const roles = this.reflector.get(Roles, context.getClass());
reflector.getAllAndOverride()

可传入多个目标,按优先级读取目标中的元数据,如果找到了(结果不为 undefined),立即返回结果,不会再进行后续的查找

this.reflector.getAllAndOverride<返回值类型>(
  元数据key,
  [目标1, 目标2, 目标3, ...],
);
绑定元数据
@Controller("users")
export class UsersController {
  @Post()
  @Roles([Role.Admin, Role.User])
  createUser() {}
}
获取元数据
// 先在"当前路由"中查询Roles元数据,得到了['admin', 'user']后直接返回,后续目标"当前类"不会再查找。如果在"当前路由"没有找到,才会继续在"当前类"中查找
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(Roles, [
  context.getHandler(),
  context.getClass(),
]);
getAllAndMerge()

可传入多个目标,依次读取目标中的元数据并合并,并且会过滤掉 undefined 值

创建装饰器
export interface CacheConfig {
  enabled?: boolean;
  ttl?: number;
}

export const CacheOptions = Reflector.createDecorator<CacheConfig>();
绑定元数据
@CacheOptions({
  enabled: true,
  ttl: 60,
})
@Controller("users")
export class UsersController {
  @CacheOptions({
    ttl: 10,
  })
  @Get()
  findAll() {}
}
获取元数据
const config = this.reflector.getAllAndMerge<CacheConfig>(CacheOptions, [
  context.getClass(),
  context.getHandler(),
]);

// 现在当前类中获取到元数据{enabled: true,ttl: 60},再从当前路由中获取元数据{ttl:10},之后合并对象最终返回:{enabled: true,ttl: 10}

自定义组合装饰器

创建一个装饰器,它会把多个装饰器合并成一个,应用这个装饰器相当于应用了里面所有合并的装饰器

applyDecorators

通过applyDecorators()把装饰合并到一起。从 @nestjs/common 引入

合并
import { applyDecorators, UseGuards } from "@nestjs/common";
import { ApiBearerAuth, ApiUnauthorizedResponse } from "@nestjs/swagger";
// ...其他必要引入

export function Auth(...roles: Role[]) {
  return applyDecorators(
    Roles(...roles),
    UseGuards(JwtAuthGuard, RolesGuard),
    ApiBearerAuth(),
    ApiUnauthorizedResponse({
      description: "未登录或 Token 无效",
    }),
  );
}
使用
@Auth(Role.Admin)
@Get()
findAll() {
  return '管理员数据';
}
相当于
@Roles(Role.Admin)
@UseGuards(JwtAuthGuard, RolesGuard)
@ApiBearerAuth()
@ApiUnauthorizedResponse({
  description: '未登录',
})
@Get()
findAll() {
  return '管理员数据';
}