踩坑笔记 # nodejs # nestjs # 后端已发布

深入理解与解决 NestJS 中的循环依赖问题

在开发大型或复杂的 NestJS 应用程序时,开发者可能会遇到一个令人头疼的问题——循环依赖。它通常发生在两个或多个模块/服务之间存在互相引用的情况下...

2025.03.154 分钟阅读
深入理解与解决 NestJS 中的循环依赖问题
深入理解与解决 NestJS 中的循环依赖问题 · Cover

在开发大型或复杂的 NestJS 应用程序时,开发者可能会遇到一个令人头疼的问题——循环依赖。它通常发生在两个或多个模块/服务之间存在互相引用的情况下。


一、什么是循环依赖?

简单来说,循环依赖是指类 A 依赖于类 B,而类 B 又直接或间接地依赖于类 A。

例如:

  • UserService 依赖 OrderService(需要查询用户的订单)。
  • OrderService 依赖 UserService(需要获取订单对应的用户信息)。
+-----------------+                   +------------------+
|   UserService   |  -------------->  |   OrderService   |
+-----------------+                   +------------------+
        ^                                      |
        |                                      |
        +--------------------------------------+

在依赖注入(DI)容器初始化阶段, NestJS 需要先实例化 UserService,但发现它需要 OrderService;于是尝试实例化 OrderService,又发现它需要 UserService。此时就会陷入死循环,最终导致 NestJS 在启动时抛出错误或依赖项为 undefined。


二、循环依赖的危害

  1. 运行时错误/崩溃:NestJS 在启动阶段无法完成依赖关系的解析,抛出异常。
  2. undefined 依赖注入:在某些情况下,NestJS 可能会跳过某些解析,导致被注入的服务变成 undefined,从而在调用其方法时引发 TypeError: Cannot read property ... of undefined。
  3. 架构腐烂的信号:循环依赖通常意味着模块职责不清或高耦合低内聚的设计问题。

三、解决方案

针对 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 --> CommonService
  • OrderService --> 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 作为保底方案。

#nodejs #nestjs #后端
ZZC

ZZC

写代码,也写一些生活。