前言 ✨
如果说模块是 NestJS 的骨架,那么提供者就是流淌在骨架中的血液 💉 —— 它让各个组件之间能够灵活地协作,而不必关心对象的创建和生命周期管理。
我们之前学习的服务(Service)其实就是一种提供者。理解提供者对于掌握 NestJS 的依赖注入系统至关重要,这也是 NestJS 区别于传统 Node.js 框架的核心优势之一。
一、什么是提供者?
提供者的定义 🎯
提供者(Provider) 是 NestJS 中可以被注入的类、值或工厂函数。它可以是以下几种形态:
- 类提供者(Class Provider):一个类,通常是服务(Service)
- 值提供者(Value Provider):一个值,比如配置对象或常量
- 工厂提供者(Factory Provider):一个工厂函数,用于动态创建实例
📚 参考资源:
提供者的核心特性 ⚡
提供者具有以下核心特性:
- 可注入性(Injectable):可以被注入到其他类的构造函数中
- 可复用性(Reusable):可以在多个模块和组件中共享使用
- 可配置性(Configurable):可以根据不同的配置创建不同的实例
注册提供者 📦
提供者需要在模块的 providers 数组中注册:
@Module({
providers: [UserService], // 注册提供者
})
export class UserModule {}二、类提供者(Class Provider)
基本用法
类提供者是最常见的提供者类型,直接传入类即可:
@Module({
providers: [UserService], // 简写形式
})
export class UserModule {}这实际上是以下完整形式的简写:
@Module({
providers: [
{
provide: UserService, // token(标识符)
useClass: UserService, // 使用的具体类
},
],
})
export class UserModule {}使用抽象类实现接口模式 🎨
虽然 TypeScript 的接口在运行时会被擦除,但我们可以使用抽象类来实现类似接口的效果:
// 定义抽象类作为契约
abstract class IUserRepository {
abstract findAll(): Promise<User[]>;
abstract findById(id: string): Promise<User | null>;
}
// 具体实现
@Injectable()
export class UserRepository implements IUserRepository {
async findAll(): Promise<User[]> {
// 数据库查询逻辑
return [];
}
async findById(id: string): Promise<User | null> {
return null;
}
}
// 模块注册
@Module({
providers: [
{
provide: IUserRepository, // 使用抽象类作为 token
useClass: UserRepository, // 绑定具体实现
},
],
})
export class UserModule {}在服务中使用:
@Injectable()
export class UserService {
constructor(
@Inject(IUserRepository) // 注入抽象类
private readonly userRepository: IUserRepository,
) {}
async getAllUsers() {
return this.userRepository.findAll();
}
}这种模式的优势:
- ✅ 易于测试:可以轻松替换为 Mock 实现
- ✅ 松耦合:服务层不依赖具体实现
- ✅ 易于扩展:可以轻松切换不同的数据库实现
三、值提供者(Value Provider)
基本用法
值提供者直接提供一个静态值,适合配置对象、常量等场景:
@Module({
providers: [
{
provide: 'DATABASE_CONFIG', // 字符串 token
useValue: { // 直接提供值
host: 'localhost',
port: 27017,
database: 'wwzhidao',
},
},
],
})
export class DatabaseModule {}使用值提供者
通过 @Inject() 装饰器注入:
@Injectable()
export class DatabaseService {
constructor(
@Inject('DATABASE_CONFIG')
private readonly config: DatabaseConfig,
) {
console.log(`连接到: ${this.config.host}:${this.config.port}`);
}
}典型使用场景 🎯
- 配置对象:应用配置、数据库连接信息
- 常量值:API 密钥、版本号、环境变量
- 外部库实例:已实例化的第三方库对象
import axios from 'axios';
@Module({
providers: [
{
provide: 'HTTP_CLIENT',
useValue: axios.create({
baseURL: 'https://api.example.com',
timeout: 5000,
}),
},
],
})
export class HttpModule {}四、工厂提供者(Factory Provider)
基本用法
工厂提供者通过工厂函数动态创建实例,可以根据配置、环境变量或其他依赖来决定创建什么样的实例:
@Module({
providers: [
{
provide: 'DATABASE_CONNECTION',
useFactory: (configService: ConfigService) => {
const dbUrl = configService.get('DATABASE_URL');
return createConnection(dbUrl); // 根据配置创建连接
},
inject: [ConfigService], // 声明依赖
},
],
})
export class DatabaseModule {}📚 参考资源:工厂模式详解
多依赖注入
工厂函数可以接收多个依赖:
{
provide: 'USER_REPOSITORY',
useFactory: (configService: ConfigService, logger: Logger) => {
const dbType = configService.get('DB_TYPE');
if (dbType === 'mongodb') {
return new MongoUserRepository(logger);
} else if (dbType === 'postgres') {
return new PostgresUserRepository(logger);
}
throw new Error(`不支持的数据库类型: ${dbType}`);
},
inject: [ConfigService, Logger], // 按顺序注入
}异步工厂 ⏳
工厂函数也可以是异步的,NestJS 会自动等待 Promise 完成:
{
provide: 'ASYNC_CONFIG',
useFactory: async (configService: ConfigService) => {
const remoteConfig = await fetch('https://api.example.com/config')
.then(res => res.json());
return {
...configService.get('localConfig'),
...remoteConfig,
};
},
inject: [ConfigService],
}五、提供者的作用域(Scope)
作用域类型
作用域决定了提供者实例的生命周期和共享范围。NestJS 提供了三种作用域:
📚 参考资源:NestJS Injection Scopes 文档
1. 默认作用域(Singleton)
整个应用生命周期中只有一个实例:
@Injectable() // 默认就是 Scope.DEFAULT
export class UserService {}
// 等价于
@Injectable({ scope: Scope.DEFAULT })
export class UserService {}特点:
- ✅ 性能最好,只创建一次
- ✅ 所有注入点共享同一个实例
- ✅ 适用于大多数场景(95%)
2. 请求级别作用域(Request Scope)
每个 HTTP 请求创建一个新实例:
@Injectable({ scope: Scope.REQUEST })
export class RequestLoggerService {
private requestId: string;
setRequestId(id: string) {
this.requestId = id; // 每个请求独立
}
}特点:
- ✅ 请求隔离,每个请求有独立状态
- ⚠️ 性能开销较大
- ⚠️ 不能在应用启动时注入
适用场景:请求级别的日志追踪、用户会话状态
3. 瞬态作用域(Transient Scope)
每次注入时创建一个新实例:
@Injectable({ scope: Scope.TRANSIENT })
export class UniqueIdGenerator {
private readonly id = Math.random().toString(36);
}特点:
- ✅ 完全隔离
- ⚠️ 性能开销最大
- ⚠️ 状态不共享
适用场景:需要完全隔离的临时对象(极少使用)
作用域选择建议 💡
| 作用域 | 性能 | 使用频率 | 典型场景 |
|---|---|---|---|
| DEFAULT | ⭐⭐⭐⭐⭐ | 95% | 服务、仓储、工具类 |
| REQUEST | ⭐⭐⭐ | 4% | 请求日志、用户会话 |
| TRANSIENT | ⭐ | 1% | 临时对象、唯一标识 |
六、自定义提供者的高级用法
1. 使用 useClass:条件类选择
根据环境变量动态选择实现类:
@Module({
providers: [
{
provide: 'USER_REPOSITORY',
useClass: process.env.NODE_ENV === 'test'
? MockUserRepository // 测试环境
: UserRepository, // 生产环境
},
],
})
export class UserModule {}2. 使用 useValue:环境变量注入
直接注入环境变量或配置值:
@Module({
providers: [
{
provide: 'API_KEY',
useValue: process.env.API_KEY || 'default-api-key',
},
],
})
export class ConfigModule {}3. 使用 useFactory:创建外部库实例
使用工厂函数创建配置好的外部库实例:
import axios, { AxiosInstance } from 'axios';
@Module({
providers: [
{
provide: 'HTTP_CLIENT',
useFactory: (configService: ConfigService): AxiosInstance => {
return axios.create({
baseURL: configService.get('API_BASE_URL'),
timeout: 5000,
headers: {
'X-API-Key': configService.get('API_KEY'),
},
});
},
inject: [ConfigService],
},
],
exports: ['HTTP_CLIENT'],
})
export class HttpModule {}4. 使用 useExisting:创建别名
别名允许用不同的名称引用同一个提供者实例:
@Module({
providers: [
UserService,
{
provide: 'UserServiceAlias', // 别名
useExisting: UserService, // 指向已存在的提供者
},
],
})
export class UserModule {}使用场景:
- 向后兼容(重命名类时保持旧名称可用)
- 多名称支持(同一个服务提供多个访问入口)
七、实战案例:创建数据库配置提供者
需求分析 📋
创建一个自定义提供者,根据环境变量动态选择数据库类型(MongoDB 或 PostgreSQL),并提供相应的连接配置。
实现步骤
第一步:创建数据库模块
// src/database/database.module.ts
import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
interface DatabaseConfig {
type: 'mongodb' | 'postgres';
uri?: string;
host?: string;
port?: number;
database?: string;
}
@Module({
providers: [
{
provide: 'DATABASE_CONNECTION',
useFactory: (configService: ConfigService): DatabaseConfig => {
const dbType = configService.get<string>('DB_TYPE', 'mongodb');
if (dbType === 'mongodb') {
return {
type: 'mongodb',
uri: configService.get<string>('MONGODB_URI'),
};
} else if (dbType === 'postgres') {
return {
type: 'postgres',
host: configService.get<string>('POSTGRES_HOST'),
port: configService.get<number>('POSTGRES_PORT'),
database: configService.get<string>('POSTGRES_DB'),
};
}
throw new Error(`不支持的数据库类型: ${dbType}`);
},
inject: [ConfigService],
},
],
exports: ['DATABASE_CONNECTION'],
})
export class DatabaseModule {}第二步:在服务中使用
// src/user/user.service.ts
@Injectable()
export class UserService {
constructor(
@Inject('DATABASE_CONNECTION')
private readonly dbConfig: DatabaseConfig,
) {
console.log('数据库配置:', this.dbConfig);
}
async getUsers() {
if (this.dbConfig.type === 'mongodb') {
console.log(`连接到 MongoDB: ${this.dbConfig.uri}`);
} else if (this.dbConfig.type === 'postgres') {
console.log(`连接到 PostgreSQL: ${this.dbConfig.host}`);
}
return [];
}
}第三步:导入模块
// src/user/user.module.ts
@Module({
imports: [DatabaseModule], // 导入数据库模块
controllers: [UserController],
providers: [UserService],
})
export class UserModule {}第四步:配置环境变量
# .env 文件
DB_TYPE=mongodb
MONGODB_URI=mongodb://localhost:27017/wwzhidao
# 或者
DB_TYPE=postgres
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_DB=wwzhidao测试验证 ✅
启动应用后,控制台应该会打印:
数据库配置: { type: 'mongodb', uri: 'mongodb://localhost:27017/wwzhidao' }类型安全优化 🛡️
使用常量 token 提高类型安全:
// src/database/constants.ts
export const DATABASE_CONNECTION = 'DATABASE_CONNECTION';
// src/database/interfaces.ts
export interface DatabaseConfig {
type: 'mongodb' | 'postgres';
uri?: string;
host?: string;
port?: number;
database?: string;
}
// 使用时
@Injectable()
export class UserService {
constructor(
@Inject(DATABASE_CONNECTION) // 使用常量
private readonly dbConfig: DatabaseConfig,
) {}
}八、提供者的最佳实践
1. 优先使用类提供者 🎯
// ✅ 推荐:简单直接
@Module({
providers: [UserService],
})
// ❌ 避免:不必要的复杂化
@Module({
providers: [
{ provide: UserService, useFactory: () => new UserService() },
],
})2. 使用抽象类增强灵活性 🔌
// ✅ 推荐:依赖抽象
abstract class IUserRepository {
abstract findAll(): Promise<User[]>;
}
@Injectable()
export class UserService {
constructor(
@Inject(IUserRepository)
private readonly repository: IUserRepository,
) {}
}3. 合理选择作用域 ⚖️
// ✅ 推荐:默认单例(95% 的场景)
@Injectable()
export class UserService {}
// ⚠️ 谨慎使用:请求作用域
@Injectable({ scope: Scope.REQUEST })
export class RequestLoggerService {}4. 使用常量作为字符串 Token 🏷️
// ✅ 推荐:使用常量
export const DATABASE_CONFIG = 'DATABASE_CONFIG';
@Module({
providers: [
{ provide: DATABASE_CONFIG, useValue: { /* ... */ } },
],
})
// ❌ 避免:直接使用字符串(容易拼写错误)
@Module({
providers: [
{ provide: 'DATABASE_CONFIG', useValue: { /* ... */ } },
],
})5. 导出提供者供其他模块使用 📤
@Module({
providers: [SharedService],
exports: [SharedService], // 导出供其他模块使用
})
export class SharedModule {}6. 添加文档注释 📝
/**
* 用户服务
*
* 提供用户相关的业务逻辑,包括:
* - 用户查询
* - 用户创建
* - 用户更新
*
* @remarks 该服务使用单例模式
*/
@Injectable()
export class UserService {}总结 🎯
这节课我们深入学习了 NestJS 提供者的核心概念和用法。让我来总结一下要点:
核心知识点 ✅
- ✅ 理解了提供者的定义:可以注入的类、值或工厂函数
- ✅ 掌握了三种提供者类型:类提供者、值提供者、工厂提供者
- ✅ 理解了作用域概念:单例、请求级别、瞬态三种作用域
- ✅ 学习了自定义提供者:
useClass、useValue、useFactory、useExisting - ✅ 完成了实战练习:创建了动态数据库配置提供者
- ✅ 掌握了最佳实践:优先使用类提供者、合理选择作用域等
提供者类型对比 📊
| 类型 | 语法 | 使用场景 | 灵活性 | 复杂度 |
|---|---|---|---|---|
| 类提供者 | providers: [Service] | 服务、仓储、工具类 | ⭐⭐⭐ | ⭐ |
| 值提供者 | useValue: {} | 配置、常量、外部实例 | ⭐⭐ | ⭐ |
| 工厂提供者 | useFactory: () => {} | 条件创建、异步初始化 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 别名提供者 | useExisting: Service | 向后兼容、多名称 | ⭐⭐ | ⭐⭐ |
延伸学习资源 📚
- 本文链接:https://fridolph.top/posts/2025-10-06__nest08-provider
- 版权声明:本博客所有文章除特别声明外,均默认采用 CC BY-NC-SA 许可协议。