【NestJS】08 提供者(Provider)完全指南
前言 ✨
如果说模块是 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.DEFAULTexport 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),并提供相应的连接配置。
实现步骤
第一步:创建数据库模块
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 {}第二步:在服务中使用
@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 []; }}第三步:导入模块
@Module({ imports: [DatabaseModule], // 导入数据库模块 controllers: [UserController], providers: [UserService],})export class UserModule {}第四步:配置环境变量
# .env 文件DB_TYPE=mongodbMONGODB_URI=mongodb://localhost:27017/wwzhidao
# 或者DB_TYPE=postgresPOSTGRES_HOST=localhostPOSTGRES_PORT=5432POSTGRES_DB=wwzhidao测试验证 ✅
启动应用后,控制台应该会打印:
数据库配置: { type: 'mongodb', uri: 'mongodb://localhost:27017/wwzhidao' }类型安全优化 🛡️
使用常量 token 提高类型安全:
export const DATABASE_CONNECTION = 'DATABASE_CONNECTION';
// src/database/interfaces.tsexport 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 | 向后兼容、多名称 | ⭐⭐ | ⭐⭐ |
延伸学习资源 📚
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!