Skip to content

自定义装饰器 (Custom Decorators)

打造你的专属“魔法棒”:NestJS 自定义装饰器

如果你一直在跟着学习,你会发现 NestJS 的代码充满了 @Controller(), @Get(), @Body(), @Injectable() 等以 @ 符号开头的东西。这些就是装饰器。NestJS 框架本身就是构建在这些装饰器之上的。 它们允许我们用一种声明式、简洁易读的方式为类、方法或属性附加功能和元数据。

而 NestJS 最酷的一点是,它不仅提供了丰富的内置装饰器,还允许我们创建完全符合自己需求的自定义装饰器。 这就像是游戏里的“附魔”系统,你可以打造出自己专属的“魔法棒”,让代码变得更优雅、更强大。

1. 为什么需要自定义装饰器?—— 告别重复的代码

想象一个常见的场景:在你的应用中,许多需要用户登录后才能访问的接口。通常,用户信息(比如用户 ID、角色等)会在认证中间件或守卫中被解析出来,然后附加到请求对象(request)上。

于是在你的 Controller 方法里,代码可能看起来是这样的:

typescript
// 在某个 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 提取出来。使用它之后,代码会变成这样:

typescript
// 使用自定义装饰器后
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

typescript
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;
  },
);

现在,你可以这样使用它:

typescript
@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() 装饰器实现权限控制

我们的目标是:

  1. 创建一个 @Roles() 装饰器,用来标记某个路由需要什么角色才能访问。
  2. 创建一个 RolesGuard(守卫),它会检查当前登录用户的角色是否满足 @Roles() 装饰器指定的要求。

第一步:创建 @Roles() 装饰器

这个装饰器非常简单,它只是利用 NestJS 提供的 SetMetadata 函数,将角色信息附加到路由处理函数上。

src/auth/roles.decorator.ts

typescript
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

typescript
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 开发者的必经之路。