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: '...' }) 里。

系列导航

相关推荐
小强库计算机毕业设计1 小时前
基于 Spring Boot + Vue3 的校园管理系统
java·spring boot·后端·项目实战·管理系统·技术分享·校园管理系统实战
深念Y1 小时前
基于 NapCat 与本地 RAG 的群聊 AI 机器人方案(ARM64 部署)
人工智能·ai·机器人·node.js·自动化·情感陪伴·bot
工业HMI实战笔记1 小时前
解放双手,声控未来:抗噪语音交互如何革新嘈杂车间的HMI操作体验
人工智能·学习·自动化·制造
知识分享小能手1 小时前
线性代数学习教程,从入门到精通,相似矩阵及二次型 — 知识点详解(9)
学习·线性代数·机器学习
canonical_entropy1 小时前
关于《Mission Driver:Loop Engineering 的一种通用参考实现》的补充说明
后端·agent·ai编程
__zRainy__2 小时前
Node系列 · Node基础:Node.js 概述
后端·node.js
cindershade2 小时前
用原生 HTML5 视频与 Canvas 实现浏览器端视频抽帧封面
前端
笨鸟先飞,勤能补拙2 小时前
Web 服务器与计算机网络全景原理
运维·服务器·前端·网络·人工智能·计算机网络·web安全
NutShell Wang2 小时前
Wails v3 Beta 实战:用显式对象模型重写你的第一个 Go 桌面应用
前端·人工智能·go·vibe coding