FORMA

自定义装饰器

SetMetadatacreateParamDecorator 复用元数据与参数提取。

NestJS 内置了大量装饰器(@Get@Post@Body@Req 等),但有时需要创建自己的装饰器来复用通用逻辑或提取特定数据。自定义装饰器可以分为参数装饰器方法/类装饰器两类。

一、自定义参数装饰器(createParamDecorator

参数装饰器用于从请求中提取特定数据,比如当前登录用户、分页参数、请求头中的某字段。

使用 createParamDecorator 工厂函数创建,它接收两个参数:

  • data:装饰器传入的静态参数(可选)。
  • ctxExecutionContext 对象,可以获取请求实例。

示例:提取当前用户

假设在认证守卫中已经将用户信息挂载到 request.user,我们可以创建一个 @CurrentUser() 装饰器直接获取:

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

export const CurrentUser = createParamDecorator((data: unknown, ctx: ExecutionContext) => {
  const request = ctx.switchToHttp().getRequest();
  return request.user; // 返回用户对象
});

在控制器中使用:

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

传递参数给装饰器createParamDecorator 的第一个参数 data 就是调用时传入的值。

ts
export const CurrentUser = createParamDecorator(
  (key: string, ctx: ExecutionContext) => {
    const user = ctx.switchToHttp().getRequest().user;
    return key ? user?.[key] : user;
  },
);

// 使用
@Get()
getUserEmail(@CurrentUser('email') email: string) { ... }

二、结合管道使用

自定义参数装饰器可以像内置参数装饰器(@Body@Query)一样与管道配合使用。只需在参数声明时添加管道即可。

ts
@Get(':id')
findOne(
  @Param('id', ParseIntPipe) id: number,
  @CurrentUser(ValidationPipe) user: UserEntity,
) { ... }

管道会在装饰器提取数据之后执行,对提取的结果进行验证或转换。

三、组合装饰器(applyDecorators

当需要将多个装饰器组合成一个时,可以使用 applyDecorators 函数。它接受多个装饰器并返回一个组合装饰器。

示例:创建一个 @Public() 装饰器,用于标记公开路由(跳过认证守卫)

通常我们会通过 @SetMetadata 设置元数据,然后守卫读取该元数据。可以封装成更语义化的装饰器:

ts
import { SetMetadata, applyDecorators } from "@nestjs/common";

export const Public = () =>
  applyDecorators(
    SetMetadata("isPublic", true),
    // 还可以组合其他装饰器,如 @HttpCode(200) 等
  );

然后在控制器方法上使用 @Public(),守卫中检查 isPublic 元数据即可。

批量组合示例:创建一个 @ApiPaginatedResponse 装饰器,组合 Swagger 装饰器。

ts
import { applyDecorators, Type } from "@nestjs/common";
import { ApiOkResponse, getSchemaPath } from "@nestjs/swagger";

export function ApiPaginatedResponse<T extends Type<any>>(model: T) {
  return applyDecorators(
    ApiOkResponse({
      schema: {
        allOf: [
          { properties: { data: { type: "array", items: { $ref: getSchemaPath(model) } } } },
          { properties: { total: { type: "number" } } },
        ],
      },
    }),
  );
}

四、自定义方法/类装饰器(基于 SetMetadata 与反射)

除了参数装饰器,还可以为方法或类创建装饰器,通常会结合 SetMetadata 设置元数据,然后在守卫或拦截器中通过 Reflector 读取。

1. 直接使用 SetMetadata

ts
@SetMetadata('roles', ['admin'])
@Get()
findAll() { ... }

2. 封装为自定义装饰器

ts
import { SetMetadata } from "@nestjs/common";

export const Roles = (...roles: string[]) => SetMetadata("roles", roles);

使用:

ts
@Roles('admin', 'moderator')
@Delete(':id')
remove(@Param('id') id: number) { ... }

3. 组合多个元数据

ts
import { applyDecorators, SetMetadata } from "@nestjs/common";

export const Auth = (role: string) =>
  applyDecorators(SetMetadata("role", role), SetMetadata("requiresAuth", true));

然后在守卫中通过 Reflector 读取:

ts
const role = this.reflector.get<string>("role", context.getHandler());
const requiresAuth = this.reflector.get<boolean>("requiresAuth", context.getHandler());

4. 更复杂的类装饰器

可以通过 @Injectable() 或自定义类装饰器来实现类似继承的功能,但更常见的是用元数据 + 守卫。

五、总结

类型创建方式典型用途
参数装饰器createParamDecorator提取请求数据(用户、分页、设备信息)
组合装饰器applyDecorators聚合多个装饰器,简化代码
方法/类装饰器SetMetadata + Reflector设置权限、公开路由等元数据

自定义装饰器是 NestJS 中非常强大的扩展能力,能帮助你将横切关注点从控制器中抽离,使代码更简洁、可维护。

参考文献

以下链接在编写时均可正常访问:

资料说明
NestJS 文档官方
Custom decorators本章主题
Request lifecycle执行顺序