自定义装饰器 (Custom Decorators)。
打造你的专属“魔法棒”:NestJS 自定义装饰器
如果你一直在跟着学习,你会发现 NestJS 的代码充满了 @Controller()
, @Get()
, @Body()
, @Injectable()
等以 @
符号开头的东西。这些就是装饰器。NestJS 框架本身就是构建在这些装饰器之上的。 它们允许我们用一种声明式、简洁易读的方式为类、方法或属性附加功能和元数据。
而 NestJS 最酷的一点是,它不仅提供了丰富的内置装饰器,还允许我们创建完全符合自己需求的自定义装饰器。 这就像是游戏里的“附魔”系统,你可以打造出自己专属的“魔法棒”,让代码变得更优雅、更强大。
1. 为什么需要自定义装饰器?—— 告别重复的代码
想象一个常见的场景:在你的应用中,许多需要用户登录后才能访问的接口。通常,用户信息(比如用户 ID、角色等)会在认证中间件或守卫中被解析出来,然后附加到请求对象(request
)上。
于是在你的 Controller
方法里,代码可能看起来是这样的:
// 在某个 Controller 中...
@Get('profile')
getProfile(@Req() req: Request) {
const user = req.user; // 从请求对象中手动获取 user
return this.userService.findOne(user.id);
}
@Post('articles')
createArticle(@Req() req: Request, @Body() createArticleDto: CreateArticleDto) {
const user = req.user; // 又一次手动获取 user
return this.articleService.create(user.id, createArticleDto);
}
看到了吗?const user = req.user;
这行代码在每个需要用户信息的路由处理函数中都重复出现了。虽然只有一行,但当这样的路由多了之后,就显得非常冗余和不优雅。
这时候,自定义装饰器就能大显身手了!我们可以创建一个 @User()
装饰器,直接帮我们把 req.user
提取出来。使用它之后,代码会变成这样:
// 使用自定义装饰器后
import { User } from '../auth/user.decorator'; // 引入我们的自定义装饰器
// ...
@Get('profile')
getProfile(@User() user: UserEntity) { // 代码是不是清爽多了?
return this.userService.findOne(user.id);
}
@Post('articles')
createArticle(@User() user: UserEntity, @Body() createArticleDto: CreateArticleDto) {
return this.articleService.create(user.id, createArticleDto);
}
通过自定义装饰器,我们把“从请求中提取用户信息”这个重复的逻辑封装了起来,让代码更具可读性和表现力。
2. 创建你的第一个自定义装饰器:@User()
在 NestJS 中,创建自定义参数装饰器非常简单,只需要使用内置的 createParamDecorator
工厂函数。
让我们来动手实现上面提到的 @User()
装饰器。
src/auth/user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const User = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
// 1. 获取请求对象
const request = ctx.switchToHttp().getRequest();
// 2. 返回请求中的 user 对象
return request.user;
},
);```
代码分析:
* **`createParamDecorator((data, ctx) => { ... })`**: 这是创建参数装饰器的核心函数。它接受一个回调函数作为参数。
* **`ctx: ExecutionContext`**: 这个参数我们已经很熟悉了,它是**执行上下文**,提供了访问当前请求处理过程的各种信息的能力。`ctx.switchToHttp().getRequest()` 可以帮助我们拿到当前 HTTP 请求的 `request` 对象。
* **`data: unknown`**: 这个参数可以用来在**使用装饰器时传递额外的数据**。我们稍后会看到它的用例。
* **`return request.user;`**: 这个回调函数的**返回值**,就是 NestJS 将会注入到你的路由处理函数参数中的值。在这里,我们返回了 `request.user`,所以 `@User()` 装饰器就会把这个值赋给它所装饰的参数。
就是这么简单!现在你就可以在你的控制器中像上面的例子一样使用 `@User()` 了。
#### 进阶用法:获取 User 对象中的特定属性
如果我们只想获取 `user` 对象中的某个特定属性,比如 `id` 或 `email`,怎么办?这时,`data` 参数就派上用场了。
我们可以这样改造我们的 `@User()` 装饰器:
**`src/auth/user.decorator.ts` (增强版)**
```typescript
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const User = createParamDecorator(
(data: string, ctx: ExecutionContext) => { // data 的类型现在是 string
const request = ctx.switchToHttp().getRequest();
const user = request.user;
// 如果传入了 data (例如 @User('id')),就返回 user[data]
// 否则 (例如 @User()),就返回整个 user 对象
return data ? user?.[data] : user;
},
);
现在,你可以这样使用它:
@Get('profile')
// @User() 不传参数,注入整个 user 对象
getProfile(@User() user: UserEntity) {
// ...
}
@Post('articles')
// @User('id') 传入 'id',只注入用户的 id
createArticle(@User('id') userId: number, @Body() dto: CreateArticleDto) {
// ...
}
3. 自定义装饰器与元数据 (Metadata)
除了参数装饰器,另一种非常重要的自定义装饰器是用来附加元数据 (Metadata) 的。
元数据,简单来说,就是“关于数据的数据”。在 NestJS 中,我们可以通过装饰器给路由处理函数附加一些额外的信息,然后在守卫 (Guards) 或拦截器 (Interceptors) 中读取这些信息,以实现更复杂的逻辑。
最典型的例子就是基于角色的访问控制 (RBAC)。
实战:创建 @Roles()
装饰器实现权限控制
我们的目标是:
- 创建一个
@Roles()
装饰器,用来标记某个路由需要什么角色才能访问。 - 创建一个
RolesGuard
(守卫),它会检查当前登录用户的角色是否满足@Roles()
装饰器指定的要求。
第一步:创建 @Roles()
装饰器
这个装饰器非常简单,它只是利用 NestJS 提供的 SetMetadata
函数,将角色信息附加到路由处理函数上。
src/auth/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
SetMetadata(key, value)
: 这个函数会创建一个装饰器,这个装饰器会将一个键值对 ('roles': ['admin']
) 作为元数据附加到它所装饰的目标上。
第二步:创建 RolesGuard
来读取元数据
守卫的 canActivate
方法是实现授权逻辑的地方。我们需要在这里读取之前通过 @Roles()
设置的元数据。
src/auth/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from './roles.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
// 1. 通过 Reflector 读取在路由处理函数上设置的元数据
const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
// 2. 如果没有设置 @Roles(),则默认允许访问
if (!requiredRoles) {
return true;
}
// 3. 获取请求中的 user 对象
const { user } = context.switchToHttp().getRequest();
// 4. 判断用户的角色是否包含任何一个所需角色
return requiredRoles.some((role) => user.roles?.includes(role));
}
}```
* **`private reflector: Reflector`**: `Reflector` 是 NestJS 提供的辅助类,专门用来在守卫、拦截器等地方轻松读取元数据。
* **`reflector.getAllAndOverride(...)`**: 这是一个非常有用的方法,它会查找在**方法层面**和**类层面**设置的元数据,并且方法层面的会覆盖类层面的。这让我们可以灵活地进行权限设置。
**第三步:在控制器中使用它们**
现在,我们可以像搭积木一样,把 `@Roles()` 装饰器和 `RolesGuard` 组合起来使用。
```typescript
import { Controller, Get, UseGuards } from '@nestjs/common';
import { Roles } from '../auth/roles.decorator';
import { RolesGuard } from '../auth/roles.guard';
import { AuthGuard } from '@nestjs/passport'; // 假设你使用了 Passport 做认证
@Controller('admin')
@UseGuards(AuthGuard('jwt'), RolesGuard) // 先认证,再鉴权
export class AdminController {
@Get('dashboard')
@Roles('admin') // 这个路由只有 'admin' 角色的用户才能访问
getDashboard() {
return { message: 'Welcome to the admin dashboard!' };
}
@Get('logs')
@Roles('admin', 'operator') // 'admin' 或 'operator' 角色都可以访问
getLogs() {
return { message: 'System logs...' };
}
}
看到了吗?通过自定义装饰器和守卫的配合,我们以一种非常声明式和清晰的方式实现了复杂的权限控制逻辑。代码的核心业务(返回 dashboard 信息)和权限逻辑完全分离,大大提高了代码的可维护性。
总结
自定义装饰器是 NestJS 框架的精髓之一。它能帮助我们:
- 简化代码:通过创建自定义参数装饰器(如
@User()
)来消除控制器中的重复代码。 - 增强表现力:让代码的意图更加清晰,比如
@Roles('admin')
一眼就能看出该接口的权限要求。 - 实现关注点分离:将授权、日志、缓存等横切关注点的逻辑与核心业务逻辑解耦。
当你发现自己在不同的控制器或服务中反复编写相同的逻辑时,不妨停下来想一想:是否可以创建一个自定义装饰器来将这些逻辑封装起来?熟练运用自定义装饰器,是成为一名优秀 NestJS 开发者的必经之路。