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 '管理员数据';
}