上一篇: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 });
}
}
这和连库是同一类事:
- 异步 I/O,构造函数里做不了
- 等依赖就绪 :你
imports了PrismaModule,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:你 imports 了 PrismaModule,通常 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. 小结
- 构造函数:同步装配,不要
awaitI/O OnModuleInit:模块内 Provider 创建完之后,适合连库、再读数据OnModuleDestroy要先enableShutdownHooks(),否则 SIGTERM 时不会走- 依赖「已经注入」不等于依赖「已经初始化完」;跨模块异步就绪要靠钩子顺序或显式 Promise
- 连不上库就让启动失败,不要偷偷改成内存用户
对照本系列:
- Controller / Service / Module?
- 依赖从哪注入?模块导出了吗?
- Guard / Pipe / 统一信封?
- 启动时的异步准备,是写在构造函数,还是
onModuleInit?关停钩子打开了吗?
下一篇会讲:ConfigModule 与环境变量 ------密钥、数据库 URL 为什么不要写死在 JwtModule.register({ secret: '...' }) 里。