【NestJS】08 提供者(Provider)完全指南

2626 字
13 分钟
【NestJS】08 提供者(Provider)完全指南

前言 ✨#

如果说模块是 NestJS 的骨架,那么提供者就是流淌在骨架中的血液 💉 —— 它让各个组件之间能够灵活地协作,而不必关心对象的创建和生命周期管理。

我们之前学习的服务(Service)其实就是一种提供者。理解提供者对于掌握 NestJS 的依赖注入系统至关重要,这也是 NestJS 区别于传统 Node.js 框架的核心优势之一。


一、什么是提供者?#

提供者的定义 🎯#

提供者(Provider) 是 NestJS 中可以被注入的类、值或工厂函数。它可以是以下几种形态:

  • 类提供者(Class Provider):一个类,通常是服务(Service)
  • 值提供者(Value Provider):一个值,比如配置对象或常量
  • 工厂提供者(Factory Provider):一个工厂函数,用于动态创建实例

📚 参考资源

提供者的核心特性 ⚡#

提供者具有以下核心特性:

  1. 可注入性(Injectable):可以被注入到其他类的构造函数中
  2. 可复用性(Reusable):可以在多个模块和组件中共享使用
  3. 可配置性(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%请求日志、用户会话
TRANSIENT1%临时对象、唯一标识

六、自定义提供者的高级用法#

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 {}

第四步:配置环境变量

Terminal window
# .env 文件
DB_TYPE=mongodb
MONGODB_URI=mongodb://localhost:27017/wwzhidao
# 或者
DB_TYPE=postgres
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_DB=wwzhidao

测试验证 ✅#

启动应用后,控制台应该会打印:

Terminal window
数据库配置: { 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 提供者的核心概念和用法。让我来总结一下要点:

核心知识点 ✅#

  • 理解了提供者的定义:可以注入的类、值或工厂函数
  • 掌握了三种提供者类型:类提供者、值提供者、工厂提供者
  • 理解了作用域概念:单例、请求级别、瞬态三种作用域
  • 学习了自定义提供者useClassuseValueuseFactoryuseExisting
  • 完成了实战练习:创建了动态数据库配置提供者
  • 掌握了最佳实践:优先使用类提供者、合理选择作用域等

提供者类型对比 📊#

类型语法使用场景灵活性复杂度
类提供者providers: [Service]服务、仓储、工具类⭐⭐⭐
值提供者useValue: {}配置、常量、外部实例⭐⭐
工厂提供者useFactory: () => {}条件创建、异步初始化⭐⭐⭐⭐⭐⭐⭐⭐⭐
别名提供者useExisting: Service向后兼容、多名称⭐⭐⭐⭐

延伸学习资源 📚#

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!

【NestJS】08 提供者(Provider)完全指南
https://blog.fridolph.top/posts/2025-10-06__nest08-provider/
作者
Fridolph
发布于
2025-10-06
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
Fridolph
热爱 Coding、音乐和羽毛球的 90 后全栈工程师
公告
欢迎访问我的小站 ^_^ 我是昇哥,热爱Coding,喜爱音乐、羽毛球和摄影的 90后全栈工程师
分类
标签

文章目录