Skip to content

ModuleRef —— 你的“幕后总管”或“后台通行证”。

揭秘 NestJS 的“幕后总管”:ModuleRef 动态获取服务

想象一下,你正在参加一个盛大的晚宴。

通常情况下,服务员(NestJS 的依赖注入 DI 容器)会根据你桌上的菜单(构造函数 constructor),自动地、有条不紊地将菜品(服务 Provider)送到你的面前。这是最优雅、最高效的方式,我们称之为声明式依赖注入

但现在,你突然有了一个特殊的需求。你不想等服务员上菜,而是想直接走进厨房,对厨师说:“我现在就要一份那个正在烤的牛排!”。这种主动去获取服务的能力,就是程序化地获取提供者

ModuleRef 类,就是 NestJS 给你的那张可以自由进出“厨房”的“后台通行证”。它允许你在代码的任何地方,动态地、按需地获取任何在当前模块上下文中可用的提供者实例。

1. 为什么我需要一张“后台通行证”?

你可能会问:“服务员自动上菜不是挺好的吗?我为什么非要自己跑去厨房?”

在 95% 的情况下,你都不需要。标准的构造函数注入是最佳实践。但在一些特殊场景下,ModuleRef 就成了解决问题的关键:

  • 动态解析提供者:假设你有多个“支付服务”(AlipayService, WechatPayService),它们都实现了同一个 Payment 接口。你希望根据用户的选择(一个字符串,如 'alipay'),在运行时动态地获取对应的支付服务实例。
  • 在单例服务中获取请求作用域的服务:这是一个非常重要的用例。你的一个单例服务(CEO),想知道某个特定请求(项目)的信息。如果它直接注入一个请求作用域的服务(项目顾问),它自己也会被“降级”成请求作用域,造成性能损失。通过 ModuleRef,这个 CEO 可以在需要时,临时“召唤”一个只服务于当前请求的“项目顾问”,而自己保持单例的身份。
  • 在非类方法中获取服务:比如在一个独立的函数或脚本中,如果能拿到 ModuleRef,就能利用整个 NestJS 应用的 DI 生态。

2. 如何获得并使用 ModuleRef

获取 ModuleRef 本身非常简单,就像注入其他任何服务一样。

src/cats/cats.service.ts

typescript
import { Injectable } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';

@Injectable()
export class CatsService {
  // 就这样,你拿到了后台通行证!
  constructor(private moduleRef: ModuleRef) {}
}

ModuleRef 最核心的方法就是 get()。它接收一个令牌(通常是类名或字符串令牌)作为参数,然后返回该令牌对应的提供者实例。

示例:在方法中动态获取服务

假设我们有一个 LoggerService,但 CatsService 并不是总需要它,只在一个特定的方法里才用。

typescript
// logger.service.ts
@Injectable()
export class LoggerService {
  log(message: string) {
    console.log(`[LOG]: ${message}`);
  }
}

// cats.service.ts
@Injectable()
export class CatsService {
  constructor(private moduleRef: ModuleRef) {}

  findAndLog() {
    console.log('Finding all cats...');
    const cats = ['Mittens', 'Felix'];

    // 在需要时,才去“厨房”拿 LoggerService
    const logger = this.moduleRef.get(LoggerService);
    logger.log(`${cats.length} cats found.`);

    return cats;
  }
}

这展示了基本用法,但还不够体现它的威力。让我们看一个更真实的例子。

3. “超级能力”实战:动态选择支付渠道

场景:用户下单后,根据 paymentMethod 字段('alipay' 或 'wechat')来调用不同的支付服务。

第一步:定义支付接口和实现

typescript
// payment.interface.ts
export interface PaymentProvider {
  pay(amount: number): string;
}

// alipay.service.ts
@Injectable()
export class AlipayService implements PaymentProvider {
  pay(amount: number) {
    return `Paid ${amount} via Alipay.`;
  }
}

// wechat.service.ts
@Injectable()
export class WechatPayService implements PaymentProvider {
  pay(amount: number) {
    return `Paid ${amount} via WeChat Pay.`;
  }
}

第二步:创建 OrdersService 并注入 ModuleRef

typescript
// orders.service.ts
import { Injectable } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { AlipayService } from './alipay.service';
import { WechatPayService } from './wechat.service';
import { PaymentProvider } from './payment.interface';

// 创建一个映射,将字符串和类名对应起来
const paymentProviderMap = {
  alipay: AlipayService,
  wechat: WechatPayService,
};

type PaymentMethod = 'alipay' | 'wechat';

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

  createOrder(amount: number, method: PaymentMethod) {
    console.log(`Creating an order of ${amount} to be paid with ${method}.`);

    // 1. 根据传入的方法名,从映射中找到对应的类名(令牌)
    const providerToken = paymentProviderMap[method];

    // 2. 使用 moduleRef.get() 和这个动态的令牌来获取实例
    const paymentProvider = this.moduleRef.get<PaymentProvider>(providerToken);

    // 3. 调用获取到的实例的方法
    const result = paymentProvider.pay(amount);
    console.log(result);
    return { success: true, message: result };
  }
}

第三步:在模块中注册所有服务

typescript
// app.module.ts
@Module({
  providers: [OrdersService, AlipayService, WechatPayService],
  // ...
})
export class AppModule {}

现在,OrdersService 可以根据传入的字符串,灵活地决定使用哪个支付服务,而无需在构造函数中把所有支付服务都注入一遍。

4. 终极用法:在单例中安全地使用请求作用域服务

这是 ModuleRef 最重要、也最高级的用法。

前置知识回顾:作用域

  • Singleton (单例):应用生命周期内只有一个实例。
  • Request (请求):每个 HTTP 请求都会创建一个新实例。它可以注入 REQUEST 对象来获取请求相关信息。

问题:我的 AppService 是单例的,我想在其中记录每个请求的唯一 ID。这个 ID 由一个 RequestScopeService 提供。如果我直接注入,AppService 会被“污染”成请求作用 อด 域,性能下降。

解决方案AppService 保持单例,但注入 ModuleRef。在处理请求时,通过 moduleRef 临时解析出属于当前请求的 RequestScopeService 实例。

src/request-scope.service.ts

typescript
@Injectable({ scope: Scope.REQUEST })
export class RequestScopeService {
  private readonly requestId: string = Math.random().toString(36).substring(2);

  getRequestId() {
    console.log(
      `[RequestScopeService instance: ${this.requestId}] Providing ID.`
    );
    return this.requestId;
  }
}

src/app.service.ts (使用 ModuleRef)

typescript
@Injectable() // 保持默认的 Singleton 作用域
export class AppService {
  constructor(private moduleRef: ModuleRef) {}

  async getHello(): Promise<string> {
    // 关键!在方法执行时,动态解析 RequestScopeService
    // { strict: false } 允许解析器在整个模块图中查找,而不仅限于当前模块
    const requestScopeService = await this.moduleRef.resolve(
      RequestScopeService,
      undefined,
      { strict: false }
    );

    const requestId = requestScopeService.getRequestId();
    const message = `Hello World! This is being handled for request ID: ${requestId}`;

    console.log(
      `[AppService] (Singleton) is working with request ID: ${requestId}`
    );
    return message;
  }
}
  • moduleRef.resolve():这是 get() 的一个变体,它总是返回一个 Promise,非常适合用来解析请求作用域的提供者。

现在,AppService 依然是高性能的单例,但它具备了在需要时与请求上下文互动的能力,而不会牺牲自己的身份。

5. 一句忠告:能力越大,责任越大

ModuleRef 非常强大,但它是一把双刃剑。

构造函数注入 (默认方式)ModuleRef
范式声明式 (Declarative)命令式 (Programmatic)
依赖可见性 (所有依赖在构造函数中一目了然) (依赖隐藏在方法内部,不易发现)
代码可读性较低
测试难度 (易于模拟依赖)较高 (需要模拟 ModuleRef 本身)
使用场景95% 的情况动态解析、作用域穿透等特殊场景

核心原则优先使用标准的构造函数注入。只有当标准的 DI 模式无法解决你的问题时,才考虑使用 ModuleRef。它是一个解决特定复杂问题的“专家工具”,而不是日常使用的“螺丝刀”。过度使用它会让你的代码变得难以理解和维护。