NestJs源码解析 @Controller() 如何实现

1,173 阅读5分钟

在 Nest 中,@Controller() 装饰器是用来标记一个类为控制器类的装饰器。当控制器类被@Controller() 装饰器标记后,Nest 会将其添加到依赖注入容器中,并自动注册相应的路由,使得控制器可以响应来自客户端的 HTTP 请求。

下面我将从源码角度解释 @Controller() 装饰器的实现原理。

1. 装饰器的定义

首先,我们来看一下 @Controller() 装饰器的定义

export function Controller(prefix?: string | RegExp, options?: ControllerOptions): ClassDecorator {
  const pathMetadataKey = PATH_METADATA;
  const metadata = {
    ...options,
    [pathMetadataKey]: prefix ?? '',
  };
  return (target: object) => {
    if (!isUndefined(prefix)) {
      Reflect.defineMetadata(pathMetadataKey, prefix, target);
    }
    setControllerMetadata(metadata, target);
  };
}

@Controller() 装饰器是一个函数,它接收两个可选参数:prefixoptionsprefix 是一个字符串或正则表达式,用于指定控制器的路由前缀;options 是一个可选的对象,包含了控制器的一些配置项。

在函数内部,pathMetadataKey 常量定义了控制器路径元数据的键名,metadata 变量则是一个包含了路由前缀和配置项的元数据对象。最后,函数返回了一个函数,这个函数会在控制器类被装饰时调用。

2. 装饰器的执行

当控制器类被 @Controller() 装饰器标记时,Nest 会自动执行这个装饰器函数,并将控制器类作为参数传递进去。在函数内部,首先会根据 prefix 参数将控制器路径元数据保存到控制器类的原型对象上。

if (!isUndefined(prefix)) {
  Reflect.defineMetadata(pathMetadataKey, prefix, target);
}

接着,会将 metadata 元数据对象和控制器类作为参数传递给 setControllerMetadata() 函数。这个函数的作用是将元数据对象保存到依赖注入容器中,以便后续的路由注册和依赖注入操作可以使用。

setControllerMetadata(metadata, target);

3. 元数据的保存和获取

setControllerMetadata() 函数中,会将 metadata 元数据对象保存到控制器类的 METADATA_CONTROLLER 属性上。这个属性是一个 JavaScript 对象,用于保存控制器类的元数据。

export const setControllerMetadata = (metadata: ControllerMetadata, target: object) => {
  Reflect.defineMetadata(METADATA_CONTROLLER, metadata, target);
};

当需要获取控制器类的元数据时,可以通过 Reflect.getMetadata() 方法从 METADATA_CONTROLLER 属性中读取元数据。例如:

const metadata = Reflect.getMetadata(METADATA_CONTROLLER, MyController);

这个元数据对象包含了控制器类的路由前缀、配置项等信息,可以在路由注册和依赖注入时使用。

4. 路由的注册

当控制器类被装饰时,Nest 会自动将控制器类的方法注册为路由。这个过程是通过 RoutesResolver 类来实现的。

RoutesResolver 类是一个单例类,用于解析控制器类中的方法,并将其转换为路由定义。在解析方法时,会读取方法的装饰器,如 @Get()@Post() 等,根据装饰器的参数来生成路由定义。例如:

@Get(':id')
findOne(@Param('id') id: string) {
  return `This action returns a #${id} cat`;
}

这个方法的 @Get(':id') 装饰器表示这个方法响应 HTTP GET 请求,并接受一个 id 参数。解析器会将这个装饰器解析为一个路由定义,如下所示:

{
  path: ':id',
  method: RequestMethod.GET,
  fn: 'findOne',
  argsLength: 1,
  argsMetadata: [{ metatype: String, type: Param, data: 'id' }]
}

其中,path 表示路由路径,method 表示 HTTP 请求方法,fn 表示要执行的方法名,argsLength 表示方法的参数个数,argsMetadata 表示每个参数的元数据,包括参数类型、参数装饰器等信息。

当所有控制器类的方法都被解析为路由定义后,解析器会将这些路由定义保存到 RouterExplorer 中,并通过 RouterBuilder 将这些路由定义注册到路由器中。

5. 依赖注入的实现

在控制器类中,可能会依赖于一些服务或其他组件。这些组件需要在控制器被实例化时自动注入到控制器中。在 Nest 中,可以通过 @Inject() 装饰器来指定要注入的组件。

例如:

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

这个控制器依赖于 AppService 服务,通过构造函数的参数将 AppService 注入到控制器中。当控制器被实例化时,Nest 会自动解析 AppService 的依赖关系,并将其注入到控制器中。

当控制在底层实现中,Nest 使用了 Injector 类来实现依赖注入。当控制器被实例化时,Nest 会调用 Injector 类的 resolve() 方法,该方法会递归解析控制器的依赖关系,并在容器中查找这些依赖的实例。如果实例尚未创建,则会先创建实例并进行递归注入。

例如,对于上面的 AppController 类,Nest 会先解析出其构造函数的参数,即 AppService,并调用 Injectorresolve() 方法解析 AppService 的依赖关系。如果 AppService 没有依赖,就可以直接创建实例并注入到 AppController 中。如果 AppService 有依赖,则需要递归解析其依赖,直到依赖全部被解析为止。

在解析依赖时,Nest 会使用 Reflector 类来读取类的元数据。例如,@Inject() 装饰器会在类的元数据中添加一条 design:paramtypes 记录,记录了构造函数参数的类型信息。通过 Reflector 类可以方便地读取这些元数据,并实现依赖注入的自动解析。

总结

@Controller() 装饰器的实现原理可以概括为:装饰器的执行时机是在控制器类被装饰时,它将控制器类的元数据保存到容器中,并让 RoutesResolver 类解析控制器类中的方法,生成路由定义。同时,它也将控制器类的依赖注入的配置信息保存到容器中,由 Injector 类在控制器被实例化时自动解析依赖关系并注入实例。这些机制协同作用,实现了控制器的自动路由注册和依赖注入的功能,大大简化了控制器的开发和维护工作。