循环依赖解决:使用 forwardRef 或动态模块


循环依赖解决:使用 forwardRef 或动态模块

在实际开发中经常遇到但又有点头疼的问题——循环依赖。写过复杂模块的同学应该深有体会,有时候两个服务互相调用,或者模块之间互相引用,结果 NestJS 启动时就给你抛出一个“循环依赖”的错误,一脸懵。别慌,Nest 提供了几种优雅的解决方案,今天我们就来捋一捋。


什么是循环依赖?

简单来说,就是 A 依赖 B,B 又依赖 A,形成了一个环。比如:

  • UserService 里调用了 AuthService 的方法。
  • AuthService 里又调用了 UserService 的方法。

或者模块级别:UserModule 导入了 AuthModuleAuthModule 又导入了 UserModule

当 Nest 启动时,它会分析依赖关系并尝试实例化这些类。但是遇到循环依赖,它就会陷入“先有鸡还是先有蛋”的困境,不知道该先创建谁,最后报错。


怎么解决?

Nest 提供了两种主要方式来解决循环依赖:forwardRef动态模块。我们分别来看。

1. 使用 forwardRef 函数

forwardRef 是 Nest 提供的一个工具函数,它允许我们“延迟引用”一个暂时还没有定义好的类。简单说,就是告诉 Nest:“这个类现在还没完全准备好,但你先把引用记下来,等它准备好了再处理。”

场景一:服务之间的循环依赖

假设我们有 UserServiceAuthService 相互依赖:

// user.service.ts
@Injectable()
export class UserService {
  constructor(private authService: AuthService) {} // 依赖 AuthService
}

// auth.service.ts
@Injectable()
export class AuthService {
  constructor(private userService: UserService) {} // 依赖 UserService
}

这时候直接运行就会报循环依赖。解决办法:在双方(或至少一方)的构造函数注入时,用 @Inject(forwardRef(() => 对方类))

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

@Injectable()
export class UserService {
  constructor(
    @Inject(forwardRef(() => AuthService))
    private authService: AuthService,
  ) {}
}
// auth.service.ts
@Injectable()
export class AuthService {
  constructor(
    @Inject(forwardRef(() => UserService))
    private userService: UserService,
  ) {}
}

这样,Nest 就会先创建一个代理,等到两边都实例化完成后,再把真正的实例注入进去。

场景二:模块之间的循环依赖

同样,如果两个模块相互导入,也可以用 forwardRef

// user.module.ts
@Module({
  imports: [forwardRef(() => AuthModule)],
  providers: [UserService],
  exports: [UserService],
})
export class UserModule {}
// auth.module.ts
@Module({
  imports: [forwardRef(() => UserModule)],
  providers: [AuthService],
  exports: [AuthService],
})
export class AuthModule {}

forwardRef 就是一把“万能钥匙”,能解开大多数循环依赖的死结。


2. 使用动态模块重构

有时候循环依赖是因为模块设计得不够合理,导致紧密耦合。这时候可以考虑用动态模块来解耦。动态模块允许我们在导入时传递配置,把依赖关系变成单向的。

举个例子:假设 DatabaseModule 需要依赖 ConfigModule 获取数据库配置,而 ConfigModule 又需要依赖 DatabaseModule 来读取配置表中的配置(比如从数据库动态加载配置)。这就形成了循环。

我们可以用动态模块,让 DatabaseModuleforRootforFeature 时接收配置,而不是直接依赖 ConfigService。这样就把依赖关系反转了。

动态模块通常有三种方法:registerforRootforFeature。它们的作用分别是:

  • register:每次导入时传递配置,比如 BbbModule.register({aaa: 1}),下次再 register 又可以传不同的配置。
  • forRoot:只在根模块导入一次,配置全局生效,比如数据库连接配置。
  • forFeature:在 forRoot 的基础上,为特定功能传递额外配置,比如指定某个模块访问哪个表。

通过动态模块,我们可以把依赖的配置作为参数传入,而不是直接引用配置服务,从而打破循环。

例如,我们设计一个 ConfigModule 提供配置服务,然后 DatabaseModule 在导入时通过 forRoot 传入配置对象:

// database.module.ts
@Module({})
export class DatabaseModule {
  static forRoot(config: DbConfig): DynamicModule {
    return {
      module: DatabaseModule,
      providers: [
        {
          provide: 'DB_CONFIG',
          useValue: config,
        },
        DatabaseService,
      ],
      exports: [DatabaseService],
    };
  }
}

然后在 AppModule 中导入:

@Module({
  imports: [DatabaseModule.forRoot(dbConfig)],
})
export class AppModule {}

这样 DatabaseService 内部通过 @Inject('DB_CONFIG') 拿到配置,就不再需要直接依赖 ConfigService 了。


什么时候用哪种?

  • 如果只是两个类或两个模块简单的相互引用,用 forwardRef 最直接,几行代码就搞定。
  • 如果循环依赖是因为设计不合理,或者你想让模块更独立、可复用,可以考虑用动态模块重构,把依赖的配置作为参数传入,这样模块之间的耦合度更低。

不过要注意,forwardRef 虽然方便,但不宜滥用。过多的 forwardRef 可能意味着你的代码结构有些混乱,可以考虑重构来消除循环。动态模块则是一种更“治本”的方式,能让模块边界更清晰。


总结

循环依赖是模块化开发中难以完全避免的问题,好在 NestJS 提供了灵活的解决方案。forwardRef 可以快速“解围”,而动态模块则能从根本上优化模块设计。遇到这类错误时,先别急着加 forwardRef,停下来思考一下依赖关系是否合理,也许能发现更好的设计。

声明:麋鹿与鲸鱼|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - 循环依赖解决:使用 forwardRef 或动态模块


Carpe Diem and Do what I like