【NestJS】03 全局模块与生命周期解析
作为 Nest.js 开发者,你一定遇到过这些「小麻烦」:
- 某个常用模块(比如工具类、数据库模块)被 10 个业务模块引用,每个模块都要写一遍
imports: [CommonModule],烦到想复制粘贴; - 应用启动时需要初始化数据库连接、Redis 客户端,却不知道该把代码放在哪里;
- 应用 shutdown 时要关闭资源连接,却找不到「正确的时机」执行清理逻辑。
别慌!Nest.js 的全局模块和生命周期钩子正好解决这些问题。今天咱们就用「实际案例+代码演示」,把这两个知识点讲透~
一、全局模块:一次声明,处处可用
先回忆下 Nest.js 的「常规操作」:如果模块 A 要使用模块 B 的 Provider,必须在模块 A 的@Module里imports: [BModule]。比如下面这个例子:
1. 常规模块引入:重复 Import 的痛点
假设我们有AaaModule(提供AaaService)和BbbModule(需要用AaaService):
import { Module } from '@nestjs/common'import { AaaService } from './aaa.service'
@Module({ providers: [AaaService], exports: [AaaService], // 导出AaaService,供其他模块使用})export class AaaModule {}import { Module } from '@nestjs/common'import { BbbService } from './bbb.service'import { AaaModule } from '../aaa/aaa.module' // 必须Import
@Module({ imports: [AaaModule], // 引入AaaModule才能用AaaService providers: [BbbService],})export class BbbModule {}然后在BbbService里注入AaaService:
import { Injectable } from '@nestjs/common'import { AaaService } from '../aaa/aaa.service'
@Injectable()export class BbbService { constructor(private readonly aaaService: AaaService) {} // 注入成功
doSomething() { return this.aaaService.getInfo() // 使用AaaService的方法 }}问题来了:如果AaaModule要被 10 个模块引用,每个模块都得写一遍imports: [AaaModule]——重复劳动不说,还容易漏写!
2. 全局模块改造:@Global 装饰器拯救你
Nest.js 提供了@Global()装饰器,给模块加上这个装饰器,它的导出 Provider 会变成「全局可用」,不需要再在其他模块里import。
改造AaaModule:
import { Module, Global } from '@nestjs/common' // 引入Global装饰器import { AaaService } from './aaa.service'
@Global() // 声明为全局模块@Module({ providers: [AaaService], exports: [AaaService], // 依然需要导出,否则全局也拿不到})export class AaaModule {}然后修改BbbModule:删掉 AaaModule 的 Import
import { Module } from '@nestjs/common'import { BbbService } from './bbb.service'
@Module({ providers: [BbbService], // 不用Import AaaModule!})export class BbbModule {}再跑服务验证:npm run start:dev,你会发现BbbService依然能正常注入AaaService——全局模块生效了!
3. 全局模块的「使用准则」:别滥用!
全局模块虽然方便,但一定要克制:
- 全局模块的 Provider「来源不明确」,新同事接手代码时,可能不知道某个 Service 是从哪个模块来的;
- 全局模块会增加依赖耦合,比如修改 AaaModule 的导出,可能影响所有用到它的模块。
建议:只给「通用基础模块」用全局,比如工具类模块、数据库连接模块(如 TypeORM/Mongoose 模块),业务模块尽量不用。
二、生命周期钩子:掌控应用的「启停逻辑」
Nest.js 的应用启动和销毁过程,就像一场「有序的舞台剧」:
- 启动时:先解析模块依赖 → 初始化 Provider 和 Controller→ 监听端口;
- 销毁时:先清理资源 → 关闭端口 → 退出进程。
Nest.js 暴露了7 个生命周期钩子,让你能在「特定阶段」执行自定义逻辑(比如初始化数据库、关闭连接)。
1. 先搞懂:生命周期的「执行顺序」
先看启动阶段的钩子(按执行顺序):
onModuleInit:模块内的Controller→Provider→Module依次执行(递归解析所有模块);onApplicationBootstrap:所有模块初始化完成后,监听端口前执行(Controller→Provider→Module)。
再看销毁阶段的钩子(按执行顺序):
onModuleDestroy:模块内的Controller→Provider→Module依次执行;beforeApplicationShutdown:收到系统信号(如SIGTERM)时执行,能拿到信号类型;onApplicationShutdown:端口关闭后执行,最后清理资源。
2. 实战:用钩子打印「执行顺序日志」
我们创建CccModule和DddModule,在 Controller、Service、Module 里分别实现生命周期钩子,看日志输出:
步骤 1:创建模块并实现钩子
以CccModule为例(DddModule同理):
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'
@Injectable()export class CccService implements OnModuleInit, OnModuleDestroy { onModuleInit() { console.log('🔧 CccService → onModuleInit') }
onModuleDestroy() { console.log('🔧 CccService → onModuleDestroy') }
findAll() { return ['ccc-data'] }}import { Controller, Get, OnModuleInit, OnModuleDestroy } from '@nestjs/common'import { CccService } from './ccc.service'
@Controller('ccc')export class CccController implements OnModuleInit, OnModuleDestroy { constructor(private readonly cccService: CccService) {}
onModuleInit() { console.log('🎮 CccController → onModuleInit') }
onModuleDestroy() { console.log('🎮 CccController → onModuleDestroy') }
@Get() findAll() { return this.cccService.findAll() }}import { Module, OnModuleInit, OnModuleDestroy, OnApplicationBootstrap, BeforeApplicationShutdown, OnApplicationShutdown,} from '@nestjs/common'import { ModuleRef } from '@nestjs/core'import { CccService } from './ccc.service'import { CccController } from './ccc.controller'
@Module({ controllers: [CccController], providers: [CccService],})export class CccModule implements OnModuleInit, OnModuleDestroy, OnApplicationBootstrap, BeforeApplicationShutdown, OnApplicationShutdown{ constructor(private readonly moduleRef: ModuleRef) {} // 注入ModuleRef,用于获取Provider
// 启动阶段钩子 onModuleInit() { console.log('📦 CccModule → onModuleInit') }
onApplicationBootstrap() { console.log('📦 CccModule → onApplicationBootstrap') }
// 销毁阶段钩子 onModuleDestroy() { console.log('📦 CccModule → onModuleDestroy') }
beforeApplicationShutdown(signal: string) { console.log(`📦 CccModule → beforeApplicationShutdown(信号:${signal})`) }
onApplicationShutdown() { const cccService = this.moduleRef.get<CccService>(CccService) // 从模块中获取Service console.log( `📦 CccModule → onApplicationShutdown(Service数据:${cccService.findAll()})` ) }}步骤 2:验证启动阶段的钩子顺序
跑服务npm run start:dev,你会看到日志输出:
🎮 CccController → onModuleInit🔧 CccService → onModuleInit📦 CccModule → onModuleInit🎮 DddController → onModuleInit🔧 DddService → onModuleInit📦 DddModule → onModuleInit🎮 CccController → onApplicationBootstrap🔧 CccService → onApplicationBootstrap📦 CccModule → onApplicationBootstrap🎮 DddController → onApplicationBootstrap🔧 DddService → onApplicationBootstrap📦 DddModule → onApplicationBootstrap[Nest] 12345 - 09/20/2024, 10:00:00 AM LOG [NestFactory] Starting Nest application...[Nest] 12345 - 09/20/2024, 10:00:01 AM LOG [NestApplication] Nest application successfully started结论:启动阶段是「Controller→Service→Module」的顺序,递归执行所有模块。
步骤 3:验证销毁阶段的钩子顺序
我们在main.ts里加一段代码,3 秒后触发销毁:
import { NestFactory } from '@nestjs/core'import { AppModule } from './app.module'
async function bootstrap() { const app = await NestFactory.create(AppModule) await app.listen(3000)
// 3秒后触发销毁逻辑 setTimeout(async () => { await app.close() // 触发销毁钩子,但不会退出进程 }, 3000)}bootstrap()重新跑服务,3 秒后会看到销毁日志:
🎮 CccController → onModuleDestroy🔧 CccService → onModuleDestroy📦 CccModule → onModuleDestroy🎮 DddController → onModuleDestroy🔧 DddService → onModuleDestroy📦 DddModule → onModuleDestroy📦 CccModule → beforeApplicationShutdown(信号:SIGTERM)📦 DddModule → beforeApplicationShutdown(信号:SIGTERM)[Nest] 12345 - 09/20/2024, 10:00:03 AM LOG [NestApplication] Nest application is closing...📦 CccModule → onApplicationShutdown(Service数据:ccc-data)📦 DddModule → onApplicationShutdown(Service数据:ddd-data)[Nest] 12345 - 09/20/2024, 10:00:03 AM LOG [NestApplication] Nest application closed successfully3. 生命周期钩子的「实际用途」
生命周期钩子的核心价值是「在特定阶段处理资源」,比如:
onModuleInit:初始化数据库连接、Redis 客户端;onApplicationBootstrap:预热缓存、加载配置文件;onApplicationShutdown:关闭数据库连接、清理临时文件。
举个实际例子:用onApplicationShutdown关闭 TypeORM 连接:
import { Module, OnApplicationShutdown } from '@nestjs/common'import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm'import { ModuleRef } from '@nestjs/core'import { Connection } from 'typeorm'
@Module({ imports: [ TypeOrmModule.forRoot({ /* TypeORM配置 */ } as TypeOrmModuleOptions), ],})export class DatabaseModule implements OnApplicationShutdown { constructor(private moduleRef: ModuleRef) {}
onApplicationShutdown() { const connection = this.moduleRef.get<Connection>(Connection) // 获取TypeORM连接 if (connection.isConnected) { await connection.close() // 关闭连接 console.log('💾 TypeORM连接已关闭') } }}三、总结:关键知识点速记
- 全局模块:用
@Global()装饰器,一次声明处处可用,适用于通用基础模块; - 生命周期钩子:
- 启动阶段:
onModuleInit(模块内初始化)→onApplicationBootstrap(全部模块初始化); - 销毁阶段:
onModuleDestroy(模块内清理)→beforeApplicationShutdown(收到信号)→onApplicationShutdown(最后清理);
- 启动阶段:
- 最佳实践:
- 全局模块别滥用,业务模块尽量用普通 Import;
- 生命周期钩子用于「资源初始化/清理」,比如数据库连接、缓存预热。
参考资料
如果这篇文章帮你解决了「重复 Import」或「资源管理」的问题,点个赞再走~ 你的支持是我写下去的动力!🚀
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!