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
import { Injectable } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
@Injectable()
export class CatsService {
// 就这样,你拿到了后台通行证!
constructor(private moduleRef: ModuleRef) {}
}
ModuleRef
最核心的方法就是 get()
。它接收一个令牌(通常是类名或字符串令牌)作为参数,然后返回该令牌对应的提供者实例。
示例:在方法中动态获取服务
假设我们有一个 LoggerService
,但 CatsService
并不是总需要它,只在一个特定的方法里才用。
// 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')来调用不同的支付服务。
第一步:定义支付接口和实现
// 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
// 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 };
}
}
第三步:在模块中注册所有服务
// 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
@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
)
@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
。它是一个解决特定复杂问题的“专家工具”,而不是日常使用的“螺丝刀”。过度使用它会让你的代码变得难以理解和维护。