自定义提供器
约 3439 字大约 11 分钟
2025-07-19
DI 基础概念
依赖注入是一种控制反转(IoC)技术。在传统编程中,我们通常在代码内部直接创建和管理依赖对象;
而控制反转则将这一控制权交给外部容器,在 NestJS 中就是其运行时系统。这样,依赖项的实例化不再由使用方硬编码创建,而是由 IoC 容器统一管理和注入。
这种机制带来了几个关键优势:它让各个组件之间的耦合度显著降低,提升了代码的可测试性和模块化程度,同时也让整个应用架构更加灵活和可维护。
依赖注入(DI)是 NestJS 的核心机制,其底层实现可以概括为三个关键步骤:
第一步:声明提供者 在 cats.service.ts 中,使用 @Injectable() 装饰器将 CatsService 标记为一个提供者。这相当于告诉 Nest IoC 容器:“这个类可以由容器来管理和实例化”。
第二步:声明依赖 在 cats.controller.ts 中,CatsController 通过其构造函数声明它依赖于 CatsService:
constructor(private catsService: CatsService)这里的 CatsService 在这里充当着一个“依赖令牌”(Token)的角色。
第三步:注册关联 在 app.module.ts 中,我们在 @Module 装饰器的 providers 数组中注册 CatsService。
这一步的本质是在 IoC 容器中建立了一个映射关系:当需要 CatsService 这个令牌时,就使用 cats.service.ts 文件中的 CatsService 类来满足需求。
底层解析过程 当 Nest IoC 容器需要实例化 CatsController 时,其内部运作流程如下:
- 发现依赖:容器检查
CatsController的构造函数,发现它依赖一个名为CatsService的令牌。 - 令牌查找:容器根据第三步的注册信息,查找
CatsService令牌所对应的具体类。 - 实例化或返回:假设是默认的单例(SINGLETON)模式,容器会检查是否已经缓存过
CatsService的实例。如果有,则直接返回该现有实例;如果没有,则创建一个新的实例并缓存起来,最后将其注入到CatsController中。
注意: 以上描述进行了一定程度的简化。一个至关重要的细节是,整个依赖关系的解析过程(或称“构建依赖图”)是在应用程序启动引导阶段完成的。这个过程是传递性的——如果 CatsService 自身也依赖其他服务,这些依赖也会被递归地解析。依赖图确保了所有对象都能按照正确的、自底向上的顺序被创建出来。正是这种自动化机制,将开发者从手动管理和组装复杂依赖关系的繁琐工作中彻底解放出来。
标准提供者
@Module({
controllers: [CatsController],
providers: [CatsService],
})providers 属性接受一个提供者数组。
之前,我们一直通过一个类名列表(如 [CatsService])来提供这些依赖。实际上,这种写法是一种语法糖,它等价于下面更完整的对象语法:
providers: [
{
provide: CatsService, // 依赖令牌(Token)
useClass: CatsService, // 要实例化的类
},
];通过这种显式的配置对象,注册过程就一目了然了:我们在这里清晰地将令牌 CatsService 与类 CatsService 进行了关联。
简写形式 providers: [CatsService] 只是为了方便最常见的场景,即当依赖的令牌与你想要实例化的类是同一个类型时。
Nest 会自动将其扩展为完整的 { provide: CatsService, useClass: CatsService } 形式。
这种完整语法揭示了依赖注入容器的核心机制:建立一个映射关系,指明“当请求某个令牌时,应该使用哪个类(或值、工厂等)来满足依赖”。
自定义提供程序
当您的需求超出了标准提供者的能力范围时,该怎么办?以下是一些典型场景:
- 您希望手动创建实例,而不是让 Nest 自动实例化(或返回其缓存实例)。
- 您希望在多个依赖项中复用某个现有的类或值,而不仅仅是其自身。
- 您想要在测试时使用模拟版本(Mock)来覆盖一个真实的类。
为了应对这些复杂情况,Nest 允许您定义自定义提供者,它提供了多种灵活的方式来定义依赖关系。
提示:如果在依赖解析过程中遇到问题,您可以设置 NEST_DEBUG 环境变量。这样在应用程序启动时,就能在终端看到额外的依赖关系解析日志,帮助您定位问题。
值提供者:useValue
useValue 语法非常适合注入常量、将第三方库纳入 Nest 容器管理,或者在测试时使用模拟对象(Mock)替换真实的实现。它的一个高级应用场景是基于环境变量动态提供配置。
基础示例:模拟服务 假设您希望在测试中强制 Nest 使用一个模拟的 CatsService:
import { CatsService } from './cats.service';
// 定义一个模拟对象,它实现了与真实服务相同的接口
const mockCatsService = {
findAll: () => ['模拟猫1', '模拟猫2'], // 模拟实现
// ... 其他方法
};
@Module({
imports: [CatsModule],
providers: [
{
provide: CatsService, // 依赖令牌
useValue: mockCatsService, // 直接使用这个值作为实例
},
],
})
export class AppModule {}在此示例中,任何依赖 CatsService 令牌的类,都将获得 mockCatsService 这个模拟对象。
由于 TypeScript 的结构类型特性,只要你的模拟对象与真实类具有兼容的接口(即拥有相同的方法和属性),Nest 就会接受它。这可以是字面量对象,也可以是 new 创建的实例。
高级用法:动态配置与错误处理
useValue 的真正威力在于注入任何值,包括那些动态生成的配置对象,并可以结合错误处理逻辑。
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
// 创建一个配置对象,其值可能来自环境变量
const dynamicConfig = {
database: {
host: process.env.DATABASE_HOST || 'localhost',
port: parseInt(process.env.DATABASE_PORT, 10) || 5432,
// 添加一个简单的配置验证
get connectionString() {
if (!this.host) {
throw new Error('DATABASE_HOST environment variable is required');
}
return `postgresql://${this.host}:${this.port}`;
}
},
apiKey: process.env.API_KEY, // 敏感信息从环境变量读取
isProduction: process.env.NODE_ENV === 'production',
};
@Module({
imports: [
// 导入 ConfigModule 来管理环境变量(推荐做法)
ConfigModule.forRoot(),
],
providers: [
{
provide: 'CONFIG', // 使用字符串作为令牌,适用于非类依赖
useValue: dynamicConfig,
},
{
provide: 'EMAIL_SERVICE',
useValue: {
send: async (to: string, subject: string) => {
// 模拟一个简单的邮件服务,包含错误处理
try {
if (!process.env.SMTP_SERVER) {
throw new Error('SMTP configuration is missing');
}
// 实际的邮件发送逻辑...
console.log(`Sending email to ${to}: ${subject}`);
return { success: true };
} catch (error) {
// 在这里可以记录日志或触发监控告警
console.error('Email service error:', error.message);
throw new Error('Failed to send email');
}
}
},
},
],
exports: ['CONFIG', 'EMAIL_SERVICE'], // 导出以供其他模块使用
})
export class AppModule {}使用方式:
import { Inject, Injectable } from '@nestjs/common';
@Injectable()
export class UserService {
constructor(
@Inject('CONFIG') private readonly config: any,
@Inject('EMAIL_SERVICE') private readonly emailService: any,
) {}
async createUser(userData: any) {
// 使用配置
console.log(`Connecting to: ${this.config.database.connectionString}`);
// 使用注入的服务,并自动获得错误处理能力
await this.emailService.send(userData.email, 'Welcome!');
// ... 创建用户的逻辑
}
}关键要点:
- 环境变量集成:
useValue可以轻松封装从环境变量读取的配置,并在应用启动时就完成初始化。 - 简易验证:可以在配置对象中加入 getter 或方法,在访问时进行运行时的配置验证。
- 内置错误处理:对于服务类模拟,可以直接在方法内实现 try-catch 逻辑,为所有消费者提供统一的错误处理。
- 字符串令牌:对于值提供者,经常使用字符串作为令牌(如
'CONFIG'),注入时需要使用@Inject()装饰器。
这种模式让您的应用配置和外部服务集成变得更加声明式和可测试。
工厂提供者:useFactory
useFactory 语法允许我们动态地创建提供者,实际的提供者值将由工厂函数的返回值决定。
工厂函数可以根据需求设计得简单或复杂:简单的工厂可能不依赖任何提供者;
而复杂的工厂则可以注入它所需要的其它提供者,以便计算结果。
对于后一种情况,工厂提供者语法提供了一对相关机制来支持依赖注入:
工厂函数参数:工厂函数可以接受(可选的)参数。
inject 属性:(可选的)inject 属性接受一个提供者数组,Nest 在实例化过程中会解析这些提供者,并将它们作为参数传递给工厂函数。这些提供者也可以标记为可选的。需要注意的是,inject 数组中的提供者顺序与工厂函数的参数顺序必须一一对应。
下面的示例将演示如何使用这些机制:
const connectionProvider = {
provide: 'CONNECTION',
useFactory: (optionsProvider: MyOptionsProvider, optionalProvider?: string) => {
const options = optionsProvider.get();
return new DatabaseConnection(options);
},
inject: [MyOptionsProvider, { token: 'SomeOptionalProvider', optional: true }],
// \_______________/ \___________________________/
// 此提供者是必需的 此令牌对应的提供者可以解析为 `undefined`
};
@Module({
providers: [
connectionProvider,
MyOptionsProvider, // 基于类的提供者
// { provide: 'SomeOptionalProvider', useValue: 'anything' },
],
})
export class AppModule {}- 工厂函数 (
useFactory): 它是一个可以返回任意值的函数,返回值将作为provide所指定令牌的提供者实例。函数的参数可以来自其他提供者,由inject数组定义。 - 依赖注入 (
inject):inject数组定义了工厂函数所依赖的提供者。Nest 会按顺序解析这些提供者,并将它们作为参数传递给工厂函数。如果某个依赖被标记为optional: true,则即使该提供者不存在,也不会报错,对应的参数值将为undefined。 - 可选依赖: 通过
{ token: 'SomeOptionalProvider', optional: true }这样的语法,我们可以声明一个依赖是可选的。这在某些情况下非常有用,例如当某个功能依赖的外部服务可能不存在时。
假设我们有一个更复杂的场景,需要根据配置动态创建数据库连接,并且需要日志服务(可选)来记录连接过程:
import { Module } from '@nestjs/common';
class ConfigService {
getDatabaseOptions() {
return { host: 'localhost', port: 5432 };
}
}
class LoggerService {
log(message: string) {
console.log(`[LOG] ${message}`);
}
}
const databaseConnectionFactory = {
provide: 'DATABASE_CONNECTION',
useFactory: (
configService: ConfigService,
loggerService?: LoggerService, // 可选依赖
) => {
const options = configService.getDatabaseOptions();
if (loggerService) {
loggerService.log('Creating database connection...');
}
// 模拟创建数据库连接
const connection = `Connected to ${options.host}:${options.port}`;
if (loggerService) {
loggerService.log('Database connection established.');
}
return connection;
},
inject: [
ConfigService,
{ token: 'LOGGER_SERVICE', optional: true }, // 可选依赖
],
};
@Module({
providers: [
ConfigService,
// LoggerService, // 如果取消注释,LoggerService 将被注入
databaseConnectionFactory,
// 可以在这里提供 LOGGER_SERVICE,例如:
// { provide: 'LOGGER_SERVICE', useClass: LoggerService },
],
})
export class AppModule {}ConfigService是必需的依赖,提供数据库配置。LoggerService被声明为可选依赖。如果它被注册在模块中,则会被注入到工厂函数中,用于记录日志;否则,参数loggerService的值为undefined。- 工厂函数根据是否传入
loggerService来决定是否记录日志。
别名提供者:useExisting
useExisting 语法允许为现有提供者创建别名,这样我们就可以通过多个令牌访问同一个提供者实例。这在重构、接口隔离和向后兼容等场景中非常有用。
基本语法和原理
const aliasProvider = {
provide: 'ALIAS_TOKEN', // 新的令牌
useExisting: ExistingService, // 现有的提供者令牌
};演示
import { Module, Injectable, Inject } from '@nestjs/common';
@Injectable()
class LoggerService {
private logs: string[] = [];
log(message: string) {
const timestamp = new Date().toISOString();
const logEntry = `[${timestamp}] ${message}`;
this.logs.push(logEntry);
console.log(logEntry);
}
getLogs() {
return this.logs;
}
}
// 为 LoggerService 创建别名提供者
const loggerAliasProvider = {
provide: 'AliasedLoggerService', // 字符串令牌别名
useExisting: LoggerService, // 指向原有的类令牌
};
// 可以创建多个别名
const anotherLoggerAlias = {
provide: 'SimpleLogger',
useExisting: LoggerService,
};
@Injectable()
class UserService {
constructor(
private readonly logger: LoggerService, // 使用类令牌注入
@Inject('AliasedLoggerService')
private readonly aliasedLogger: LoggerService, // 使用别名注入,实际上是同一个实例
) {}
createUser(username: string) {
this.logger.log(`Creating user: ${username}`);
this.aliasedLogger.log(`User created via alias: ${username}`);
// 验证它们是同一个实例
console.log('Are they the same instance?', this.logger === this.aliasedLogger);
return { id: 1, username };
}
}
@Injectable()
class OrderService {
constructor(
@Inject('SimpleLogger')
private readonly logger: LoggerService, // 使用另一个别名注入
) {}
createOrder(orderId: string) {
this.logger.log(`Creating order: ${orderId}`);
return { id: orderId, status: 'created' };
}
}
@Module({
providers: [
LoggerService, // 原始提供者
loggerAliasProvider, // 第一个别名
anotherLoggerAlias, // 第二个别名
UserService,
OrderService,
],
})
export class AppModule {}实际应用场景
向后兼容性
当重构服务类名时,可以为旧名称创建别名以保持兼容:
// 重构前的服务
@Injectable()
class OldLoggerService {
log(message: string) {
console.log(`OLD: ${message}`);
}
}
// 重构后的服务
@Injectable()
class NewLoggerService {
log(message: string) {
console.log(`NEW: ${message}`);
}
}
// 保持向后兼容的别名
const backwardCompatibilityProvider = {
provide: 'OldLoggerService',
useExisting: NewLoggerService,
};
@Module({
providers: [
NewLoggerService,
backwardCompatibilityProvider,
],
})
export class AppModule {}接口隔离
为同一个服务提供不同的"视图"或接口:
// 主服务实现
@Injectable()
class DataService {
private data: Map<string, any> = new Map();
set(key: string, value: any) {
this.data.set(key, value);
}
get(key: string) {
return this.data.get(key);
}
getAll() {
return Array.from(this.data.entries());
}
}
// 只读接口的别名
const readOnlyDataProvider = {
provide: 'ReadOnlyDataService',
useExisting: DataService,
};
@Injectable()
class ReportService {
constructor(
@Inject('ReadOnlyDataService')
private readonly dataService: DataService, // 实际上只能使用读取方法
) {}
generateReport() {
// 只能调用 get() 和 getAll() 方法
const allData = this.dataService.getAll();
return `Report: ${JSON.stringify(allData)}`;
}
}多环境配置
为不同环境提供相同的服务但不同的配置:
@Injectable()
class ConfigService {
constructor(private readonly environment: string) {}
getDatabaseConfig() {
if (this.environment === 'production') {
return { host: 'prod-db', port: 5432 };
}
return { host: 'localhost', port: 5432 };
}
}
// 开发环境配置别名
const devConfigProvider = {
provide: 'DevConfigService',
useExisting: ConfigService,
};
// 生产环境配置别名
const prodConfigProvider = {
provide: 'ProdConfigService',
useExisting: ConfigService,
};
@Module({
providers: [
{
provide: ConfigService,
useFactory: () => new ConfigService(process.env.NODE_ENV || 'development'),
},
devConfigProvider,
prodConfigProvider,
],
})
export class AppModule {}验证单例行为
@Injectable()
class VerificationService {
constructor(
private readonly logger1: LoggerService,
@Inject('AliasedLoggerService')
private readonly logger2: LoggerService,
@Inject('SimpleLogger')
private readonly logger3: LoggerService,
) {
// 验证所有别名都指向同一个实例
console.log('logger1 === logger2:', logger1 === logger2); // true
console.log('logger1 === logger3:', logger1 === logger3); // true
console.log('logger2 === logger3:', logger2 === logger3); // true
// 验证它们共享状态
logger1.log('Message from logger1');
console.log('logger2 logs:', logger2.getLogs()); // 包含上面的消息
console.log('logger3 logs:', logger3.getLogs()); // 包含上面的消息
}
}