后端
2025年7月15日
15 分钟阅读
Nest.js 依赖注入深入理解
Nest.js 依赖注入深入理解
依赖注入(Dependency Injection)是 Nest.js 的核心特性之一,它让代码更加模块化、可测试和可维护。本文将深入探讨 Nest.js 的依赖注入机制,帮助你更好地理解和使用它。
什么是依赖注入?
基本概念
依赖注入是一种设计模式,通过将依赖项从类外部注入,而不是在类内部创建。这样可以:
- 解耦代码:类不需要知道如何创建依赖项
- 易于测试:可以轻松替换依赖项为模拟对象
- 提高可维护性:依赖关系清晰明了
传统方式 vs 依赖注入
// ❌ 传统方式:在类内部创建依赖
class UsersService {
private database: Database;
constructor() {
this.database = new Database(); // 硬编码依赖
}
}
// ✅ 依赖注入:从外部注入依赖
class UsersService {
constructor(private database: Database) {} // 注入依赖
}提供者(Provider)
标准提供者
使用 @Injectable() 装饰器标记的类就是提供者:
// users/users.service.ts
import { Injectable } from "@nestjs/common";
@Injectable()
export class UsersService {
findAll() {
return ["user1", "user2"];
}
}在模块中注册提供者
// users/users.module.ts
import { Module } from "@nestjs/common";
import { UsersService } from "./users.service";
import { UsersController } from "./users.controller";
@Module({
controllers: [UsersController],
providers: [UsersService], // 注册提供者
exports: [UsersService], // 导出供其他模块使用
})
export class UsersModule {}注入提供者
// users/users.controller.ts
import { Controller, Get } from "@nestjs/common";
import { UsersService } from "./users.service";
@Controller("users")
export class UsersController {
constructor(private readonly usersService: UsersService) {}
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// 通过构造函数注入 UsersService
@Get()
findAll() {
return this.usersService.findAll();
}
}自定义提供者
值提供者(Value Provider)
用于注入常量或配置值:
// app.module.ts
import { Module } from "@nestjs/common";
const config = {
apiKey: "secret-key",
database: "mongodb://localhost:27017",
};
@Module({
providers: [
{
provide: "CONFIG", // 提供者标识符
useValue: config, // 提供的值
},
],
})
export class AppModule {}使用值提供者:
import { Inject, Injectable } from "@nestjs/common";
@Injectable()
export class AppService {
constructor(@Inject("CONFIG") private config: any) {}
getApiKey() {
return this.config.apiKey;
}
}工厂提供者(Factory Provider)
用于根据条件动态创建提供者:
// app.module.ts
import { Module } from "@nestjs/common";
@Module({
providers: [
{
provide: "DATABASE_CONNECTION",
useFactory: (config: ConfigService) => {
// 根据配置创建数据库连接
if (config.get("DATABASE_TYPE") === "postgres") {
return new PostgresConnection();
}
return new MongoDBConnection();
},
inject: [ConfigService], // 注入依赖
},
],
})
export class AppModule {}使用工厂提供者:
import { Inject, Injectable } from "@nestjs/common";
@Injectable()
export class DatabaseService {
constructor(
@Inject("DATABASE_CONNECTION")
private connection: DatabaseConnection
) {}
}类提供者(Class Provider)
用于将接口绑定到具体实现:
// 定义接口
export interface Logger {
log(message: string): void;
}
// 实现类
export class ConsoleLogger implements Logger {
log(message: string) {
console.log(message);
}
}
export class FileLogger implements Logger {
log(message: string) {
// 写入文件
}
}
// 模块配置
@Module({
providers: [
{
provide: "Logger",
useClass:
process.env.NODE_ENV === "production" ? FileLogger : ConsoleLogger,
},
],
})
export class AppModule {}异步提供者(Async Provider)
用于异步初始化提供者:
// app.module.ts
import { Module } from "@nestjs/common";
@Module({
providers: [
{
provide: "ASYNC_CONNECTION",
useFactory: async () => {
const connection = await createConnection({
host: "localhost",
port: 3306,
});
return connection;
},
},
],
})
export class AppModule {}作用域(Scope)
默认作用域:单例
默认情况下,所有提供者都是单例的,在整个应用生命周期中只创建一个实例:
@Injectable()
export class UsersService {
constructor() {
console.log("UsersService created"); // 只会打印一次
}
}请求作用域(REQUEST)
每个请求创建一个新实例:
@Injectable({ scope: Scope.REQUEST })
export class UsersService {
constructor() {
console.log("UsersService created"); // 每个请求都会打印
}
}注意:请求作用域提供者不能直接注入到单例提供者中,需要使用 Ref 或 RE:
import { Injectable, Scope, Inject, REQUEST } from "@nestjs/common";
@Injectable({ scope: Scope.REQUEST })
export class UsersService {
constructor(@Inject(REQUEST) private request: Request) {}
}瞬态作用域(TRANSIENT)
每次注入时都创建新实例:
@Injectable({ scope: Scope.TRANSIENT })
export class UsersService {
constructor() {
console.log("UsersService created"); // 每次注入都会打印
}
}提供者别名
使用 useExisting
为现有提供者创建别名:
@Module({
providers: [
UsersService,
{
provide: "AliasedUsersService",
useExisting: UsersService, // 使用现有提供者
},
],
})
export class UsersModule {}使用 useFactory 创建别名
@Module({
providers: [
{
provide: "CONFIG",
useValue: { apiKey: "secret" },
},
{
provide: "APP_CONFIG",
useFactory: (config) => config, // 复用 CONFIG
inject: ["CONFIG"],
},
],
})
export class AppModule {}可选依赖
使用 @Optional()
标记可选的依赖:
import { Injectable, Optional, Inject } from "@nestjs/common";
@Injectable()
export class UsersService {
constructor(
@Optional()
@Inject("HTTP_OPTIONS")
private httpOptions?: any
) {
if (this.httpOptions) {
console.log("HTTP options provided");
}
}
}使用默认值
@Injectable()
export class UsersService {
constructor(
@Inject("HTTP_OPTIONS")
private httpOptions: any = { timeout: 5000 }
) {}
}属性注入
虽然构造函数注入是推荐的方式,但 Nest.js 也支持属性注入:
import { Injectable, Inject } from "@nestjs/common";
@Injectable()
export class UsersService {
@Inject("CONFIG")
private config: any;
// 注意:属性注入在构造函数执行后才完成
getConfig() {
return this.config;
}
}注意:构造函数注入是推荐的方式,更清晰、更易测试。
循环依赖
问题示例
// ❌ 循环依赖
@Injectable()
export class UsersService {
constructor(private postsService: PostsService) {}
}
@Injectable()
export class PostsService {
constructor(private usersService: UsersService) {}
}解决方案:forwardRef
// ✅ 使用 forwardRef 解决循环依赖
@Injectable()
export class UsersService {
constructor(
@Inject(forwardRef(() => PostsService))
private postsService: PostsService
) {}
}
@Injectable()
export class PostsService {
constructor(
@Inject(forwardRef(() => UsersService))
private usersService: UsersService
) {}
}在模块中也使用 forwardRef:
@Module({
imports: [forwardRef(() => PostsModule)],
providers: [UsersService],
})
export class UsersModule {}实际应用:配置服务
创建配置服务
// config/config.service.ts
import { Injectable } from "@nestjs/common";
@Injectable()
export class ConfigService {
private readonly envConfig: { [key: string]: string };
constructor() {
this.envConfig = {
database: process.env.DATABASE_URL,
apiKey: process.env.API_KEY,
port: process.env.PORT || "3000",
};
}
get(key: string): string {
return this.envConfig[key];
}
}全局配置服务
// config/config.module.ts
import { Global, Module } from "@nestjs/common";
import { ConfigService } from "./config.service";
@Global() // 标记为全局模块
@Module({
providers: [ConfigService],
exports: [ConfigService],
})
export class ConfigModule {}在根模块导入
// app.module.ts
import { Module } from "@nestjs/common";
import { ConfigModule } from "./config/config.module";
@Module({
imports: [ConfigModule], // 导入全局模块
})
export class AppModule {}使用配置服务
// users/users.service.ts
import { Injectable } from "@nestjs/common";
import { ConfigService } from "../config/config.service";
@Injectable()
export class UsersService {
constructor(private configService: ConfigService) {}
connectToDatabase() {
const dbUrl = this.configService.get("database");
// 使用数据库 URL
}
}动态模块
创建可配置模块
// logger/logger.module.ts
import { Module, DynamicModule } from "@nestjs/common";
import { LoggerService } from "./logger.service";
export interface LoggerOptions {
level: "debug" | "info" | "warn" | "error";
format: "json" | "text";
}
@Module({})
export class LoggerModule {
static forRoot(options: LoggerOptions): DynamicModule {
return {
module: LoggerModule,
providers: [
{
provide: "LOGGER_OPTIONS",
useValue: options,
},
LoggerService,
],
exports: [LoggerService],
};
}
}使用动态模块
// app.module.ts
import { Module } from "@nestjs/common";
import { LoggerModule } from "./logger/logger.module";
@Module({
imports: [
LoggerModule.forRoot({
level: "info",
format: "json",
}),
],
})
export class AppModule {}最佳实践
- 优先使用构造函数注入:比属性注入更清晰
- 使用接口和抽象类:提高代码的可测试性
- 避免循环依赖:重新设计代码结构,如果必须使用 forwardRef
- 合理使用作用域:大多数情况下单例就足够了
- 使用动态模块:创建可配置的功能模块
- 模块化设计:每个功能模块独立管理自己的提供者
- 使用全局模块谨慎:只在真正需要全局访问时使用
理解依赖注入是掌握 Nest.js 的关键。通过合理使用这些特性,可以构建出更加灵活、可测试和可维护的应用程序。