在开发大型或复杂的 NestJS 应用程序时,开发者可能会遇到一个令人头疼的问题——循环依赖。它通常发生在两个或多个模块/服务之间存在互相引用的情况下。
一、什么是循环依赖?
简单来说,循环依赖是指类 A 依赖于类 B,而类 B 又直接或间接地依赖于类 A。
例如:
UserService依赖OrderService(需要查询用户的订单)。OrderService依赖UserService(需要获取订单对应的用户信息)。
+-----------------+ +------------------+
| UserService | --------------> | OrderService |
+-----------------+ +------------------+
^ |
| |
+--------------------------------------+
在依赖注入(DI)容器初始化阶段, NestJS 需要先实例化 UserService,但发现它需要 OrderService;于是尝试实例化 OrderService,又发现它需要 UserService。此时就会陷入死循环,最终导致 NestJS 在启动时抛出错误或依赖项为 undefined。
二、循环依赖的危害
- 运行时错误/崩溃:NestJS 在启动阶段无法完成依赖关系的解析,抛出异常。
undefined依赖注入:在某些情况下,NestJS 可能会跳过某些解析,导致被注入的服务变成undefined,从而在调用其方法时引发TypeError: Cannot read property ... of undefined。- 架构腐烂的信号:循环依赖通常意味着模块职责不清或高耦合低内聚的设计问题。
三、解决方案
针对 NestJS 中的循环依赖,主要有以下几种解决方法,分为代码层面的修补和架构层面的重构。
方案 1:使用 forwardRef(前向引用)
NestJS 提供了 forwardRef 函数,允许 NestJS 使用 forwardRef() 延时解析依赖项。这是官方最直接提供的应对方案。
1. Provider / Service 级别的处理
在两边的 Service 构造函数中,同时使用 @Inject(forwardRef(() => TargetService)):
// user.service.ts
import { Injectable, Inject, forwardRef } from '@nestjs/common';
import { OrderService } from '../order/order.service';
@Injectable()
export class UserService {
constructor(
@Inject(forwardRef(() => OrderService))
private readonly orderService: OrderService,
) {}
}
// order.service.ts
import { Injectable, Inject, forwardRef } from '@nestjs/common';
import { UserService } from '../user/user.service';
@Injectable()
export class OrderService {
constructor(
@Inject(forwardRef(() => UserService))
private readonly userService: UserService,
) {}
}
2. Module 级别的处理
如果循环依赖发生在 Module 导入层级,也需要在两边的 imports 中使用 forwardRef:
// user.module.ts
@Module({
imports: [forwardRef(() => OrderModule)],
providers: [UserService],
exports: [UserService],
})
export class UserModule {}
// order.module.ts
@Module({
imports: [forwardRef(() => UserModule)],
providers: [OrderService],
exports: [OrderService],
})
export class OrderModule {}
注意:
forwardRef属于紧急解决手段。过度使用forwardRef会增加代码维护难度,建议优先考虑重构。
方案 2:提取公共依赖(拆分第三个服务/模块)
如果两个服务相互依赖,往往是因为它们共有某些操作或逻辑。可以将共享逻辑抽取到一个新的独立服务中。
重构前:
UserService<-->OrderService
重构后:
- 创建
UserOrderBridgeService或CommonService UserService-->CommonServiceOrderService-->CommonService
这样将原有的单向/双向依赖转化为层次清晰的单向依赖网,从而消除循环。
方案 3:使用事件机制(Event Emitter / CQRS)
在很多业务场景中,循环依赖是由直接的方法调用引起的。例如“用户注销时需要清空订单”和“订单创建时需要更新用户积分”。
通过引入事件驱动机制,可以将直接调用转变为事件发布与订阅,实现彻底解耦。
NestJS 官方提供了 @nestjs/event-emitter 包:
// 1. 安装依赖
// npm i --save @nestjs/event-emitter
// order.service.ts
import { EventEmitter2 } from '@nestjs/event-emitter';
@Injectable()
export class OrderService {
constructor(private eventEmitter: EventEmitter2) {}
createOrder(orderData: any) {
// 处理创建订单逻辑...
// 触发事件,而不是直接调用 UserService
this.eventEmitter.emit('order.created', { userId: orderData.userId });
}
}
// user.service.ts
import { OnEvent } from '@nestjs/event-emitter';
@Injectable()
export class UserService {
@OnEvent('order.created')
handleOrderCreatedEvent(payload: { userId: string }) {
// 处理更新用户积分逻辑
this.updateUserPoints(payload.userId);
}
}
方案 4:通过 ModuleRef 延迟获取
在极少数动态解析的场景中,可以通过注入 ModuleRef,在需要调用方法时再动态获取目标 Service:
import { Injectable, OnInit } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { OrderService } from '../order/order.service';
@Injectable()
export class UserService implements OnInit {
private orderService: OrderService;
constructor(private moduleRef: ModuleRef) {}
ngOnInit() {
// 延迟获取依赖,避免构造函数阶段出现循环引用
this.orderService = this.moduleRef.get(OrderService, { strict: false });
}
someMethod() {
this.orderService.getOrders();
}
}
四、总结与最佳实践
| 解决方案 | 适用场景 | 优缺点 |
|---|---|---|
forwardRef | 快速修复现存代码中的循环依赖 | 快捷简单,但可能掩盖不良的架构设计 |
| 提取公共服务/模块 | 两个服务有明显的公共逻辑重叠 | 推荐。提升代码重用度与架构清晰度 |
| 事件驱动 (EventEmitter) | 侧重于状态通知或异步解耦的业务 | 推荐。彻底解除硬编码依赖,拓展性强 |
ModuleRef | 动态加载或特殊依赖注入模式 | 灵活,但丧失了一部分 DI 的类型安全保障 |
在开发过程中,最推荐的做法是在架构设计阶段贯彻单一职责原则 (SRP) 与依赖倒置原则 (DIP)。遇到循环依赖时,优先考虑架构拆分与事件解耦,最后再使用 forwardRef 作为保底方案。
