在 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() 装饰器是一个函数,它接收两个可选参数:prefix 和 options。prefix 是一个字符串或正则表达式,用于指定控制器的路由前缀;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,并调用 Injector 的 resolve() 方法解析 AppService 的依赖关系。如果 AppService 没有依赖,就可以直接创建实例并注入到 AppController 中。如果 AppService 有依赖,则需要递归解析其依赖,直到依赖全部被解析为止。
在解析依赖时,Nest 会使用 Reflector 类来读取类的元数据。例如,@Inject() 装饰器会在类的元数据中添加一条 design:paramtypes 记录,记录了构造函数参数的类型信息。通过 Reflector 类可以方便地读取这些元数据,并实现依赖注入的自动解析。
总结
@Controller() 装饰器的实现原理可以概括为:装饰器的执行时机是在控制器类被装饰时,它将控制器类的元数据保存到容器中,并让 RoutesResolver 类解析控制器类中的方法,生成路由定义。同时,它也将控制器类的依赖注入的配置信息保存到容器中,由 Injector 类在控制器被实例化时自动解析依赖关系并注入实例。这些机制协同作用,实现了控制器的自动路由注册和依赖注入的功能,大大简化了控制器的开发和维护工作。