Skip to content

装饰器不只是语法糖:Nest 如何解析 @Controller()?

在 NestJS 中,我们经常使用各种装饰器,如 @Controller()@Get()@Post() 等来定义路由和处理 HTTP 请求。但这些装饰器到底是什么?它们仅仅是语法糖吗?实际上,它们是 NestJS 强大功能的核心机制之一。本文将深入探讨装饰器的本质以及 NestJS 如何解析和使用它们。

1. 装饰器基础概念

装饰器是 JavaScript 的一个提案特性,目前处于 Stage 2 阶段。在 TypeScript 中已经可以使用,并且是构建 NestJS 框架的基础。

1.1 装饰器的类型

TypeScript 支持以下几种装饰器:

typescript
// 类装饰器
@MyClassDecorator()
class MyClass {}

// 属性装饰器
class MyClass {
  @MyPropertyDecorator()
  property: string;
}

// 方法装饰器
class MyClass {
  @MyMethodDecorator()
  method() {}
}

// 参数装饰器
class MyClass {
  method(@MyParameterDecorator() param: string) {}
}

1.2 装饰器的本质

装饰器本质上是一个函数,它可以在类、方法、属性或参数被定义时修改它们的行为:

typescript
// 类装饰器示例
function MyClassDecorator() {
  return function (constructor: Function) {
    // 修改构造函数或原型
    console.log('装饰类:', constructor.name);
  };
}

@MyClassDecorator()
class MyClass {}

2. @Controller() 装饰器详解

让我们从最基础的 @Controller() 装饰器开始:

typescript
import { Controller } from '@nestjs/common';

@Controller('users')
export class UserController {
  // 控制器逻辑
}

2.1 源码分析

查看 @Controller() 的源码实现:

typescript
// nestjs/common/decorators/core/controller.decorator.ts(简化版)
export function Controller(prefix?: string | string[]): ClassDecorator {
  return (target: object) => {
    // 定义元数据
    Reflect.defineMetadata(CONTROLLER_WATERMARK, true, target);
    
    // 存储控制器前缀
    if (prefix) {
      Reflect.defineMetadata(PATH_METADATA, prefix, target);
    }
    
    // 标记为可路由
    Reflect.defineMetadata(ROUTE_METADATA, [], target);
  };
}

2.2 元数据的作用

@Controller() 装饰器通过 Reflect.defineMetadata 在类上定义了元数据:

  1. CONTROLLER_WATERMARK: 标记这是一个控制器类
  2. PATH_METADATA: 存储控制器的路由前缀
  3. ROUTE_METADATA: 存储路由方法信息

3. 路由方法装饰器

3.1 @Get()、@Post() 等装饰器

typescript
@Controller('users')
export class UserController {
  @Get(':id')
  findOne(@Param('id') id: string) {
    return { id, name: 'User Name' };
  }
  
  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    return { id: '1', ...createUserDto };
  }
}

3.2 装饰器源码实现

typescript
// nestjs/common/decorators/http/request-mapping.decorator.ts(简化版)
export function RequestMapping(metadata: RequestMappingMetadata = {}): MethodDecorator {
  return (target: object, key: string | symbol, descriptor: PropertyDescriptor) => {
    // 存储路由方法信息
    Reflect.defineMetadata(METHOD_METADATA, metadata.method, descriptor.value);
    Reflect.defineMetadata(PATH_METADATA, metadata.path, descriptor.value);
    
    // 将路由添加到控制器的路由列表中
    const routes = Reflect.getMetadata(ROUTE_METADATA, target.constructor) || [];
    routes.push(descriptor.value);
    Reflect.defineMetadata(ROUTE_METADATA, routes, target.constructor);
  };
}

// 具体的 HTTP 方法装饰器
export const Get = (path?: string | string[]): MethodDecorator => 
  RequestMapping({ method: RequestMethod.GET, path });

export const Post = (path?: string | string[]): MethodDecorator => 
  RequestMapping({ method: RequestMethod.POST, path });

4. NestJS 如何扫描和解析装饰器

4.1 模块扫描过程

当 NestJS 应用启动时,会进行模块扫描:

typescript
// 简化的扫描过程
class DependenciesScanner {
  async scan(module: Type<any>) {
    // 扫描模块中的控制器
    await this.scanForControllers(module);
  }
  
  async scanForControllers(module: Type<any>) {
    // 获取模块元数据中的控制器列表
    const controllers = Reflect.getMetadata(MODULE_PATH, module) || [];
    
    for (const controller of controllers) {
      // 解析控制器的路由信息
      this.extractRouterPath(controller);
    }
  }
}

4.2 路由映射构建

typescript
class RouterExplorer {
  extractRouterPath(controller: Type<any>): string {
    // 获取控制器前缀
    const controllerPath = Reflect.getMetadata(PATH_METADATA, controller) || '';
    
    // 获取控制器中的所有路由方法
    const routes = Reflect.getMetadata(ROUTE_METADATA, controller) || [];
    
    for (const route of routes) {
      // 获取路由方法和路径
      const method = Reflect.getMetadata(METHOD_METADATA, route);
      const path = Reflect.getMetadata(PATH_METADATA, route);
      
      // 构建完整路由路径
      const fullPath = this.normalizePath(controllerPath, path);
      
      // 注册路由到 Express/Fastify
      this.registerRoute(method, fullPath, controller, route.name);
    }
    
    return controllerPath;
  }
}

5. 装饰器与元数据反射

5.1 Reflect Metadata API

NestJS 依赖于 reflect-metadata 库来处理元数据:

typescript
// 需要在应用入口处引入
import 'reflect-metadata';

// 定义元数据
Reflect.defineMetadata('key', 'value', target);

// 获取元数据
const value = Reflect.getMetadata('key', target);

5.2 编译器选项

要使装饰器正常工作,需要在 tsconfig.json 中启用相关选项:

json
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

6. 自定义装饰器示例

6.1 创建自定义路由装饰器

typescript
// decorators/public.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

// 使用自定义装饰器
@Controller('users')
export class UserController {
  @Public()
  @Get('health')
  health() {
    return { status: 'ok' };
  }
}

6.2 在守卫中使用自定义装饰器

typescript
// guards/auth.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    // 检查路由是否标记为公共访问
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    
    if (isPublic) {
      return true; // 允许公共访问
    }
    
    // 执行认证逻辑
    const request = context.switchToHttp().getRequest();
    return this.validateRequest(request);
  }
  
  private validateRequest(request: any): boolean {
    // 实际的认证逻辑
    return !!request.user;
  }
}

7. 装饰器执行时机

7.1 类定义时执行

装饰器在类被定义时执行,而不是在实例化时:

typescript
console.log('1. 类定义前');

@MyDecorator()
class MyClass {
  constructor() {
    console.log('3. 实例化类');
  }
}

console.log('2. 类定义后');

new MyClass(); // 输出: 3. 实例化类

function MyDecorator() {
  console.log('0. 装饰器执行');
  return function (constructor: Function) {
    console.log('1.5. 装饰器函数执行');
  };
}

7.2 多个装饰器的执行顺序

typescript
function Decorator1() {
  console.log('Decorator1 factory');
  return function (target: any) {
    console.log('Decorator1 executed');
  };
}

function Decorator2() {
  console.log('Decorator2 factory');
  return function (target: any) {
    console.log('Decorator2 executed');
  };
}

@Decorator1()
@Decorator2()
class MyClass {}

输出顺序:

  1. Decorator2 factory
  2. Decorator1 factory
  3. Decorator2 executed
  4. Decorator1 executed

8. 最佳实践

8.1 合理使用装饰器

typescript
// 推荐:保持装饰器简单
@Controller('users')
export class UserController {
  @Get(':id')
  @UseInterceptors(LoggingInterceptor)
  @UseGuards(AuthGuard)
  findOne(@Param('id') id: string) {
    return this.userService.findOne(id);
  }
}

// 不推荐:过度使用装饰器导致代码难以理解
@Controller('users')
@UseInterceptors(LoggingInterceptor)
@UseGuards(AuthGuard)
@SetMetadata('roles', ['admin'])
export class UserController {
  @Get(':id')
  @UseInterceptors(TransformInterceptor)
  @UseGuards(RoleGuard)
  @SetMetadata('permissions', ['read'])
  findOne(
    @Param('id') id: string,
    @Headers('x-request-id') requestId: string
  ) {
    return this.userService.findOne(id);
  }
}

8.2 组合装饰器

typescript
// 创建组合装饰器
export function AuthAndRoles(roles: string[]) {
  return applyDecorators(
    UseGuards(AuthGuard, RolesGuard),
    SetMetadata('roles', roles),
  );
}

// 使用组合装饰器
@Controller('users')
export class UserController {
  @Get(':id')
  @AuthAndRoles(['admin'])
  findOne(@Param('id') id: string) {
    return this.userService.findOne(id);
  }
}

9. 总结

装饰器绝不仅仅是语法糖,它们是 NestJS 框架的核心机制:

  1. 元数据定义:装饰器通过 Reflect.defineMetadata 定义元数据
  2. 模块扫描:NestJS 在启动时扫描装饰器定义的元数据
  3. 路由构建:基于元数据构建路由映射
  4. 运行时处理:在请求处理过程中使用元数据

通过理解装饰器的工作原理,我们可以:

  1. 更好地理解 NestJS 的内部机制
  2. 创建自定义装饰器扩展框架功能
  3. 优化应用结构和代码组织
  4. 解决框架使用中的疑难问题

在下一篇文章中,我们将探讨 @Module() 装饰器如何构建应用的依赖图谱。