公司动态

NestJS 入门(7):生命周期钩子——构造函数和 `OnModuleInit` 差在哪?

📅 2026/8/15 14:48:09
NestJS 入门(7):生命周期钩子——构造函数和 `OnModuleInit` 差在哪?
上一篇NestJS 入门6Module 边界与导出 讲了模块怎么接线。接线之后还有一个问题对象已经new出来了但数据库还没连上——这些事该写在构造函数里还是另找时机答案通常是同步、不依赖别人就绪的事放构造函数异步初始化放onModuleInit。1. Nest 创建 Provider 的顺序启动时大致是1. 解析模块依赖图 2. new XxxService(...) ← 构造函数同步 3. 本模块 Provider 都创建完 4. 调用 onModuleInit() ← 可以 asyncNest 会 await 5. 应用开始 listen接收请求所以时机保证什么不保证什么构造函数依赖已经注入进来拿到引用依赖自己的异步初始化已经完成onModuleInit本模块以及它 import 的模块里的 Provider 已创建钩子按依赖顺序调用整个应用已经对外服务那是 listen 之后「依赖已经注入」≠「依赖已经连好数据库」。PrismaService可以在构造函数里被注入但$connect()往往写在它自己的onModuleInit里。onModuleInit如果抛错Nest不会去 listen。这是对的库都没连上不该对外接请求。2. 构造函数适合做什么适合同步、便宜、不碰 I/O的事保存注入进来的依赖初始化内存结构Map、数组算路径、读开关、准备默认值没开 Postgres、只用内存演示账号就可以放构造函数Injectable()exportclassAuthService{privateusers:Mapstring,UsernewMap();constructor(privatereadonlyjwtService:JwtService,privatereadonlyprisma:PrismaService){if(!usePostgresPersistence()){this.initializeMockUser();// 同步灌一个演示账号}}}这里没有await也不依赖「Prisma 已经连上」。不要在构造函数里constructor(privatereadonlyprisma:PrismaService){// 反例构造函数不能是 asyncawait 会被丢掉this.prisma.$connect();this.prisma.user.findMany();// 连接可能还没好}构造函数不能是async。你在里面写async操作Nest 不会等它结束再继续启动。3.OnModuleInit模块就绪后再干活实现接口写一个onModuleInitimport{Injectable,OnModuleInit,OnModuleDestroy}fromnestjs/common;Injectable()exportclassPrismaServiceextendsPrismaClientimplementsOnModuleInit,OnModuleDestroy{asynconModuleInit(){if(!usePostgresPersistence()){return;}awaitthis.$connect();}asynconModuleDestroy(){awaitthis.$disconnect();}}这是最典型的用法连数据库。Nest 会await onModuleInit()连上之前不会认为这个模块初始化完成。对称的还有OnModuleDestroy进程退出时断开连接避免连接泄漏。但有一句很容易漏Nest默认不听SIGTERM/SIGINT。不打开关停钩子$disconnect根本不会跑asyncfunctionbootstrap(){constappawaitNestFactory.create(AppModule);app.enableShutdownHooks();// 没有这句容器停进程时 OnModuleDestroy 不会走awaitapp.listen(3000);}本地 CtrlC、Docker / K8s 发 SIGTERM都靠这一句把「关停」接到 Nest 的 destroy / shutdown 钩子上。4. 连上之后再读数据第二个常见用法Prisma 已经$connect完再findMany灌进内存。asynconModuleInit(){constrowsawaitthis.prisma.user.findMany();for(constuofrows){this.users.set(u.id,{id:u.id,email:u.email,name:u.name});}}这和连库是同一类事异步 I/O构造函数里做不了等依赖就绪你imports了PrismaModulePrisma 的onModuleInit会先跑完你再跑读失败就让它抛错。不要catch之后偷偷改成内存用户——库都没连上进程不该假装还能登录。5. 对照表放哪边事情构造函数onModuleInit保存注入进来的依赖✅不必new Map()/ 默认配置✅可以但没必要等await prisma.$connect()❌✅await prisma.user.findMany()❌✅从 JSON / 磁盘读会话文件同步读勉强可以✅ 更合适可 async注册定时任务、订阅消息队列❌✅或OnApplicationBootstrap口诀构造函数把零件接到手上。onModuleInit零件齐了再连电源、读数据。6. 其它钩子知道即可钩子何时调用典型用途OnModuleInit该模块依赖初始化完连库、恢复缓存OnApplicationBootstrap所有模块的onModuleInit都结束需要「全应用就绪」才启动的事如开始消费队列OnModuleDestroy模块拆掉时$disconnect、取消订阅OnApplicationShutdown应用关停信号打日志、刷盘、通知健康检查下线后两个都要先app.enableShutdownHooks()否则进程被杀掉时钩子不会跑。入门先掌握OnModuleInitOnModuleDestroy就够。「只连自己的库」用前者「必须等所有模块都 init 完」再用OnApplicationBootstrap。7. 一个容易踩的坑钩子顺序 ≠ 你可以乱用别人的异步结果Nest 会按模块依赖调用onModuleInit你imports了PrismaModule通常 Prisma 的onModuleInit会先跑完你再跑。但如果两个业务 Service互相forwardRef或者你要等的不是 Nest 钩子、而是别的 Service 自己定义的persistenceReadyPromise那就不要假设「我的onModuleInit开始时对方数据已经灌好」。稳妥写法是显式等待asynconModuleInit(){awaitthis.projectsService.persistenceReady;// 对方灌库完成的信号constfromPgawaitloadDocumentsFromPostgres(this.prisma);// ...}这是循环依赖场景里的补丁不是常规必写。能单向依赖、让 Nest 按模块顺序 init更好。8. 最小示例连库 关停// prisma.service.tsInjectable()exportclassPrismaServiceextendsPrismaClientimplementsOnModuleInit,OnModuleDestroy{asynconModuleInit(){awaitthis.$connect();}asynconModuleDestroy(){awaitthis.$disconnect();}}// main.tsasyncfunctionbootstrap(){constappawaitNestFactory.create(AppModule);app.enableShutdownHooks();awaitapp.listen(3000);}启动后第一次请求到来前连接已建立。进程退出时会断开连接。9. 小结构造函数同步装配不要awaitI/OOnModuleInit模块内 Provider 创建完之后适合连库、再读数据OnModuleDestroy要先enableShutdownHooks()否则 SIGTERM 时不会走依赖「已经注入」不等于依赖「已经初始化完」跨模块异步就绪要靠钩子顺序或显式 Promise连不上库就让启动失败不要偷偷改成内存用户对照本系列Controller / Service / Module依赖从哪注入模块导出了吗Guard / Pipe / 统一信封启动时的异步准备是写在构造函数还是onModuleInit关停钩子打开了吗下一篇会讲ConfigModule 与环境变量——密钥、数据库 URL 为什么不要写死在JwtModule.register({ secret: ... })里。系列导航上一篇NestJS 入门6Module 边界与导出第五篇NestJS 入门5Pipe 与 DTO 校验第四篇NestJS 入门4统一响应与异常处理第三篇NestJS 入门3Guard 如何挡住未登录请求第二篇NestJS 入门2依赖注入到底解决了什么问题第一篇NestJS 入门1先搞懂 Module、Controller、Service