别再只会用@Injectable了!NestJS Providers的四种高级玩法(含useFactory异步工厂实战)
在NestJS开发中,@Injectable()可能是你最熟悉的装饰器,但Providers的能力远不止于此。当你需要处理动态配置、异步初始化或复杂依赖关系时,基础用法往往捉襟见肘。本文将带你突破常规,探索四种高阶Providers用法,特别是如何用useFactory解决异步服务初始化的难题。
1. 动态配置:useValue的实战技巧
useValue常被简单理解为静态值注入,但在实际项目中,它能实现更灵活的配置管理。假设我们需要根据环境变量动态配置数据库连接:
// config/database.config.ts export const DatabaseConfig = { host: process.env.DB_HOST || 'localhost', port: parseInt(process.env.DB_PORT) || 5432, username: process.env.DB_USER, password: process.env.DB_PASS }; // database.module.ts @Module({ providers: [ { provide: 'DATABASE_CONFIG', useValue: DatabaseConfig }, DatabaseService ] }) export class DatabaseModule {}进阶技巧:通过useValue注入函数,可以实现动态计算:
{ provide: 'DYNAMIC_CONFIG', useValue: (key: string) => process.env[key] || config.get(key) }注意:当注入的值需要复杂计算时,应考虑改用
useFactory
2. 自定义Token:突破类名限制的依赖注入
默认情况下,NestJS使用类名作为注入标识符,但在以下场景需要自定义Token:
- 接口实现多态:多个服务实现同一接口
- 避免命名冲突:第三方库与自有服务同名
- 动态依赖解析:运行时决定具体实现
// 定义抽象接口 export abstract class CacheService { abstract get(key: string): Promise<string>; } // Redis实现 @Injectable() export class RedisCacheService implements CacheService { // ...实现细节 } // 模块配置 @Module({ providers: [ { provide: 'CACHE_SERVICE', useClass: RedisCacheService } ] }) export class CacheModule {} // 使用时通过@Inject注入 @Injectable() export class UserService { constructor( @Inject('CACHE_SERVICE') private readonly cache: CacheService ) {} }典型应用场景:
- 不同环境使用不同的存储实现(开发用Mock,生产用Redis)
- A/B测试时动态切换服务版本
- 插件系统的基础设施抽象
3. 工厂模式:useFactory处理复杂依赖
当服务初始化需要依赖其他服务或复杂逻辑时,useFactory是最佳选择。以下是一个电商系统中价格计算服务的案例:
@Module({ providers: [ DiscountService, TaxCalculatorService, { provide: 'PRICE_ENGINE', inject: [DiscountService, TaxCalculatorService], useFactory: ( discount: DiscountService, tax: TaxCalculatorService ) => { return new PriceEngine(discount, tax, { currency: 'USD', rounding: 2 }); } } ] }) export class PricingModule {}工厂模式的优势:
- 延迟创建:只在需要时实例化对象
- 依赖解耦:工厂函数处理所有依赖关系
- 灵活配置:可基于运行时条件返回不同实现
4. 异步工厂:useFactory处理异步初始化
现代应用常需要异步加载配置或初始化连接,这正是异步工厂的用武之地。下面演示如何异步初始化一个需要从远程加载配置的邮件服务:
@Module({ providers: [ ConfigService, { provide: 'MAIL_SERVICE', inject: [ConfigService], useFactory: async (config: ConfigService) => { const mailConfig = await config.load('mail'); const transporter = nodemailer.createTransport({ host: mailConfig.host, port: mailConfig.port, auth: { user: mailConfig.user, pass: mailConfig.pass } }); await transporter.verify(); return transporter; } } ], exports: ['MAIL_SERVICE'] }) export class MailModule {}关键实践要点:
- 错误处理:工厂函数应该妥善处理异步错误
- 健康检查:关键服务应验证连接状态
- 超时控制:设置合理的初始化超时时间
// 增强版的错误处理 useFactory: async (config: ConfigService) => { try { const mailConfig = await Promise.race([ config.load('mail'), new Promise((_, reject) => setTimeout(() => reject(new Error('Config load timeout')), 5000) ) ]); // ...其余初始化逻辑 } catch (err) { logger.error('Mail service init failed', err); throw new ServiceUnavailableException('Mail service initialization failed'); } }5. 综合实战:数据库连接池的动态配置
结合上述所有技术,我们实现一个完整的数据库连接池管理方案:
@Module({ providers: [ { provide: 'DB_CONFIG_LOADER', useFactory: async () => { // 模拟从远程配置中心加载 return new Promise<DbConfig>((resolve) => { setTimeout(() => resolve({ host: 'cluster.prod.db', poolSize: 20, timeout: 3000 }), 1000); }); } }, { provide: 'DATABASE_POOL', inject: ['DB_CONFIG_LOADER', MonitoringService], useFactory: async ( config: DbConfig, monitor: MonitoringService ) => { const pool = new Pool(config); pool.on('error', (err) => { monitor.recordDatabaseError(err); }); await pool.connect(); // 测试连接 return pool; } } ] }) export class DatabaseModule {}性能优化技巧:
- 使用
APP_INITIALIZER提前初始化关键服务 - 实现健康检查接口监控服务状态
- 考虑连接池的预热策略
// 应用启动时初始化 { provide: APP_INITIALIZER, useFactory: (pool) => () => pool.warmup(), inject: ['DATABASE_POOL'], multi: true }在大型NestJS应用中合理组合这四种Providers用法,可以优雅解决以下工程难题:
- 配置信息的集中管理
- 服务依赖的动态解析
- 异步资源的初始化顺序控制
- 实现可测试的松耦合架构
掌握这些高阶技巧后,你会发现NestJS的依赖注入系统远比表面看到的强大。最近在一个微服务项目中,我们通过自定义Token+工厂模式,成功实现了在不修改核心业务代码的情况下动态切换消息队列实现,从RabbitMQ迁移到Kafka只花了不到半天时间。