Skip to content

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 — 自定义注入 token
  • PROPERTY_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

@Injectable()
class UsersService {}

// 自动解析: new UsersService(instanceA, instanceB)

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)

@Module({
  exports: [UsersService],  // 导出给其他模块使用
})
class UsersModule {}

全局模块 (global)

@Module({})
@Global()
class DatabaseModule {}
// 全局可用,无需导入

请求作用域 (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)

if (instanceHost.isResolved) {
  return settlementSignal.complete();  // 已解析,直接返回
}

关键状态字段

// 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 中添加日志

总结

  1. 元数据驱动 — 通过 reflect-metadata 在编译时存储依赖信息
  2. 递归解析 — 类似树的先序遍历,解析完依赖才实例化
  3. 容器管理NestContainer 管理所有模块,Module 管理单个模块的 providers
  4. 作用域控制 — 通过 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 项目核心原理与代码阅读指南"