NestJS Module 注入原理
核心概念
NestJS 的依赖注入(DI)系统是其核心特性,基于 TypeScript 装饰器和元数据实现。
主要组件
| 组件 | 文件 | 职责 |
|---|---|---|
NestContainer | injector/container.ts | 全局容器,管理所有模块 |
Module | injector/module.ts | 单个模块,管理其 providers/controllers |
Injector | injector/injector.ts | 依赖解析和实例化 |
InstanceWrapper | injector/instance-wrapper.ts | Provider 的包装器,包含实例元数据 |
InstanceLoader | injector/instance-loader.ts | 批量创建实例 |
整体流程
AppModule 定义
↓
NestFactory.create()
↓
Container.addModule() // 注册模块到容器
↓
InstanceLoader.createInstancesOfDependencies()
↓
Injector.loadInstance() // 解析并实例化每个 Provider
↓
resolveConstructorParams() → 读取 PARAMTYPES_METADATA
↓
new Class(...dependencies) // 实例化
依赖解析原理
1. 装饰器收集元数据
// 使用 @Injectable() 标记
@Injectable()
class UsersService {
constructor(
private readonly usersRepository: UsersRepository, // 依赖 A
private readonly logger: LoggerService // 依赖 B
) {}
}
// 编译后,NestJS 通过 reflect-metadata 存储:
Reflect.defineMetadata(PARAMTYPES_METADATA, [UsersRepository, LoggerService], UsersService);
Reflect.defineMetadata(OPTIONAL_DEPS_METADATA, [false, false], UsersService);
关键常量 (from @nestjs/common/constants):
PARAMTYPES_METADATA— 构造函数参数类型OPTIONAL_DEPS_METADATA— 可选依赖标记SELF_DECLARED_DEPS_METADATA— 自定义注入 tokenPROPERTY_DEPS_METADATA— 属性注入
2. Injector 解析依赖
// injector.ts:440 - 读取构造函数参数
public reflectConstructorParams<T>(type: Type<T>): any[] {
const paramtypes = Reflect.getMetadata(PARAMTYPES_METADATA, type) || [];
return Array.from(paramtypes);
}
3. 递归解析依赖树
// injector.ts:128 - 加载实例
public async loadInstance<T>(
wrapper: InstanceWrapper<T>,
collection: Map<InjectionToken, InstanceWrapper>,
moduleRef: Module,
) {
// 1. 解析构造函数参数
// 2. 递归解析每个依赖的实例
// 3. 创建最终实例
await this.resolveConstructorParams<T>(wrapper, moduleRef, inject, callback);
}
Provider 类型处理
Class Provider
Value Provider
const config = { apiKey: 'xxx' };
providers: [{ provide: 'CONFIG', useValue: config }]
// 注入: ctx.get('CONFIG') → { apiKey: 'xxx' }
Factory Provider
providers: [{
provide: 'CACHE',
useFactory: (logger: LoggerService) => new CacheService(logger),
inject: [LoggerService] // 显式声明依赖
}]
Token Provider
providers: [{
provide: 'USERSRepository',
useClass: TypeORMRepository,
}]
// 解析: token 'USERSRepository' → TypeORMRepository 实例
实例作用域 (Scope)
| Scope | 说明 |
|---|---|
DEFAULT | 单例,整个应用共享 |
REQUEST | 每个请求创建一个实例 |
TRANSIENT | 每次注入创建一个新实例 |
SINGLETON | 应用启动时创建,只创建一次 |
Module 之间的关系
导入 (imports)
@Module({
imports: [DatabaseModule], // 导入 DatabaseModule 的 exported providers
})
class AppModule {}
导出 (exports)
全局模块 (global)
请求作用域 (Request Scope) 实现
// packages/core/injector/instance-wrapper.ts:36
export interface ContextId {
readonly id: number;
payload?: unknown;
getParent?(info: HostComponentInfo): ContextId;
}
每个请求有唯一的 ContextId,Transient/Request Scope 的 Provider 会为每个 contextId 创建独立实例。
InstanceWrapper 标识与防重复初始化
标识机制
InstanceWrapper 使用三重标识来唯一确定一个 Provider/Service:
// packages/core/injector/instance-wrapper.ts:61-80
export class InstanceWrapper<T = any> {
public readonly token: InjectionToken; // ① Injection Token (主要标识,用于 Map 查找)
public readonly name: any; // ② 名称 (用于日志/调试)
private readonly [INSTANCE_ID_SYMBOL]: string; // ③ 内部 UUID (唯一ID)
}
| 标识 | 用途 |
|---|---|
token | Provider 的唯一标识,用于 Map 查找 |
name | 人类可读的名称 (通常是 token 的 name) |
id | 内部生成的 UUID,用于追踪 |
防重复初始化机制
NestJS 通过三层检查避免重复初始化:
1. Module 级防重 (packages/core/injector/container.ts:109-114)
if (this.modules.has(token)) {
return {
moduleRef: this.modules.get(token)!,
inserted: true, // 已存在,跳过
};
}
2. Provider 级防重 (packages/core/injector/module.ts)
public addProvider(provider: Provider, enhancerSubtype?): string | symbol {
if (this._providers.has(token)) {
return token; // 直接返回,不重复添加
}
// ...
}
3. 实例状态检查 (packages/core/injector/injector.ts:162)
关键状态字段
// packages/core/injector/instance-wrapper.ts:42-48
export interface InstancePerContext<T> {
instance: T;
isResolved?: boolean; // 是否已解析
isPending?: boolean; // 是否正在解析中 (防止并发重复解析)
donePromise?: Promise<unknown>;
isConstructorCalled?: boolean;
}
防重复初始化流程图
addProvider(MyService)
↓
检查 _providers.has(MyService.token)
↓
已存在 → 直接返回,不重复创建 InstanceWrapper
↓
不存在 → 创建新的 InstanceWrapper
↓
loadInstance()
↓
检查 instanceHost.isResolved
↓
已解析 → 直接返回
↓
未解析 → 执行 resolveConstructorParams → 实例化
并发保护
当多个请求同时需要解析同一个依赖时:
// packages/core/injector/injector.ts:141
if (instanceHost.isPending) {
// 另一个请求正在解析,等待完成后再返回
return instanceHost.donePromise!.then((err?: unknown) => {
if (err) throw err;
});
}
存储结构
NestContainer (全局容器)
└── ModulesContainer (Map<string, Module>)
└── Module
└── _providers (Map<token, InstanceWrapper>)
└── InstanceWrapper
└── values (WeakMap<ContextId, InstancePerContext>)
└── instance (实际对象)
单例模式时,所有请求共享同一个实例,存储在 STATIC_CONTEXT 对应的 InstancePerContext 中。
核心文件
| 文件 | 说明 |
|---|---|
injector/container.ts | 全局容器,模块注册 |
injector/injector.ts | 依赖解析核心逻辑 |
injector/instance-loader.ts | 批量实例加载 |
injector/instance-wrapper.ts | 实例包装器 |
injector/module.ts | 模块定义和管理 |
调试技巧
// 1. 查看已注册的模块
const container = app.get(NestApplicationContext).container;
console.log(container.getModules());
// 2. 查看 Provider 实例
const moduleRef = container.getModuleByKey('AppModule');
console.log(moduleRef.providers);
// 3. 打印依赖树
// 在 injector.ts 的 loadInstance 中添加日志
总结
- 元数据驱动 — 通过
reflect-metadata在编译时存储依赖信息 - 递归解析 — 类似树的先序遍历,解析完依赖才实例化
- 容器管理 —
NestContainer管理所有模块,Module管理单个模块的 providers - 作用域控制 — 通过
ContextId隔离不同请求的实例
Backlinks (1)
flowchart LR
n0["AI"]
n1["Database"]
n2["Dev Tools"]
n3["Emoji"]
n4["Frontend"]
n5["Game Dev"]
n6["Collection"]
n7["Languages"]
n8["Maps"]
n9["Media"]
n10["Monitor"]
n11["Plans"]
n12["对象存储基本用法(Bucket / Object / 常用操作)"]
n13["对象存储数据迁移(R2 → MinIO / 跨厂商搬迁)"]
n14["Object Storage"]
n15["挂载 Bucket 为本地文件系统(FUSE Mount)"]
n16["对象存储签名 URL(Signed URL)原理与实战"]
n17["对象存储供应商对比(S3 / R2 / OSS / Supabase / MinIO)"]
n18["Prototypes"]
n19["Research"]
n20["Better Auth 源码阅读指南"]
n21["DuckDB 环境与基本使用"]
n22["DuckDB 实战研究"]
n23["DuckDB 模拟数据"]
n24["PostgreSQL 数据用 DuckDB 加速查询"]
n25["HK 角色阅读"]
n26["HK 台词阅读"]
n27["Hollow Knight 英语主题"]
n28["HK 物品阅读"]
n29["HK 地点阅读"]
n30["HK 世界观 / Lore 阅读"]
n31["HK 英语学习素材清单"]
n32["英语学习 Dashboard"]
n33["English Scraps Archive"]
n34["English Scraps 使用指南"]
n35["Jellyfin 源码阅读指南"]
n36["Lux 资料整理"]
n37["Nest Commander 学习资料"]
n38["NestJS 源码阅读指南"]
n39["Protomaps 自建底图研究"]
n40["自制 PMTiles 地图(最简单例子)"]
n41["MapLibre 集成 Protomaps"]
n42["PMTiles 格式与工具链"]
n43["上海地区底图项目"]
n44["Redash 源码阅读指南"]
n45["Rust 学习计划"]
n46["shadcn/ui 源码阅读指南"]
n47["TRIP 项目核心原理与代码阅读指南"]
n1 --> n22
n6 --> n0
n6 --> n1
n6 --> n2
n6 --> n3
n6 --> n4
n6 --> n5
n6 --> n7
n6 --> n8
n6 --> n9
n6 --> n10
n6 --> n11
n6 --> n19
n12 --> n13
n12 --> n16
n12 --> n17
n12 --> n18
n13 --> n12
n13 --> n15
n13 --> n16
n13 --> n17
n14 --> n12
n14 --> n13
n14 --> n15
n14 --> n16
n14 --> n17
n15 --> n12
n15 --> n16
n15 --> n17
n16 --> n18
n17 --> n12
n17 --> n13
n17 --> n16
n17 --> n18
n18 --> n12
n18 --> n15
n18 --> n16
n18 --> n17
n18 --> n39
n19 --> n20
n19 --> n22
n19 --> n32
n19 --> n35
n19 --> n36
n19 --> n37
n19 --> n38
n19 --> n39
n19 --> n44
n19 --> n45
n19 --> n46
n19 --> n47
n21 --> n23
n22 --> n1
n22 --> n19
n22 --> n21
n22 --> n23
n22 --> n24
n23 --> n21
n23 --> n24
n24 --> n22
n24 --> n23
n27 --> n25
n27 --> n26
n27 --> n28
n27 --> n29
n27 --> n30
n27 --> n31
n27 --> n34
n31 --> n26
n32 --> n27
n32 --> n33
n32 --> n34
n34 --> n32
n34 --> n33
n39 --> n8
n39 --> n40
n39 --> n41
n39 --> n42
n39 --> n43
n40 --> n42
n41 --> n18
n41 --> n43
n42 --> n40
n42 --> n43
n43 --> n41
n43 --> n42
click n0 "../../../collection/ai/" "AI"
click n1 "../../../collection/database/" "Database"
click n2 "../../../collection/dev-tools/" "Dev Tools"
click n3 "../../../collection/emoji/" "Emoji"
click n4 "../../../collection/frontend/" "Frontend"
click n5 "../../../collection/game-dev/" "Game Dev"
click n6 "../../../collection/" "Collection"
click n7 "../../../collection/languages/" "Languages"
click n8 "../../../collection/maps/" "Maps"
click n9 "../../../collection/media/" "Media"
click n10 "../../../collection/monitor/" "Monitor"
click n11 "../../../collection/scraps/plans/" "Plans"
click n12 "../../../knowledge/infrastructure/cloud/object-storage/basic-usage/" "对象存储基本用法(Bucket / Object / 常用操作)"
click n13 "../../../knowledge/infrastructure/cloud/object-storage/data-migration/" "对象存储数据迁移(R2 → MinIO / 跨厂商搬迁)"
click n14 "../../../knowledge/infrastructure/cloud/object-storage/" "Object Storage"
click n15 "../../../knowledge/infrastructure/cloud/object-storage/mount-bucket/" "挂载 Bucket 为本地文件系统(FUSE Mount)"
click n16 "../../../knowledge/infrastructure/cloud/object-storage/signed-url/" "对象存储签名 URL(Signed URL)原理与实战"
click n17 "../../../knowledge/infrastructure/cloud/object-storage/vendors-comparison/" "对象存储供应商对比(S3 / R2 / OSS / Supabase / MinIO)"
click n18 "../../../prototypes/" "Prototypes"
click n19 "../../" "Research"
click n20 "../better-auth/" "Better Auth 源码阅读指南"
click n21 "../duckdb/basic-usage/" "DuckDB 环境与基本使用"
click n22 "../duckdb/" "DuckDB 实战研究"
click n23 "../duckdb/mock-data/" "DuckDB 模拟数据"
click n24 "../duckdb/postgresql-acceleration/" "PostgreSQL 数据用 DuckDB 加速查询"
click n25 "../english/hollow-knight/characters/" "HK 角色阅读"
click n26 "../english/hollow-knight/dialogues/" "HK 台词阅读"
click n27 "../english/hollow-knight/" "Hollow Knight 英语主题"
click n28 "../english/hollow-knight/items/" "HK 物品阅读"
click n29 "../english/hollow-knight/locations/" "HK 地点阅读"
click n30 "../english/hollow-knight/lore/" "HK 世界观 / Lore 阅读"
click n31 "../english/hollow-knight/resources/" "HK 英语学习素材清单"
click n32 "../english/" "英语学习 Dashboard"
click n33 "../english/scraps/archive/" "English Scraps Archive"
click n34 "../english/scraps/" "English Scraps 使用指南"
click n35 "../jellyfin/" "Jellyfin 源码阅读指南"
click n36 "../lux/" "Lux 资料整理"
click n37 "../nest-commander/" "Nest Commander 学习资料"
click n38 "./" "NestJS 源码阅读指南"
click n39 "../protomaps/" "Protomaps 自建底图研究"
click n40 "../protomaps/make-own-map/" "自制 PMTiles 地图(最简单例子)"
click n41 "../protomaps/maplibre/" "MapLibre 集成 Protomaps"
click n42 "../protomaps/pmtiles/" "PMTiles 格式与工具链"
click n43 "../protomaps/shanghai-map/" "上海地区底图项目"
click n44 "../redash/" "Redash 源码阅读指南"
click n45 "../rust/" "Rust 学习计划"
click n46 "../shadcn-ui/" "shadcn/ui 源码阅读指南"
click n47 "../trip/" "TRIP 项目核心原理与代码阅读指南"