理解 NestJS 的 DI 管理机制

  • 我们想要了解依赖注入(Dependency Injection, DI)最核心的工作逻辑
  • NestJS 拥有自己的一套 DI 管理系统,它通过一个称为 DI 容器 的机制,来统一管理应用中所有类(class)的依赖关系与生命周期
  • 这与传统的手动 new 实例的方式不同,是实现控制反转(IoC)的关键

NestJS 初始化流程概览

为了更好地理解 NestJS 的工作流程,我们来看其初始化与依赖管理的整体流程:

  1. 启动阶段:程序启动时,NestJS 会从主模块(如 AppModule)开始解析。
  2. 依赖解析:系统会扫描带有 @Injectable() 注解的类,读取其构造函数(constructor),分析其依赖关系。
  3. 实例化与注册:将这些类及其依赖关系注册到 DI 容器中,并创建其实例。
  4. 模块通信:通过模块间的 importsexportsproviders 属性,建立模块与服务之间的依赖路径。
  • 理解重点:整个流程是自动化的,开发者只需通过注解与配置告诉 NestJS 哪些类需要注册,以及哪些模块之间有依赖关系

关键术语解释与概念澄清


1 ) 注解(Decorator)与 @Injectable()

  • @Injectable() 是一个装饰器(Decorator),用于标记一个类可以被 NestJS 的 DI 容器管理。
  • 当类被标记为 @Injectable(),NestJS 会在程序启动时自动扫描并注册该类

2 ) DI 容器(DI Container)

  • 不要将其与 Docker 容器混淆,这里的“容器”是一个抽象概念,指的是 NestJS 管理依赖的“区域”或“对象空间”
  • 在这个容器中,所有的服务类(Service)都会被实例化并挂载,供其他模块或控制器调用

3 ) 构造函数中的依赖注入(Constructor Injection)

  • 在控制器(Controller)或其他服务类中,我们通过构造函数声明依赖项,如:
    constructor(private readonly appService: AppService) {}
    
  • NestJS 会自动识别构造函数中的依赖关系,并注入对应的实例

模块化结构与 DI 系统的关系

在 NestJS 中,模块(Module)是组织代码的核心单位,通过模块间的引用关系,我们可以构建复杂的依赖网络。

  1. providers:服务注册的核心
  • providers 是模块中用于注册服务类的地方。
  • 只有被注册到 providers 中的类,才会被 DI 容器管理
  • 示例代码:
    @Module({
      providers: [AppService],
    })
    export class AppModule {}
    
  1. exports:服务导出供其他模块使用
  • 如果一个模块中的服务需要被其他模块调用,必须通过 exports 暴露出来。
  • 否则即使模块被导入(imports),也不能访问其内部的服务。
  • 示例代码:
    @Module({
      providers: [AppService],
      exports: [AppService],
    })
    export class AppModule {}
    
  1. imports:模块间依赖的桥梁
  • 通过 imports,我们可以将其他模块引入当前模块,从而访问其 exports 出来的服务。
  • 如果某个控制器依赖的服务不在当前模块中,必须通过 imports 明确引入目标模块。

常见问题与理解难点

1 ) 控制反转(IoC)与依赖注入(DI)的本质

  • 传统方式中,我们手动通过 new MyService() 创建实例。
  • 而在 NestJS 中,我们只需声明依赖关系,由框架自动完成实例化。
  • 这种方式称为控制反转,即对象的创建过程不再由开发者控制,而是交给 DI 容器处理。

2 ) 多模块嵌套下的依赖混乱

  • 当模块结构复杂、存在嵌套时,容易出现服务找不到的错误。
  • 错误提示:Nest can't resolve dependencies of the SomeController
  • 解决方式:
    • 检查依赖服务是否在 providers 中注册。
    • 检查依赖模块是否被正确 imports
    • 检查是否在 exports 中导出服务。

3 ) 缺少 exports 或 providers 导致的访问失败

  • 若服务类在 A 模块中定义,但 B 模块需要使用它:
    • A 模块必须将其注册到 providers
    • A 模块必须在 exports 中导出该服务
    • B 模块必须通过 imports 引入 A 模块

代码示例:模块与服务的注册与使用

以下是一个完整的 NestJS 模块结构示例,帮助理解 DI 机制的运作:

// app.service.ts
import { Injectable } from '@nestjs/common';
 
@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}
// app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
 
@Controller()
export class AppController {
  // 构造函数中注入 AppService
  // 这个就是 获取DI中具体的Class类的实例,告诉DI系统它们(controller 和 service)之间的依赖关系
  constructor(private readonly appService: AppService) {}
 
  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}
// app.module.ts 
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
 
@Module({
  imports: [], // 当前模块依赖的其他模块
  controllers: [AppController], // 控制器列表
  providers: [AppService], // 服务注册 启动时最先执行,告诉DI系统将Service下的Class类进行初始化
  exports: [AppService], // 导出服务以供其他模块使用,不导出,则非AppModule的其他模块无法使用
})
export class AppModule {}
  • 倘若上面的AppController 关联的AppModule没有去包含AppService的这个模块或者是没有进行全局注册
  • 或者是在这个providers 里面提供service里面有没有它的一个具体的class类在DI系统中注册
  • 如果不在providers里面提供AppService,它就会去在 imports 中找其他的模块
  • 其他的模块里面,它需要有两个部分
    • 或者是providers里面去进行注册
    • 还有一个需要要去export出来
    • 这样它就能获取到具体的这个实例了
    • 这是它的一个查询依赖的路径的原理
  • 其次我可以直接在providers里面给它提供一个service,它就可以去把这个service注册到DI系统里面去
  • 这样controller里面也是可以去获取得到对应的这个service的实例的
  • 在 constructor 中定义了 appService 其实就是 new AppService
  • 这个new 的这个过程是:向上一级去进行查找
    • 如果当前 providers里面没有,就会去找 imports 的module 中寻找 providers 和 exports,最后发现了这个AppService,这是一个路径
    • 如果当前 providres 里面有,它就会去交由DI系统里面自动的来去初始化一个APP的实例

掌握 NestJS 的核心机制

  • DI 容器 是管理所有服务实例的核心
  • 模块结构 是组织依赖和实现模块化开发的基础
  • 控制反转 和 依赖注入 是实现松耦合、高内聚架构的关键
  • 模块间的 imports、exports、providers 配置决定了服务的可用性与作用域
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐