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

上一篇:NestJS 入门(6):Module 边界与导出 讲了模块怎么接线。

接线之后还有一个问题:

对象已经 new 出来了,但数据库还没连上------这些事该写在构造函数里,还是另找时机?

答案通常是:同步、不依赖别人就绪的事放构造函数;异步初始化放 onModuleInit


1. Nest 创建 Provider 的顺序

启动时大致是:

text 复制代码
1. 解析模块依赖图
2. new XxxService(...)     ← 构造函数(同步)
3. 本模块 Provider 都创建完
4. 调用 onModuleInit()     ← 可以 async;Nest 会 await
5. 应用开始 listen,接收请求

所以:

时机 保证什么 不保证什么
构造函数 依赖已经注入进来(拿到引用) 依赖自己的异步初始化已经完成
onModuleInit 本模块(以及它 import 的模块)里的 Provider 已创建,钩子按依赖顺序调用 整个应用已经对外服务(那是 listen 之后)

「依赖已经注入」≠「依赖已经连好数据库」。

PrismaService 可以在构造函数里被注入,但 $connect() 往往写在它自己的 onModuleInit 里。

onModuleInit 如果抛错,Nest 不会去 listen。这是对的:库都没连上,不该对外接请求。


2. 构造函数适合做什么?

适合 同步、便宜、不碰 I/O 的事:

  • 保存注入进来的依赖
  • 初始化内存结构(Map、数组)
  • 算路径、读开关、准备默认值

没开 Postgres、只用内存演示账号,就可以放构造函数:

typescript 复制代码
@Injectable()
export class AuthService {
  private users: Map<string, User> = new Map();

  constructor(
    private readonly jwtService: JwtService,
    private readonly prisma: PrismaService
  ) {
    if (!usePostgresPersistence()) {
      this.initializeMockUser(); // 同步灌一个演示账号
    }
  }
}

这里没有 await,也不依赖「Prisma 已经连上」。

不要在构造函数里:

typescript 复制代码
constructor(private readonly prisma: PrismaService) {
  // 反例:构造函数不能是 async,await 会被丢掉
  this.prisma.$connect();
  this.prisma.user.findMany(); // 连接可能还没好
}

构造函数不能是 async。你在里面写 async 操作,Nest 不会等它结束再继续启动。


3. OnModuleInit:模块就绪后再干活

实现接口,写一个 onModuleInit

typescript 复制代码
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
  async onModuleInit() {
    if (!usePostgresPersistence()) {
      return;
    }
    await this.$connect();
  }

  async onModuleDestroy() {
    await this.$disconnect();
  }
}

这是最典型的用法:连数据库

Nest 会 await onModuleInit(),连上之前不会认为这个模块初始化完成。

对称的还有 OnModuleDestroy:进程退出时断开连接,避免连接泄漏。

但有一句很容易漏:Nest 默认不听 SIGTERM / SIGINT。不打开关停钩子,$disconnect 根本不会跑:

typescript 复制代码
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.enableShutdownHooks(); // 没有这句,容器停进程时 OnModuleDestroy 不会走
  await app.listen(3000);
}

本地 Ctrl+C、Docker / K8s 发 SIGTERM,都靠这一句把「关停」接到 Nest 的 destroy / shutdown 钩子上。


4. 连上之后再读数据

第二个常见用法:Prisma 已经 $connect 完,再 findMany 灌进内存。

typescript 复制代码
async onModuleInit() {
  const rows = await this.prisma.user.findMany();
  for (const u of rows) {
    this.users.set(u.id, { id: u.id, email: u.email, name: u.name });
  }
}

这和连库是同一类事:

  1. 异步 I/O,构造函数里做不了
  2. 等依赖就绪 :你 importsPrismaModule,Prisma 的 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(),否则进程被杀掉时钩子不会跑。

入门先掌握 OnModuleInit + OnModuleDestroy 就够。

「只连自己的库」用前者;「必须等所有模块都 init 完」再用 OnApplicationBootstrap


7. 一个容易踩的坑:钩子顺序 ≠ 你可以乱用别人的异步结果

Nest 会按模块依赖调用 onModuleInit:你 importsPrismaModule,通常 Prisma 的 onModuleInit 会先跑完,你再跑。

但如果两个业务 Service 互相 forwardRef ,或者你要等的不是 Nest 钩子、而是别的 Service 自己定义的 persistenceReady Promise,那就不要假设「我的 onModuleInit 开始时,对方数据已经灌好」。

稳妥写法是显式等待:

typescript 复制代码
async onModuleInit() {
  await this.projectsService.persistenceReady; // 对方灌库完成的信号
  const fromPg = await loadDocumentsFromPostgres(this.prisma);
  // ...
}

这是循环依赖场景里的补丁,不是常规必写。能单向依赖、让 Nest 按模块顺序 init,更好。


8. 最小示例:连库 + 关停

typescript 复制代码
// prisma.service.ts
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
  async onModuleInit() {
    await this.$connect();
  }

  async onModuleDestroy() {
    await this.$disconnect();
  }
}
typescript 复制代码
// main.ts
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.enableShutdownHooks();
  await app.listen(3000);
}

启动后第一次请求到来前,连接已建立。进程退出时会断开连接。


9. 小结

  • 构造函数:同步装配,不要 await I/O
  • OnModuleInit:模块内 Provider 创建完之后,适合连库、再读数据
  • OnModuleDestroy 要先 enableShutdownHooks(),否则 SIGTERM 时不会走
  • 依赖「已经注入」不等于依赖「已经初始化完」;跨模块异步就绪要靠钩子顺序或显式 Promise
  • 连不上库就让启动失败,不要偷偷改成内存用户

对照本系列:

  1. Controller / Service / Module?
  2. 依赖从哪注入?模块导出了吗?
  3. Guard / Pipe / 统一信封?
  4. 启动时的异步准备,是写在构造函数,还是 onModuleInit?关停钩子打开了吗?

下一篇会讲:ConfigModule 与环境变量 ------密钥、数据库 URL 为什么不要写死在 JwtModule.register({ secret: '...' }) 里。

系列导航

相关推荐
OpenTiny社区2 分钟前
太酷了!装上OpenTiny dsh‑genui这个插件,你的DeepSeek Harness点击就能干活了!
前端·ai编程
ModyQyW9 分钟前
vite-plugin-uni-pages 更新了什么
前端·uni-app
oliver_sys_log13 分钟前
RocketMQ 4.5.1 延迟消息"发送成功但消费不到"排查分析报告
后端·rocketmq
码上观网15 分钟前
VXLAN 中的 ARP 代答:从原理到 SONiC 代码实现
后端
前端大卫28 分钟前
H5 渲染 PDF 并添加高亮的两种方案【附源码】
前端
计算机毕设定制辅导-无忧学长44 分钟前
《基于SpringBoot的马术俱乐部管理系统》
java·spring boot·后端
摇滚侠1 小时前
《SpringBoot 3:入门与应用实战》第 12 章 JDBC 与事务 JDBC 事务管理 阅读笔记 33
spring boot·笔记·后端
学长毕业设计1 小时前
基于SpringBoot的民间艺术传承管理系统(源码+文档+讲解视频)
java·spring boot·后端
小蒜学长1 小时前
基于Springboot+Vue的环保行动志愿者招募系统设计与实现(代码+数据库+LW)
java·数据库·vue.js·spring boot·后端
因_崔斯汀1 小时前
用 AI 生成 Three.js 沉浸式网站,代码与 Skill 均已开源
前端·人工智能