依赖注入深入
本教程共 47 篇 · 第 8 篇 · 更新于 2026-08-09 · 约 11 分钟阅读
本节目标:搞懂 NestJS 依赖注入的底层机制,学会处理循环依赖、使用 ModuleRef 动态获取实例、掌握生命周期钩子。
先搞懂依赖注入是什么
不用依赖注入时,你写代码是这样的:
class UsersController {
private usersService = new UsersService();
}
控制器自己 new 了一个 UsersService。看着挺简单,但问题很大:
- 耦合太紧——
UsersController和UsersService绑死了,想换个实现?改代码。 - 测试困难——想 mock 一个
UsersService做单元测试?没法换。 - 管理混乱——谁创建、谁销毁、是不是单例?全靠自己管。
依赖注入的思路很简单:别自己创建依赖,让外部容器帮你创建和注入。
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
}
你只管在构造函数里声明”我需要什么”,NestJS 的 IoC 容器会帮你把 UsersService 的实例送过来。
打个比方:你不用自己种菜做饭。去餐厅,告诉服务员你要什么菜,厨房给你做好端上来。你不需要知道菜怎么种的、怎么做的。
IoC 容器的工作流程
NestJS 的 IoC 容器做了四件事:
1. 解析依赖关系
↓
2. 创建依赖实例
↓
3. 注入到目标类
↓
4. 管理实例生命周期
具体到你写的那行 constructor(private usersService: UsersService),背后发生的事情是:
- NestJS 启动时扫描所有模块和 Provider
- 构建一棵依赖树——谁依赖谁,谁先创建
- 按依赖顺序实例化——先创建被依赖的,再创建依赖别人的
- 把实例注入到构造函数里
Note依赖分析是”传递的”。如果
UsersService依赖DatabaseService,DatabaseService依赖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 需要 AuthService,AuthService 又需要 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) {}
}
NoteREQUEST 和 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应急,但最好重构消除 - 生命周期钩子让你在特定时机执行逻辑
- 默认单例,非必要不用其他作用域