首页 / NestJS 入门教程 / 依赖注入深入

NestJS 入门教程

依赖注入深入

本教程共 47 篇 · 第 8 篇 · 更新于 2026-08-09 · 约 11 分钟阅读

NestJS依赖注入IoCModuleRef循环依赖forwardRef生命周期

本节目标:搞懂 NestJS 依赖注入的底层机制,学会处理循环依赖、使用 ModuleRef 动态获取实例、掌握生命周期钩子。

先搞懂依赖注入是什么

不用依赖注入时,你写代码是这样的:

class UsersController {
  private usersService = new UsersService();
}

控制器自己 new 了一个 UsersService。看着挺简单,但问题很大:

  1. 耦合太紧——UsersControllerUsersService 绑死了,想换个实现?改代码。
  2. 测试困难——想 mock 一个 UsersService 做单元测试?没法换。
  3. 管理混乱——谁创建、谁销毁、是不是单例?全靠自己管。

依赖注入的思路很简单:别自己创建依赖,让外部容器帮你创建和注入。

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}
}

你只管在构造函数里声明”我需要什么”,NestJS 的 IoC 容器会帮你把 UsersService 的实例送过来。

打个比方:你不用自己种菜做饭。去餐厅,告诉服务员你要什么菜,厨房给你做好端上来。你不需要知道菜怎么种的、怎么做的。

IoC 容器的工作流程

NestJS 的 IoC 容器做了四件事:

1. 解析依赖关系

2. 创建依赖实例

3. 注入到目标类

4. 管理实例生命周期

具体到你写的那行 constructor(private usersService: UsersService),背后发生的事情是:

  1. NestJS 启动时扫描所有模块和 Provider
  2. 构建一棵依赖树——谁依赖谁,谁先创建
  3. 按依赖顺序实例化——先创建被依赖的,再创建依赖别人的
  4. 把实例注入到构造函数里
Note

依赖分析是”传递的”。如果 UsersService 依赖 DatabaseServiceDatabaseService 依赖 ConfigService,NestJS 会按 ConfigService → DatabaseService → UsersService 的顺序创建。你不需要手动管理这个顺序。

构造函数注入

最常用的注入方式,前面已经用了很多次了:

@Injectable()
export class UsersService {
  constructor(
    private readonly databaseService: DatabaseService,
    private readonly loggerService: LoggerService,
  ) {}
}

NestJS 通过 TypeScript 的类型信息自动识别依赖。你写 private readonly databaseService: DatabaseService,NestJS 就知道要注入 DatabaseService 的实例。

Tip

这个自动识别依赖的能力依赖 TypeScript 的 emitDecoratorMetadata 编译选项。记得在 tsconfig.json 里确认它开着。

@Inject 装饰器

当 token 不是类名时(比如字符串、Symbol),需要用 @Inject() 显式告诉 NestJS 注入什么:

@Injectable()
export class AppService {
  constructor(
    @Inject('CONFIG_OPTIONS') private options: ConfigOptions,
  ) {}
}

@Inject() 接收一个 token 参数,告诉 NestJS:“用这个 token 去容器里找对应的实例。“

属性注入

除了构造函数注入,还能用属性注入:

@Injectable()
export class UsersService {
  @Inject('CONFIG_OPTIONS')
  private configOptions: ConfigOptions;

  @Inject(LoggerService)
  private logger: LoggerService;
}
Note

属性注入不常用。构造函数注入更清晰——一眼就能看出这个类依赖了什么。属性注入适合可选依赖的场景。

ModuleRef:动态获取实例

有时候你没法在构造函数里声明依赖——比如需要运行时动态决定用哪个服务。这时候用 ModuleRef

import { Injectable, OnModuleInit, ModuleRef } from '@nestjs/common';

@Injectable()
export class UsersService implements OnModuleInit {
  private someService: SomeService;

  constructor(private moduleRef: ModuleRef) {}

  onModuleInit() {
    // 在模块初始化后手动获取实例
    this.someService = this.moduleRef.get(SomeService);
  }
}

ModuleRef.get() 可以获取当前模块内的任何 Provider 实例。

更实用的场景——根据条件动态选择服务:

@Injectable()
export class NotificationService {
  constructor(private moduleRef: ModuleRef) {}

  async send(type: string, message: string) {
    const service = this.moduleRef.get(
      type === 'email' ? EmailService : SmsService,
    );
    return service.send(message);
  }
}
Tip

ModuleRef 还能解析瞬态(TRANSIENT)Provider。用 moduleRef.resolve() 代替 moduleRef.get(),每次调用都会创建新实例。

循环依赖

两个类互相依赖,就是循环依赖:

// users.service.ts
@Injectable()
export class UsersService {
  constructor(private authService: AuthService) {}
}

// auth.service.ts
@Injectable()
export class AuthService {
  constructor(private usersService: UsersService) {}
}

UsersService 需要 AuthServiceAuthService 又需要 UsersService。鸡生蛋、蛋生鸡。NestJS 不知道该先创建谁,会报错。

用 forwardRef 解决

forwardRef 的意思是”前向引用”——先告诉 NestJS 这个类型是什么,等用到的时候再去解析:

// users.service.ts
import { Injectable, Inject, forwardRef } from '@nestjs/common';

@Injectable()
export class UsersService {
  constructor(
    @Inject(forwardRef(() => AuthService))
    private authService: AuthService,
  ) {}
}
// auth.service.ts
import { Injectable, Inject, forwardRef } from '@nestjs/common';

@Injectable()
export class AuthService {
  constructor(
    @Inject(forwardRef(() => UsersService))
    private usersService: UsersService,
  ) {}
}

两边都要用 forwardRef,缺一不可。

模块之间也有循环依赖,解法一样:

// users.module.ts
@Module({
  imports: [forwardRef(() => AuthModule)],
})
export class UsersModule {}

// auth.module.ts
@Module({
  imports: [forwardRef(() => UsersModule)],
})
export class AuthModule {}
Warning

forwardRef 是应急方案。用了它说明你的设计有问题——两个类互相依赖,通常意味着它们应该合并,或者把共享逻辑抽出来放到第三个类里。

更好的做法:重构消除循环

// 提取共享逻辑
@Injectable()
export class SharedService {
  // 公共逻辑放这里
}

@Injectable()
export class UsersService {
  constructor(private sharedService: SharedService) {}
}

@Injectable()
export class AuthService {
  constructor(private sharedService: SharedService) {}
}

循环没了,代码也更清晰。

Tip

循环依赖有时候是 barrel 文件(index.ts)引起的。如果同一个目录下的模块通过 barrel 文件互相导入,也会触发循环依赖。避免在 barrel 文件里导入同目录的模块/Provider 类。

生命周期钩子

NestJS 的 Provider 可以感知应用的生命周期,在特定时机执行逻辑。

钩子触发时机
onModuleInit()模块初始化完成,所有依赖已注入
onApplicationBootstrap()所有模块初始化完成,应用启动
onModuleDestroy()模块即将销毁
beforeApplicationShutdown()应用即将关闭(收到关闭信号后)
onApplicationShutdown()应用正在关闭

执行顺序:

应用启动阶段:
  1. onModuleInit()
  2. onApplicationBootstrap()

  应用运行中...

应用关闭阶段:
  3. onModuleDestroy()
  4. beforeApplicationShutdown()
  5. onApplicationShutdown()

使用方式——实现对应的接口:

import {
  Injectable,
  OnModuleInit,
  OnApplicationBootstrap,
  OnModuleDestroy,
} from '@nestjs/common';

@Injectable()
export class DatabaseService
  implements OnModuleInit, OnApplicationBootstrap, OnModuleDestroy
{
  onModuleInit() {
    console.log('模块初始化完成,可以建立数据库连接');
  }

  onApplicationBootstrap() {
    console.log('应用启动完成,可以执行预热逻辑');
  }

  onModuleDestroy() {
    console.log('模块即将销毁,关闭数据库连接');
  }
}
Tip

最常用的钩子是 onModuleInit()——适合做初始化逻辑,比如预热缓存、建立连接。onModuleDestroy() 适合做清理工作。

Provider 作用域详解

单例(默认)

@Injectable()
export class UsersService {
  // 整个应用只有一个实例
  // 所有注入点共享同一个实例
}

请求作用域

@Injectable({ scope: Scope.REQUEST })
export class RequestService {
  // 每个 HTTP 请求创建一个新实例
  // 请求结束后实例被销毁
}

瞬态作用域

@Injectable({ scope: Scope.TRANSIENT })
export class LoggerService {
  // 每次注入都创建一个新实例
  // 即使同一个类注入两次,也是两个不同实例
}

作用域传播

如果 UsersService 是 REQUEST 作用域,那依赖它的 AuthService 也必须变成 REQUEST 作用域。作用域会”传染”:

@Injectable({ scope: Scope.REQUEST })
export class UsersService {}

@Injectable()
export class AuthService {
  // 因为依赖了 REQUEST 作用域的 UsersService
  // AuthService 也会变成 REQUEST 作用域
  constructor(private usersService: UsersService) {}
}
Note

REQUEST 和 TRANSIENT 作用域会影响性能。单例是最快的——创建一次,到处复用。非必要不要用其他作用域。

可选依赖

@Optional() 标记一个依赖是可选的:

@Injectable()
export class UsersService {
  constructor(
    @Optional()
    private readonly logger?: LoggerService,
  ) {}

  doSomething() {
    this.logger?.log('doing something');
  }
}

如果 LoggerService 没有在模块里注册,也不会报错。logger 的值是 undefined

依赖注入最佳实践

依赖抽象而非具体实现

// 定义抽象
export abstract class LoggerService {
  abstract log(message: string): void;
}

// 提供实现
@Injectable()
export class ConsoleLogger extends LoggerService {
  log(message: string) {
    console.log(message);
  }
}

// 注册
@Module({
  providers: [
    { provide: LoggerService, useClass: ConsoleLogger },
  ],
})
export class AppModule {}

// 注入时用抽象类型
@Injectable()
export class UsersService {
  constructor(private logger: LoggerService) {}
}

这样以后想换日志实现,只需要改 useClass,业务代码不用动。

用 Symbol 或常量管理 Token

// constants.ts
export const DATABASE_CONNECTION = Symbol('DATABASE_CONNECTION');
export const CONFIG_OPTIONS = Symbol('CONFIG_OPTIONS');

// 注册
providers: [
  { provide: DATABASE_CONNECTION, useFactory: createConnection },
]

// 注入
constructor(@Inject(DATABASE_CONNECTION) private db: Connection) {}

默认用单例,谨慎用其他作用域

单例性能好、状态一致。只有真正需要请求隔离时才用 REQUEST 作用域。

小结

依赖注入是 NestJS 的灵魂。搞懂了它,你就理解了 NestJS 最核心的设计思想。

核心要点:

  • 构造函数注入是默认选择,简洁清晰
  • @Inject() 用于非类名 token 的注入
  • ModuleRef 用于运行时动态获取实例
  • 循环依赖用 forwardRef 应急,但最好重构消除
  • 生命周期钩子让你在特定时机执行逻辑
  • 默认单例,非必要不用其他作用域