NestJS 有个看起来很神奇的地方:在类或方法上加一个 @Something(),框架就能理解你的意图。

@Controller('users')  // 这是个控制器,处理 /users 路由
export class UserController {
  @Get()              // 这是个 GET 请求
  @Roles('admin')     // 需要 admin 权限
  getUsers() {}
}

这些 @ 开头的东西叫装饰器(Decorator),它们看起来像是在给代码”做标记”。那么问题来了:

@Controller 怎么让 NestJS 知道这是个控制器?@Get() 怎么让框架知道这是个 GET 路由? @Roles('admin') 怎么让 Guard 知道需要检查权限?

答案是:所有这些装饰器,本质上都是在写 Metadata(元数据)

可以把 Metadata 理解成给代码贴便签

  • 装饰器负责”贴便签”(写入信息)
  • NestJS 负责”读便签”(读取信息并执行对应逻辑)

从一个例子开始

假设我们要实现权限控制:只有 admin 可以删除用户。

传统方式可能是传参数:

// ❌ 参数方式:侵入业务逻辑
deleteUser({ requiredRole: 'admin' }) {
  // 业务代码要关心权限
}

NestJS 的方式:

// ✅ 装饰器方式:声明式,业务代码干净
@Roles('admin')
deleteUser() {
  // 业务代码不关心权限
}

@Roles('admin') 做了什么?它在 deleteUser 方法上贴了一张便签,写着:“这个方法需要 admin 角色”。

当请求到来时,RolesGuard 会去读这张便签,然后决定是否放行。

核心流程

  1. 装饰器在编译时写入 Metadata
  2. Guard 在运行时读取 Metadata
  3. 根据读到的信息做决策(放行或拒绝)

Metadata 底层:reflect-metadata 库

“贴便签”这个比喻背后,是 JavaScript 的 reflect-metadata 库:

// 写入:给 deleteUser 方法贴便签
Reflect.defineMetadata('roles', ['admin'], UserController.prototype, 'deleteUser');
 
// 读取:需要的时候读取便签
const roles = Reflect.getMetadata('roles', UserController.prototype, 'deleteUser');
// ['admin']

三个关键参数

  • key:便签内容的标识('roles'
  • target:贴在哪个对象上(UserController.prototype
  • propertyKey:具体哪个方法('deleteUser'

这就是 Metadata 的本质:一个键值对存储系统,可以在任何对象或方法上存储额外信息。

装饰器 = 写 Metadata 的函数

@Roles('admin') 看起来很特殊,但它就是个函数:

export const Roles = (...roles: string[]) => {
  return (target: any, propertyKey: string) => {
    Reflect.defineMetadata('roles', roles, target, propertyKey);
  };
};

当你写下:

@Roles('admin', 'moderator')
deleteUser() {}

执行过程是:

  1. Roles('admin', 'moderator') 被调用,返回一个装饰器函数
  2. 装饰器函数接收 targetpropertyKey
  3. 调用 Reflect.defineMetadata 存储信息

所以装饰器的本质就是:编译时执行的函数,用来写 Metadata

NestJS 的所有装饰器都遵循这个规律:

  • @Controller('users') → 写入 path: 'users'
  • @Get() → 写入 method: 'GET'
  • @UseGuards(RolesGuard) → 写入 guards: [RolesGuard]
  • @Injectable() → 标记这个类可以被依赖注入

Reflector:读取 Metadata

装饰器负责写,那谁来读?NestJS 提供了 Reflector

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}
  
  canActivate(context: ExecutionContext): boolean {
    const roles = this.reflector.get<string[]>('roles', context.getHandler());
    if (!roles) return true;
    
    // ... 检查用户角色
    return roles.some(role => request.user?.roles?.includes(role));
  }
}

关键代码:

this.reflector.get<string[]>('roles', context.getHandler())
  • context.getHandler():当前请求对应的方法(如 deleteUser
  • 从这个方法上读取 'roles' 的值

类和方法都有装饰器怎么办?

实际场景中,装饰器可能同时出现在类和方法上:

@Controller('users')
@Roles('user')  // 类级别:所有方法默认需要 user 角色
export class UserController {
  @Get()
  getUsers() {}
  
  @Delete(':id')
  @Roles('admin')  // 方法级别:覆盖成 admin
  deleteUser() {}
}

Reflector 提供三种策略:

1. get() - 只读取指定位置

const roles = this.reflector.get<string[]>('roles', context.getHandler());

只看方法上有没有,不管类上的。

2. getAllAndOverride() - 就近原则(最常用)

const roles = this.reflector.getAllAndOverride<string[]>('roles', [
  context.getHandler(),  // 先看方法
  context.getClass(),    // 再看类
]);

方法上有就用方法的,没有才用类的。

3. getAllAndMerge() - 合并所有

const roles = this.reflector.getAllAndMerge<string[]>('roles', [
  context.getHandler(),
  context.getClass(),
]);

把类和方法的值合并在一起。

对比

装饰器位置get(handler)getAllAndOverride()getAllAndMerge()
只在方法:@Roles('admin')['admin']['admin']['admin']
只在类:@Roles('user')undefined['user']['user']
类+方法都有['admin']['admin'](方法覆盖)['admin', 'user'](合并)

一个例子:从装饰器到 Guard

创建装饰器

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

SetMetadata 是 NestJS 提供的便捷方法,等价于 Reflect.defineMetadata

创建 Guard

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}
  
  canActivate(context: ExecutionContext): boolean {
    const roles = this.reflector.getAllAndOverride<string[]>('roles', [
      context.getHandler(),
      context.getClass(),
    ]);
    
    if (!roles) return true;
    
    // ... 检查用户是否有对应角色
  }
}

使用

@Controller('users')
@UseGuards(RolesGuard)
export class UserController {
  @Get()
  @Roles('user')
  getUsers() {}
  
  @Delete(':id')
  @Roles('admin')
  deleteUser() {}
}

Metadata 的执行时机

Metadata 不是实时读写的,而是分阶段发生:

三个阶段

  1. 编译时(TypeScript → JavaScript)

    • 装饰器执行,写入 Metadata
  2. 启动时(应用启动)

    • NestJS 扫描所有类和方法
    • 读取 Metadata,构建路由表、依赖关系
  3. 运行时(请求处理)

    • Guard/Interceptor 读取 Metadata
    • 根据标签决定是否放行

NestJS 内置的 Metadata Key

NestJS 内部大量使用 Metadata,常见的 key:

// 路由相关
'path'           // @Controller('users')
'method'         // @Get(), @Post()
 
// 参数相关
'design:paramtypes'  // 构造函数参数类型(DI 的核心)
'design:type'        // 属性类型
'design:returntype'  // 返回值类型
 
// 装饰器相关
'guards'         // @UseGuards()
'interceptors'   // @UseInterceptors()
'filters'        // @UseFilters()
'pipes'          // @UsePipes()

举个例子

@Injectable()
export class UserService {
  constructor(private repo: UserRepository) {}
}

NestJS 如何知道要注入 UserRepository

因为 TypeScript 编译时自动写入了 design:paramtypes 元数据:

Reflect.defineMetadata(
  'design:paramtypes', 
  [UserRepository], 
  UserService
);

NestJS 启动时读取这个 Metadata,知道 UserService 的构造函数需要 UserRepository,然后自动注入。

来点实践技巧

技巧 1:自定义复杂 Metadata

除了存储简单的数组,还可以存储复杂对象:

// 限流装饰器
export const RateLimit = (limit: number, window: number) => 
  SetMetadata('rateLimit', { limit, window });
 
// 使用
@RateLimit(100, 60)  // 每分钟最多 100 次请求
getUsers() {}
 
// Guard 中读取
const config = this.reflector.get('rateLimit', context.getHandler());
// { limit: 100, window: 60 }

技巧 2:组合多个装饰器

export const AdminOnly = () => {
  return applyDecorators(
    Roles('admin'),
    RateLimit(1000, 60),
    UseGuards(RolesGuard, RateLimitGuard)
  );
};
 
// 使用
@AdminOnly()
getAdminData() {}

技巧 3:类型安全的 Metadata

export const ROLES_KEY = 'roles';
 
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
 
// Guard 中读取
const roles = this.reflector.get<string[]>(ROLES_KEY, context.getHandler());

避免硬编码字符串,减少拼写错误。

技巧 4:调试 Metadata

想知道某个方法上有哪些 Metadata?

@Controller('users')
export class UserController {
  @Get()
  @Roles('admin')
  getUsers() {}
}
 
// 读取所有 Metadata Key
const allKeys = Reflect.getMetadataKeys(UserController.prototype, 'getUsers');
// ['path', 'method', 'roles', 'design:paramtypes', ...]
 
// 逐个读取值
allKeys.forEach(key => {
  const value = Reflect.getMetadata(key, UserController.prototype, 'getUsers');
  console.log(`${key}:`, value);
});

输出示例:

method: GET
roles: ['admin']
design:paramtypes: []

总结

Metadata 是 NestJS 装饰器系统的底层机制:装饰器在编译时执行并写入 Metadata,这些 Metadata 以键值对形式存储在类和方法上,然后通过 Reflector 读取。整个过程分三个阶段:编译时写入、启动时构建依赖图、运行时读取做权限检查。

所有 NestJS 的装饰器都基于 Metadata:

  • @Injectable() 让 DI 知道可以注入这个类
  • @Controller() 让框架知道这是个控制器
  • @Get() 让框架知道路由和 HTTP 方法
  • @Roles() 让 Guard 知道权限要求
  • @UseGuards() 让框架知道要应用哪些 Guard

理解 Metadata,就能明白 NestJS 的装饰器的”魔法”本质上是元数据设计。

继续了解我的其他相关文章请移步: