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

- 启动阶段:程序启动时,NestJS 会从主模块(如
AppModule
)开始解析。 - 依赖解析:系统会扫描带有
@Injectable()
注解的类,读取其构造函数(constructor),分析其依赖关系。 - 实例化与注册:将这些类及其依赖关系注册到 DI 容器中,并创建其实例。
- 模块通信:通过模块间的
imports
、exports
与providers
属性,建立模块与服务之间的依赖路径。
- 理解重点:整个流程是自动化的,开发者只需通过注解与配置告诉 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)是组织代码的核心单位,通过模块间的引用关系,我们可以构建复杂的依赖网络。
- providers:服务注册的核心
providers
是模块中用于注册服务类的地方。- 只有被注册到
providers
中的类,才会被 DI 容器管理 - 示例代码:
@Module({providers: [AppService], }) export class AppModule {}
- exports:服务导出供其他模块使用
- 如果一个模块中的服务需要被其他模块调用,必须通过
exports
暴露出来。 - 否则即使模块被导入(
imports
),也不能访问其内部的服务。 - 示例代码:
@Module({providers: [AppService],exports: [AppService], }) export class AppModule {}
- 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 模块
- 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 配置决定了服务的可用性与作用域