NestJS 把 Angular 的依赖注入(Dependency Injection,DI)思想搬到了后端。第一次接触时,最困惑的是这些概念:Module(模块)、Provider(提供者)、生命周期,以及它们如何串联起来。
Module:应用的积木块
Module 是 NestJS 的组织单元,类似 React 的组件树,但是是平级的。每个应用至少有一个 AppModule,大型应用会拆分成多个 Module。
一个简单的 UserModule
@Module({
imports: [DatabaseModule], // 引入其他 Module
controllers: [UserController], // 注册 HTTP 控制器
providers: [UserService], // 注册服务
exports: [UserService], // 导出给其他 Module 使用
})
export class UserModule {}关键点:
- imports:引入依赖的 Module(如数据库连接)
- controllers:处理 HTTP 请求的入口
- providers:业务逻辑层(Service、Repository 等)
- exports:把自己的 Provider 暴露给其他 Module
Module 之间的依赖关系
graph LR A(AppModule) --> B(UserModule) A --> C(AuthModule) A --> D(DatabaseModule) B --> D C --> B C --> D style A fill:#e1f5ff style B fill:#f0e1ff style C fill:#ffe1f5 style D fill:#e1ffe1
实际场景:
AuthModule需要验证用户,所以 importsUserModuleUserModule和AuthModule都需要数据库,所以 importsDatabaseModuleDatabaseModule是全局依赖,在AppModule中一次性导入
全局 Module
@Global() // 声明为全局 Module
@Module({
providers: [DatabaseService],
exports: [DatabaseService],
})
export class DatabaseModule {}加上 @Global() 后,其他 Module 不需要在 imports 中显式引入,可以直接注入 DatabaseService。
适用场景:数据库连接、日志服务、配置服务等全局依赖。
DI:依赖注入的魔法
依赖注入解决的是”如何获取依赖”的问题。传统方式是 new Service(),DI 方式是”告诉框架你需要什么,框架自动给你”。
最简单的注入
@Injectable()
export class UserService {
getUser(id: number) {
return { id, name: 'Alice' };
}
}
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@Get(':id')
getUser(@Param('id') id: string) {
return this.userService.getUser(+id);
}
}发生了什么:
UserService被@Injectable()标记,告诉 NestJS”我可以被注入”UserModule在providers中注册了UserServiceUserController的constructor参数声明了依赖- NestJS 自动创建
UserService实例并注入
注册 Provider 的三种方式
useClass:注入类实例
最常见的方式,直接写类名:
// 1. 注册:在 Module 中声明
@Module({
providers: [UserService], // 等价于 { provide: UserService, useClass: UserService }
})
export class UserModule {}
// 2. 使用:在 Controller 中注入
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
}
// 测试时替换实现
const module = await Test.createTestingModule({
providers: [
UserService,
{ provide: UserRepository, useValue: mockRepo }, // 用 Mock 替换真实依赖
],
}).compile();useValue:注入静态值
适合注入配置项、常量:
// 1. 注册:注入一个静态值
@Module({
providers: [
{ provide: 'API_URL', useValue: 'https://api.example.com' }
],
})
export class AppModule {}
// 2. 使用:需要 @Inject() 指定 key
@Injectable()
export class UserService {
constructor(@Inject('API_URL') private apiUrl: string) {}
fetchData() {
console.log(`Fetching from ${this.apiUrl}`);
}
}useFactory:动态创建实例
适合异步初始化、根据条件创建:
// 1. 注册:用 factory 函数动态创建
@Module({
providers: [
{
provide: 'DATABASE_CONNECTION',
useFactory: async (config: ConfigService) => {
const connection = await createConnection(config.get('DB_URL'));
return connection;
},
inject: [ConfigService], // factory 函数的依赖
},
ConfigService,
],
})
export class DatabaseModule {}
// 2. 使用:注入 factory 创建的实例
@Injectable()
export class UserRepository {
constructor(@Inject('DATABASE_CONNECTION') private db: any) {}
async findUser(id: number) {
return this.db.query('SELECT * FROM users WHERE id = ?', [id]);
}
}跨 Module 注入
Service 不仅能在本 Module 内使用,还能导出给其他 Module:
// UserModule:提供 UserService
@Module({
providers: [UserService],
exports: [UserService], // 必须 export
})
export class UserModule {}
// AuthModule:使用 UserService
@Module({
imports: [UserModule], // 必须 import
providers: [AuthService],
})
export class AuthModule {}
// AuthService 就能注入 UserService 了
@Injectable()
export class AuthService {
constructor(private readonly userService: UserService) {}
}三步走:
- 提供方
exports - 使用方
imports - Service 中才能
constructor注入
DI 的完整流程
从 Controller 到数据库,依赖关系是这样串起来的:
graph TB A[UserController] -->|注入| B[UserService] B -->|注入| C[UserRepository] C -->|注入| D[DATABASE_CONNECTION] E[UserModule] -.注册.-> A E -.注册.-> B E -.注册.-> C F[DatabaseModule] -.提供.-> D style A fill:#e1f5ff style B fill:#f0e1ff style C fill:#ffe1f5 style D fill:#e1ffe1
作用域:实例的生命周期
默认情况下,Provider 是单例,整个应用生命周期只创建一次。但可以改成每次请求创建、或每次注入创建:
@Injectable({ scope: Scope.DEFAULT }) // 单例(默认)
export class UserService {}
@Injectable({ scope: Scope.REQUEST }) // 每个请求创建新实例
export class RequestContextService {}
@Injectable({ scope: Scope.TRANSIENT }) // 每次注入都创建新实例
export class LoggerService {}REQUEST 作用域的坑:如果一个 Service 依赖了 REQUEST 作用域的 Provider,它自己也会被提升为 REQUEST 作用域,性能会下降。除非需要请求隔离(如请求 ID、用户上下文),否则别用。
生命周期:从启动到关闭
NestJS 应用的生命周期分为三个阶段:初始化 → 运行 → 销毁。每个阶段都有对应的钩子函数。
生命周期钩子
@Injectable()
export class UserService
implements OnModuleInit, OnApplicationBootstrap, OnModuleDestroy {
onModuleInit() {
console.log('1. Module 初始化完成');
}
onApplicationBootstrap() {
console.log('2. 应用启动完成,可以接收请求');
}
onModuleDestroy() {
console.log('3. Module 即将销毁(如关闭数据库连接)');
}
}完整的启动顺序
graph TB A(创建实例) --> B(依赖注入) B --> C(onModuleInit) C --> D(onApplicationBootstrap) D --> E(应用监听端口) E --> F(接收请求) F --> G(收到关闭信号) G --> H(beforeApplicationShutdown) H --> I(onModuleDestroy) I --> J(onApplicationShutdown) style A fill:#e1f5ff style C fill:#f0e1ff style D fill:#ffe1f5 style H fill:#ffe1e1 style I fill:#ffe1e1 style J fill:#ffe1e1
实际例子:数据库连接管理
@Injectable()
export class DatabaseService implements OnModuleInit, OnModuleDestroy {
private connection: any;
async onModuleInit() {
console.log('连接数据库...');
this.connection = await createConnection({
host: 'localhost',
port: 5432,
});
console.log('数据库连接成功');
}
async onModuleDestroy() {
console.log('关闭数据库连接...');
await this.connection.close();
console.log('数据库连接已关闭');
}
query(sql: string) {
return this.connection.query(sql);
}
}钩子执行顺序
假设有三个 Module:DatabaseModule → UserModule → AppModule
启动时(从依赖到根):
1. DatabaseService.onModuleInit()
2. UserService.onModuleInit()
3. AppModule.onModuleInit()
4. DatabaseService.onApplicationBootstrap()
5. UserService.onApplicationBootstrap()
6. AppModule.onApplicationBootstrap()
关闭时(从根到依赖):
1. AppModule.onModuleDestroy()
2. UserService.onModuleDestroy()
3. DatabaseService.onModuleDestroy()
先初始化底层依赖,再初始化上层;关闭时反过来。
常见陷阱
陷阱 1:循环依赖
// ❌ UserService 和 AuthService 互相依赖
@Injectable()
export class UserService {
constructor(private authService: AuthService) {}
}
@Injectable()
export class AuthService {
constructor(private userService: UserService) {}
}解决方法 1:使用 forwardRef()
@Injectable()
export class UserService {
constructor(
@Inject(forwardRef(() => AuthService))
private authService: AuthService
) {}
}解决方法 2(推荐):重新设计依赖关系,提取共享逻辑到第三个 Service。
陷阱 2:忘记 export
// UserModule
@Module({
providers: [UserService],
// 忘记 exports: [UserService]
})
export class UserModule {}
// AuthModule
@Module({
imports: [UserModule],
providers: [AuthService],
})
export class AuthModule {}
// ❌ 报错:Nest can't resolve dependencies of AuthService
@Injectable()
export class AuthService {
constructor(private userService: UserService) {}
}错误原因:UserService 没有被导出,其他 Module 看不到。
陷阱 3:constructor 里做初始化
@Injectable()
export class UserService implements OnModuleInit {
private client: any;
// ❌ 错误:constructor 里做异步操作
constructor(private configService: ConfigService) {
const apiUrl = this.configService.get('API_URL');
this.client = this.initClient(apiUrl); // ConfigService 可能还没准备好
}
// ✅ 正确:用生命周期钩子
async onModuleInit() {
const apiUrl = this.configService.get('API_URL');
this.client = await this.initClient(apiUrl);
}
}规则:constructor 只声明依赖,初始化逻辑放在 onModuleInit 或 onApplicationBootstrap。
陷阱 4:REQUEST 作用域会传播
@Injectable({ scope: Scope.REQUEST })
export class RequestService {}
@Injectable() // 默认是单例
export class UserService {
constructor(private requestService: RequestService) {}
}结果:UserService 会被自动提升为 REQUEST 作用域,每个请求都创建新实例,性能下降。
原因:NestJS 为了保证依赖关系正确,会把依赖链上的所有 Provider 都提升为 REQUEST 作用域。
建议:尽量少用 REQUEST 作用域,或者隔离到单独的 Module。
总结
NestJS 的核心是三个概念:
- Module:组织代码的容器,通过
imports和exports建立依赖关系 - DI:通过构造函数注入依赖,框架自动管理实例创建
- 生命周期:从
onModuleInit到onModuleDestroy,掌握钩子时机做初始化和清理
理解这三者的配合,就能快速上手 NestJS。实际开发中:
- 用 Module 拆分功能模块(User、Auth、Database)
- 用 DI 降低耦合,方便测试
- 用生命周期钩子管理资源(数据库连接、定时任务)
想深入了解装饰器的底层机制,可以看Metadata:装饰器背后的魔法标签。配合装饰器流水线,就能理解 NestJS 的完整请求处理机制。