Skip to content

NestJS 自定义装饰器

目录

自定义路由装饰器

Nest.js 框架的核心特性之一就是装饰器。装饰器是一种强大的元编程特性,在许多主流编程语言中都有实现,比如 Python 的装饰器、Java 的注解等。虽然在 JavaScript 生态中,装饰器相对较新,但它已经在 TypeScript 中得到了广泛应用。为了更好地理解装饰器的工作原理,我们先来看看它的基本概念:

快速上手装饰器

NestJS 在框架层面大量使用装饰器来简化开发。比如在路由处理方面,它提供了一系列 HTTP 方法装饰器:@Get()用于处理 GET 请求,@Post()用于处理 POST 请求,此外还有@Put()@Delete()@Patch()@Options()@Head()以及通用的@All()等。这些装饰器不仅定义了路由的处理方法,还可以携带路径参数和其他配置选项。

从技术角度来看,装饰器本质上是一个返回函数的表达式,它可以接收三个参数:目标对象、属性名称和属性描述对象。通过在类、方法或属性定义前添加 @ 符号来使用装饰器。装饰器的执行时机是在代码解析阶段,而不是运行时,这使得它能够在编译期间修改或增强目标代码的行为。

参数装饰器

Nest 提供了一组功能强大的参数装饰器,它们可以与 HTTP 路由处理程序无缝集成,大大简化了请求处理的过程。这些装饰器本质上是对底层框架(Express 或 Fastify)请求对象的智能封装,让开发者可以以更优雅和类型安全的方式访问请求数据。

下面是 NestJS 提供的核心参数装饰器及其对应的底层对象映射关系:

装饰器对应对象使用场景
@Request(), @Req()req获取完整的请求对象
@Response(), @Res()res获取响应对象,用于自定义响应
@Next()next获取 next 函数,用于中间件流程控制
@Session()req.session访问会话数据,用于状态管理
@Param(param?: string)req.params / req.params[param]获取路由参数,如 /users/:id 中的 id
@Body(param?: string)req.body / req.body[param]获取请求体数据,常用于 POST/PUT 请求
@Query(param?: string)req.query / req.query[param]获取查询字符串参数,如 ?page=1 中的 page
@Headers(param?: string)req.headers / req.headers[param]访问请求头信息,用于获取认证令牌等元数据
@Ip()req.ip获取客户端 IP 地址
@HostParam()req.hosts获取主机名信息,用于子域名路由等场景

这些装饰器不仅提供了类型安全的参数访问,还支持参数转换和验证。例如,你可以结合 class-validator 和 class-transformer 使用这些装饰器来实现自动的数据验证和转换:

typescript
@Post()
async create(@Body() createUserDto: CreateUserDto) {
  // createUserDto 已经经过验证和转换
  return this.usersService.create(createUserDto);
}

还可以创建自己的自定义装饰器

比如将属性附加到请求对象是常见的做法。然后在每个路由处理程序中手动提取它们,使用如下代码:

ts
const user = req.user;

但是,使用装饰器可以更方便地实现这一点,让代码更简洁,并且可以在所有的控制器中复用。

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

export const User = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return request.user;
  },
);

在任何控制器中,都可以使用 @User() 装饰器来获取用户信息。

ts
@Get()
async findAll(@User() user: User) {
  return user;
}

参数传递与数据提取

在实际开发中,装饰器往往需要根据不同场景动态调整其行为。通过 data 参数,我们可以实现装饰器的可配置性,使其更加灵活和可复用。这在处理复杂的用户信息提取场景中特别有用。

例如,在一个典型的身份认证系统中,认证中间件会将验证后的用户信息附加到请求对象上。这个用户实体通常包含丰富的信息:

json
{
  "id": 101,
  "firstName": "Alan",
  "lastName": "Turing",
  "email": "[email protected]",
  "roles": ["admin"],
  "preferences": {
    "theme": "dark",
    "notifications": true
  },
  "metadata": {
    "lastLogin": "2023-01-01T00:00:00Z",
    "loginCount": 42
  }
}

为了优雅地处理这种复杂对象,我们可以创建一个智能的参数装饰器。这个装饰器不仅可以获取整个用户对象,还能根据传入的属性路径精确提取所需信息:

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

export const User = createParamDecorator(
  (data: string, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    const user = request.user;

    // 增强的错误处理
    if (!user) {
      throw new BadRequestException('User not found in request');
    }

    // 支持嵌套属性访问,如 preferences.theme
    if (data) {
      const value = data.split('.').reduce((obj, key) => obj?.[key], user);
      if (value === undefined) {
        throw new BadRequestException(`Property ${data} not found in user object`);
      }
      return value;
    }

    return user;
  },
);

这样设计的装饰器提供了更强大和灵活的使用方式。在控制器中,你可以根据需要精确提取所需的用户信息:

ts
@Get('profile')
async getProfile(
  @User('firstName') firstName: string,
  @User('preferences.theme') theme: string,
  @User('metadata.lastLogin') lastLogin: Date
) {
  console.log(`User ${firstName} last logged in at ${lastLogin} with ${theme} theme`);
  return { firstName, theme, lastLogin };
}

这种方式不仅让代码更简洁易读,还提供了以下优势:

  1. 类型安全 - 通过泛型参数可以确保类型检查
  2. 代码复用 - 避免重复编写属性访问逻辑
  3. 错误处理 - 集中处理异常情况
  4. 灵活性 - 支持深层属性访问
  5. 可维护性 - 统一的数据获取接口

提示

TypeScript 用户可以利用泛型来增强类型安全。

createParamDecorator<T>() 是泛型。这意味着你可以显式强制执行类型安全,例如 createParamDecorator<string>((data, ctx) => ...)。或者,在工厂函数中指定参数类型,例如 createParamDecorator((data: string, ctx) => ...)。如果两者都省略,则 data 的类型将为 any

ts
export const User = createParamDecorator<keyof UserEntity>((data, ctx) => ...);

或者使用联合类型:

ts
type UserPaths = 'firstName' | 'preferences.theme' | 'metadata.lastLogin';
export const User = createParamDecorator<UserPaths>((data, ctx) => ...);

使用管道

Nest 对自定义参数装饰器的处理方式与内置装饰器(如 @Body()@Param()@Query())完全一致。这意味着你可以像使用内置装饰器一样,对自定义装饰器的参数应用管道进行验证和转换。

例如,在我们的 @User() 装饰器中,可以使用 ValidationPipe 来验证提取的用户数据是否符合预期格式:

ts
@Get()
async findOne(
  @User(new ValidationPipe({ 
    validateCustomDecorators: true,  // 启用自定义装饰器验证
    transform: true,                 // 启用数据转换
    whitelist: true                  // 过滤掉未定义的属性
  }))
  user: UserEntity,
) {
  console.log('验证后的用户数据:', user);
  return this.userService.process(user);
}

重要提示

  • 使用 ValidationPipe 时必须设置 validateCustomDecorators: true,因为默认情况下它不会验证自定义装饰器的参数
  • 可以配合 class-validator 的装饰器在 UserEntity 类中定义验证规则
  • 管道不仅可以验证数据,还能进行类型转换和数据清理

装饰器组成

Nest 提供了一个强大的辅助方法 applyDecorators 来组合多个装饰器。这种组合模式可以大大提高代码的可维护性和复用性。例如,在实际开发中,身份验证和授权往往需要多个装饰器配合使用,我们可以将这些相关的装饰器组合成一个更高级的装饰器:

ts
import { applyDecorators } from '@nestjs/common';

export function Auth(...roles: Role[]) {
  return applyDecorators(
    // 设置角色元数据
    SetMetadata('roles', roles),
    // 应用认证和角色守卫
    UseGuards(AuthGuard, RolesGuard),
    // Swagger 文档相关装饰器
    ApiBearerAuth(),
    ApiUnauthorizedResponse({ 
      description: '未经授权的访问',
      schema: {
        type: 'object',
        properties: {
          statusCode: { type: 'number', example: 401 },
          message: { type: 'string', example: '无效的认证凭证' }
        }
      }
    }),
    // 其他装饰器...
  );
}

这样设计的装饰器可以在控制器中优雅地使用:

ts
@Controller('users')
export class UsersController {
  @Get()
  @Auth('admin', 'super-admin')  // 一行代码完成所有认证和授权配置
  @ApiOperation({ summary: '获取用户列表' })
  findAllUsers() {
    return this.usersService.findAll();
  }

  @Post()
  @Auth('admin')  // 不同接口可以指定不同的角色要求
  @ApiOperation({ summary: '创建新用户' })
  createUser(@Body() dto: CreateUserDto) {
    return this.usersService.create(dto);
  }
}

这种组合装饰器的方式带来了多重好处:

  • 代码复用:避免在每个需要认证的接口上重复编写相同的装饰器组合
  • 关注点分离:认证相关的配置被封装在一个地方,便于维护和修改
  • 更清晰的语义@Auth() 比一堆单独的装饰器更能表达代码意图
  • 类型安全:可以限制角色参数的类型,避免拼写错误

注意事项@nestjs/swagger 包中的 @ApiHideProperty() 装饰器不可组合,无法与 applyDecorators 一起使用