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 使用这些装饰器来实现自动的数据验证和转换:
@Post()
async create(@Body() createUserDto: CreateUserDto) {
// createUserDto 已经经过验证和转换
return this.usersService.create(createUserDto);
}
还可以创建自己的自定义装饰器
比如将属性附加到请求对象是常见的做法。然后在每个路由处理程序中手动提取它们,使用如下代码:
const user = req.user;
但是,使用装饰器可以更方便地实现这一点,让代码更简洁,并且可以在所有的控制器中复用。
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const User = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},
);
在任何控制器中,都可以使用 @User()
装饰器来获取用户信息。
@Get()
async findAll(@User() user: User) {
return user;
}
参数传递与数据提取
在实际开发中,装饰器往往需要根据不同场景动态调整其行为。通过 data 参数,我们可以实现装饰器的可配置性,使其更加灵活和可复用。这在处理复杂的用户信息提取场景中特别有用。
例如,在一个典型的身份认证系统中,认证中间件会将验证后的用户信息附加到请求对象上。这个用户实体通常包含丰富的信息:
{
"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
}
}
为了优雅地处理这种复杂对象,我们可以创建一个智能的参数装饰器。这个装饰器不仅可以获取整个用户对象,还能根据传入的属性路径精确提取所需信息:
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;
},
);
这样设计的装饰器提供了更强大和灵活的使用方式。在控制器中,你可以根据需要精确提取所需的用户信息:
@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 };
}
这种方式不仅让代码更简洁易读,还提供了以下优势:
- 类型安全 - 通过泛型参数可以确保类型检查
- 代码复用 - 避免重复编写属性访问逻辑
- 错误处理 - 集中处理异常情况
- 灵活性 - 支持深层属性访问
- 可维护性 - 统一的数据获取接口
提示
TypeScript 用户可以利用泛型来增强类型安全。
createParamDecorator<T>()
是泛型。这意味着你可以显式强制执行类型安全,例如 createParamDecorator<string>((data, ctx) => ...)
。或者,在工厂函数中指定参数类型,例如 createParamDecorator((data: string, ctx) => ...)
。如果两者都省略,则 data
的类型将为 any
。
export const User = createParamDecorator<keyof UserEntity>((data, ctx) => ...);
或者使用联合类型:
type UserPaths = 'firstName' | 'preferences.theme' | 'metadata.lastLogin';
export const User = createParamDecorator<UserPaths>((data, ctx) => ...);
使用管道
Nest 对自定义参数装饰器的处理方式与内置装饰器(如 @Body()
、@Param()
和 @Query()
)完全一致。这意味着你可以像使用内置装饰器一样,对自定义装饰器的参数应用管道进行验证和转换。
例如,在我们的 @User()
装饰器中,可以使用 ValidationPipe
来验证提取的用户数据是否符合预期格式:
@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
来组合多个装饰器。这种组合模式可以大大提高代码的可维护性和复用性。例如,在实际开发中,身份验证和授权往往需要多个装饰器配合使用,我们可以将这些相关的装饰器组合成一个更高级的装饰器:
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: '无效的认证凭证' }
}
}
}),
// 其他装饰器...
);
}
这样设计的装饰器可以在控制器中优雅地使用:
@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
一起使用