NestJS 的生命周期钩子以及它们在不同阶段的都适合做什么?

文章开始之前分享两个开源项目,会一直维护的,欢迎 star,如果你感兴趣或者想参与学习,可以加我微信 yunmz777,最近也在找工作 ing,欢迎内推......

浪费你两秒时间,我们正文开始

Nest 应用程序以及每个应用程序元素都有一个由 Nest 管理的生命周期。Nest 提供了生命周期钩子,这些钩子能够让你观察到关键的生命周期事件,并且在这些事件发生时能够采取行动(在你的模块、提供者或控制器上运行注册的代码)。

生命周期顺序

下图描述了从应用程序启动到节点进程退出的关键应用生命周期事件的序列。我们可以将整个生命周期划分为三个阶段:初始化、运行和终止。利用这个生命周期,你可以为模块和服务的适当初始化进行计划,管理活跃的连接,并在应用程序接收到终止信号时优雅地关闭你的应用程序。

生命周期事件

生命周期事件在应用程序启动和关闭期间发生。Nest 在以下每个生命周期事件中调用在模块、提供者和控制器上注册的生命周期钩子方法(首先需要启用关闭钩子,如下所述)。如上图所示,Nest 还调用适当的底层方法开始监听连接,并停止监听连接。

在下表中,只有在你显式调用 app.close() 或者进程接收到特殊的系统信号(例如 SIGTERM)并且你已经在应用程序启动时正确调用了 enableShutdownHooksonModuleDestroybeforeApplicationShutdownonApplicationShutdown 这些钩子才会被触发。

生命周期钩子方法 生命周期事件触发钩子方法调用
onModuleInit() 在模块的所有提供者(包括服务、工厂、帮助器等)都已经实例化,并且所有相关依赖项都已经被注入后(模块完全准备好之前进行必要的初始化操作)。
onApplicationBootstrap() 在所有模块初始化后但在侦听连接之前调用。
onModuleDestroy() 在收到终止信号(例如 SIGTERM)后调用
beforeApplicationShutdown() 主动调用 app.close() 或应用程序接收到如 SIGTERM 或 SIGINT 等系统信号时触发,前提是已通过 enableShutdownHooks() 方法启用了关闭钩子。
onApplicationShutdown() 在应用程序即将完全关闭时被调用。这个钩子允许你在应用程序关闭所有连接和服务之后,但在进程退出前,执行必要的最终清理操作。

这些钩子确保开发者可以在关键时刻控制应用行为,优化资源管理,和平滑地进行服务的启动与停止。通过这些生命周期钩子的合理使用,可以大幅提高应用的稳定性和响应能力。

onModuleInit()

onModuleInit() 是一个在 NestJS 模块初始化时执行的生命周期钩子,通常用于执行模块级的设置或初始化任务,例如建立数据库连接、加载配置文件或启动后台任务。这里提供一个详细的使用案例,展示如何在一个模块中使用 onModuleInit() 钩子来初始化数据库连接。

假设我们正在开发一个使用 MongoDB 的博客应用。我们需要在应用启动时确保数据库连接已建立,并且相关的数据模型已经同步。

首先我们创建一个服务来管理数据库操作,这个服务奖负责创建和维护数据库的连接:

ts 复制代码
// src/mongo/mongo.service.ts
import { Injectable, OnModuleInit } from "@nestjs/common";
import { MongoClient } from "mongodb";

@Injectable()
export class MongoService implements OnModuleInit {
  private client: MongoClient;

  constructor() {
    this.client = new MongoClient(process.env.MONGO_URI);
  }

  async onModuleInit() {
    try {
      await this.client.connect();
      console.log("MongoDB 连接成功");
    } catch (error) {
      console.error("MongoDB 连接失败:", error);
    }
  }

  getDb() {
    return this.client.db("blog");
  }

  async closeConnection() {
    await this.client.close();
  }
}

在这个服务中,我们实现了 OnModuleInit 接口,并在 onModuleInit() 方法中建立了 MongoDB 的连接。这确保了在模块的其他部分使用数据库前,连接已经建立。

接下来,我们需要确保这个服务被正确注册到相关模块中。

ts 复制代码
// src/mongo/mongo.module.ts
import { Module } from "@nestjs/common";
import { MongoService } from "./mongo.service";

@Module({
  providers: [MongoService],
  exports: [MongoService],
})
export class MongoModule {}

现在 MongoService 已经准备好,其他服务或控制器可以通过依赖注入来使用它。

ts 复制代码
// src/blog/blog.service.ts
import { Injectable } from "@nestjs/common";
import { MongoService } from "../mongo/mongo.service";

@Injectable()
export class BlogService {
  constructor(private mongoService: MongoService) {}

  async getPosts() {
    const db = this.mongoService.getDb();
    return db.collection("posts").find().toArray();
  }
}

在 BlogService 中,我们注入了 MongoService 并使用它来获取博客帖子。

通过在 MongoService 中使用 onModuleInit() 钩子来确保数据库连接在服务开始前已经建立,我们可以避免在应用运行中遇到未初始化连接的问题。

onApplicationBootstrap()

onApplicationBootstrap() 这个钩子在所有模块的 onModuleInit() 方法执行完毕后调用,适合用于需要跨模块或全局执行的初始化任务。这包括例如启动日志服务、加载全局配置、或者初始化与外部系统的连接等。

假设我们的电子商务平台需要确保与支付网关的连接在应用启动时建立,并验证其他外部依赖服务的可用性。此外,希望在每次应用启动时,系统能自动通知管理员。

首先,我们创建一个服务来管理与支付网关的交互:

ts 复制代码
// src/payment/payment.service.ts
import { Injectable, OnApplicationBootstrap } from "@nestjs/common";
import { HttpClient } from "@nestjs/common/http";

@Injectable()
export class PaymentService implements OnApplicationBootstrap {
  constructor(private http: HttpClient) {}

  async onApplicationBootstrap() {
    console.log("正在建立到支付网关的连接...");
    try {
      const response = await this.http
        .get("https://api.paymentgateway.com/health")
        .toPromise();
      if (response.status === 200) {
        console.log("成功连接到支付网关。");
      } else {
        console.error("连接到支付网关失败。");
      }
    } catch (error) {
      console.error("连接到支付网关时出现错误:", error.message);
    }
  }
}

接下来,我们创建一个服务来检查所有关键外部服务的可用性。

ts 复制代码
// src/services/external-check.service.ts
import { Injectable, OnApplicationBootstrap } from "@nestjs/common";
import { HttpClient } from "@nestjs/common/http";

@Injectable()
export class ExternalCheckService implements OnApplicationBootstrap {
  constructor(private http: HttpClient) {}

  async onApplicationBootstrap() {
    console.log("检查外部服务健康状态...");
    const services = [
      { name: "库存服务", url: "https://api.inventoryservice.com/health" },
      { name: "物流服务", url: "https://api.shippingservice.com/health" },
    ];

    for (let service of services) {
      try {
        const response = await this.http.get(service.url).toPromise();
        if (response.status === 200) {
          console.log(`${service.name}运行正常。`);
        } else {
          console.error(`${service.name}响应失败。`);
        }
      } catch (error) {
        console.error(`连接到${service.name}时出现错误:`, error.message);
      }
    }
  }
}

我们还需要一个服务来在启动时发送通知邮件给管理员。

ts 复制代码
// src/notification/startup-notification.service.ts
import { Injectable, OnApplicationBootstrap } from "@nestjs/common";
import { MailerService } from "@nestjs-modules/mailer";

@Injectable()
export class StartupNotificationService implements OnApplicationBootstrap {
  constructor(private mailerService: MailerService) {}

  async onApplicationBootstrap() {
    console.log("向管理员发送启动通知...");
    try {
      await this.mailerService.sendMail({
        to: "admin@example.com",
        subject: "应用启动通知",
        text: "应用已成功启动。",
      });
      console.log("启动通知发送成功。");
    } catch (error) {
      console.error("发送启动通知失败:", error.message);
    }
  }
}

最后,我们需要将这些服务注册到主模块中。

ts 复制代码
// src/app.module.ts
import { Module, HttpModule } from "@nestjs/common";
import { MailerModule } from "@nestjs-modules/mailer";
import { PaymentService } from "./payment/payment.service";
import { ExternalCheckService } from "./services/external-check.service";
import { StartupNotificationService } from "./notification/startup-notification.service";

@Module({
  imports: [
    HttpModule,
    MailerModule.forRoot({
      // 邮件服务配置
    }),
  ],
  providers: [PaymentService, ExternalCheckService, StartupNotificationService],
})
export class AppModule {}

通过使用 onApplicationBootstrap() 钩子,我们在应用程序完全启动并准备接受请求之前,确保了与支付网关的连接、外部服务的可用性检查,并向管理员发送了启动通知。这种方法确保应用在提供服务前处于完全准备状态,增强了系统的可靠性和管理员的信心。

onApplicationBootstrap()

在 NestJS 中,enableShutdownHooks() 方法是一个关键的功能,用于优雅地处理应用程序的关闭过程。此功能确保在应用程序接收到终止信号(如 SIGINT、SIGTERM)时,可以执行自定义的清理操作,例如关闭数据库连接、停止正在执行的后台任务、发送通知等。

默认情况下,当 Node.js 应用接收到终止信号时,它会立即尝试退出。这种突然的退出可能导致正在进行的操作被中断,例如:

  1. 数据库事务可能未正确完成。

  2. 文件可能未完全写入。

  3. 必要的清理逻辑未被执行。

  4. 通过使用 enableShutdownHooks(),NestJS 可以捕获这些信号,并延迟应用程序的关闭,直到所有注册的清理逻辑都执行完毕。

enableShutdownHooks() 工作机制如下:

  1. 捕获信号:监听 Node.js 进程的终止信号,如 SIGINT(通常由 Ctrl+C 触发)和 SIGTERM(通常由系统的服务管理器发送,如 Kubernetes 在停止 Pod 时发送)。

  2. 执行生命周期钩子:在捕获到信号后,执行 onModuleDestroy() 和 beforeApplicationShutdown() 钩子。这些钩子允许你执行必要的资源释放和清理操作。

  3. 延迟退出:在所有注册的钩子函数执行完毕后,NestJS 会调用 process.exit() 来正式结束进程。这确保了所有的清理操作都得到妥善处理。

在 NestJs 中,只需要添加这一行代码即可:

ts 复制代码
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.enableShutdownHooks(); // 启用终止钩子

  await app.listen(3000);
}
bootstrap();

接下来我们来讲解一下需要用到 enableShutdownHooks 才能使用的这几个 hooks。

OnModuleDestroy()

OnModuleDestroy 是 NestJS 中的一个生命周期钩子接口,它提供了在模块销毁过程中执行清理逻辑的机会。这个钩子在 NestJS 模块生命周期的末尾调用,通常用于释放资源、关闭连接、清理订阅等任务,以确保应用程序优雅地关闭。

OnModuleDestroy 钩子在以下情况下被触发:

  1. 应用程序关闭:当通过调用 app.close() 主动关闭应用程序时,或者通过 enableShutdownHooks() 启用了系统关闭信号的监听(如 SIGINT 或 SIGTERM),并接收到这些信号时。

  2. 模块动态移除:如果你的应用动态移除了某个模块(这在微服务或动态模块加载的场景中可能会用到),那么该模块的 OnModuleDestroy 钩子将被调用。

OnModuleDestroy 钩子常见的用例包括:

  1. 关闭数据库连接:如上例所示,确保所有数据库连接在模块销毁时被关闭,防止连接泄漏。

  2. 停止后台任务:如果你的模块启动了定时任务或后台进程,可以在这里停止这些任务。

  3. 清理事件监听器:注销事件订阅和监听器,避免内存泄漏。

  4. 释放外部资源:例如关闭文件句柄、网络套接字等。

OnModuleDestroy 提供了一个在模块生命周期结束时进行资源清理和执行其他必要清理操作的机会,是确保资源得到妥善管理和应用优雅关闭的关键。通过实现这一接口,开发者可以有效防止资源泄漏,确保应用组件在销毁时不会留下后患。

beforeApplicationShutdown()

beforeApplicationShutdown() 是 NestJS 中的一个生命周期钩子,用于在应用程序完全关闭之前执行必要的清理和释放操作。这个钩子提供了一个在应用程序关闭流程中,但在所有服务和控制器被销毁之前,执行自定义逻辑的机会。这允许开发者确保所有的资源都得到妥善处理,如保存关键数据到数据库、关闭外部服务的连接、或者清理临时文件等。

beforeApplicationShutdown() 钩子可以在几种情况下被触发:

  1. 主动关闭:当应用程序通过 app.close() 方法被主动请求关闭时。

  2. 接收终止信号:当应用程序通过 enableShutdownHooks() 方法启用监听并接收到如 SIGINT(通常由用户发起的中断,如 Ctrl+C)或 SIGTERM(由系统发送的终止信号)时。

这个钩子的执行点位于 Nest 完成所有控制器和服务的清理之前,实际 Node.js 进程退出之前,因此是处理最后一步清理工作的理想位置。

beforeApplicationShutdown() 钩子的常见用例包括:

  1. 保存状态:确保应用程序的当前状态或未完成的数据被安全保存,以防止数据丢失。

  2. 释放资源:关闭数据库连接、网络连接、释放文件句柄等,确保资源被正确释放。

  3. 清理任务:完成或取消正在执行的定时任务、后台进程等。

  4. 发送通知:向其他系统或服务发送关闭通知,如通过 API 通知依赖的外部系统应用即将关闭。

beforeApplicationShutdown() 钩子是确保应用程序在关闭过程中能够执行必要清理和资源释放的关键。正确使用这个钩子可以大大增强应用的健壮性和可靠性,防止资源泄漏,并确保应用在重启或关闭时表现出最佳状态。

onApplicationShutdown()

onApplicationShutdown() 在应用程序即将完全关闭时被调用,它允许你在应用程序关闭所有连接和服务之后,但在进程退出前,执行必要的最终清理操作。该钩子提供了在应用程序关闭流程的最后阶段执行操作的机会,确保所有的清理工作均已完成。

onApplicationShutdown() 钩子的常见用途包括:

  1. 资源释放:关闭打开的文件、数据库连接和网络连接等,确保所有外部资源都被适当关闭。

  2. 停止后台进程:确保后台进程如定时任务、长轮询操作等被正确停止。

  3. 清理缓存:如果有临时或持久化缓存机制,确保缓存数据被保存或释放。

  4. 记录日志:记录应用关闭的相关信息,有助于事后分析应用的关闭过程和状态。

onApplicationShutdown() 钩子是处理应用程序关闭过程中的最后一步清理工作的关键机制。它允许开发者在应用程序生命周期的最后阶段确保资源得到妥善处理,从而提高应用的可维护性和稳定性,避免可能的资源泄漏或其他关闭相关的问题。通过合理使用这一钩子,可以使得应用的关闭过程既平滑又可控。

OnModuleDestroy 和 beforeApplicationShutdown 的区别

OnModuleDestroy 和 beforeApplicationShutdown 两个生命周期钩子,它们在应用程序的不同阶段被触发,以处理不同的清理和资源管理任务。尽管它们的目的都是为了在应用程序关闭前执行清理工作,但它们的应用场景和触发时机有明显的区别。

OnModuleDestroy 定义与触发时机:

  1. OnModuleDestroy 钩子在每个模块被销毁时触发,无论是由于应用程序关闭还是因为模块被动态移除。

  2. 这个钩子主要用于模块级别的资源清理,如关闭模块内部使用的数据库连接、停止模块内启动的定时任务等。

它主要适用于那些需要清理特定于模块的资源的场景,例如,当一个模块拥有独立于其他模块管理的资源(如特定服务的连接)时。

beforeApplicationShutdown 定义与触发时机:

  1. beforeApplicationShutdown 钩子在整个应用程序即将关闭前触发,但在模块和服务被销毁之前。

  2. 这个钩子用于在应用程序级别进行清理,比如当接收到停止信号时确保所有数据都已经保存到数据库,或者发送一个关闭通知到外部监控服务。

它主要适用于需要在整个应用程序关闭前执行的清理任务,这些任务通常涉及多个模块或跨服务的操作。

它们两者的区别主要有以下几个方面:

  1. 触发范围:

    • OnModuleDestroy 是模块级别的,针对单独模块的销毁进行响应。

    • beforeApplicationShutdown 是应用程序级别的,涉及整个应用即将关闭的预处理。

  2. 目的和关注点:

    • OnModuleDestroy 关注于模块内部的资源和服务的清理。

    • beforeApplicationShutdown 更关注于应用程序范围内的整体状态保存和外部通知。

  3. 触发时机:

    • OnModuleDestroy 可以在模块动态移除时或应用关闭时触发。

    • beforeApplicationShutdown 仅在应用程序关闭流程中,接收到关闭信号后触发。

这两个钩子的合理使用可以大大增强应用的稳健性和优雅关闭的能力,确保资源的正确释放和数据的安全保存。在设计应用时,开发者应根据具体的资源管理和清理需求选择适当的钩子进行实现。

举个 🌰

接下来我们将编写一个完整的 demo 来验证一下它们的执行顺序是怎么样的,如下代码所示:

ts 复制代码
import {
  Injectable,
  OnModuleInit,
  OnApplicationBootstrap,
  OnModuleDestroy,
  BeforeApplicationShutdown,
  OnApplicationShutdown,
} from "@nestjs/common";

@Injectable()
export class LifetimeService implements OnModuleInit {
  // 当模块初始化完成时调用此方法
  onModuleInit(): void {
    console.log("生命周期服务初始化,onModuleInit。"); // 使用中文日志输出,标示服务初始化
  }
}

@Injectable()
export class LifetimeService1 implements OnApplicationBootstrap {
  // 当应用程序引导完成时调用此方法
  onApplicationBootstrap(): void {
    console.log("生命周期服务1启动,onApplicationBootstrap。");
  }
}

@Injectable()
export class LifetimeService2 implements OnModuleDestroy {
  onModuleDestroy(): void {
    console.log("生命周期服务2销毁,onModuleDestroy。"); // 模块销毁
  }
}

@Injectable()
export class LifetimeService3 implements BeforeApplicationShutdown {
  // 当应用程序即将关闭时调用此方法
  beforeApplicationShutdown(signal?: string): void {
    console.log(`生命周期服务3即将关闭,beforeApplicationShutdown:${signal}`); // 应用即将关闭
  }
}

@Injectable()
export class LifetimeService4 implements OnApplicationShutdown {
  // 当应用程序关闭时调用此方法
  onApplicationShutdown(signal?: string): void {
    console.log(`生命周期服务4关闭,OnApplicationShutdown:${signal}`); // 应用关闭
  }
}

首先我们先测试没有使用 app.enableShutdownHooks() 例子:

应用启动后,我使用 Ctrl+C 的方式停止应用,可以看到按照生命周期钩子回调顺序依次执行了,但是这里只执行了两个。

继续,我们开启 enableShutdownHooks 这个钩子,如下图所示:

这些执行顺序正如我们前面中所提到的那样。

总结

NestJS 的生命周期钩子提供了在应用启动、运行和关闭各阶段精确控制的能力,允许我们在开发的过程中管理资源、处理异常并优化应用性能。这些钩子涵盖从模块初始化到应用关闭的全过程,使得资源使用更高效,确保了应用的稳定性和优雅关闭。通过这些机制,NestJS 支持复杂应用的健壮开发和维护。

相关推荐
非著名架构师3 分钟前
js混淆的方式方法
开发语言·javascript·ecmascript
多多米100537 分钟前
初学Vue(2)
前端·javascript·vue.js
敏编程1 小时前
网页前端开发之Javascript入门篇(5/9):函数
开发语言·javascript
柏箱1 小时前
PHP基本语法总结
开发语言·前端·html·php
新缸中之脑1 小时前
Llama 3.2 安卓手机安装教程
前端·人工智能·算法
hmz8561 小时前
最新网课搜题答案查询小程序源码/题库多接口微信小程序源码+自带流量主
前端·微信小程序·小程序
看到请催我学习1 小时前
内存缓存和硬盘缓存
开发语言·前端·javascript·vue.js·缓存·ecmascript
blaizeer2 小时前
深入理解 CSS 浮动(Float):详尽指南
前端·css
编程老船长2 小时前
网页设计基础 第一讲:软件分类介绍、工具选择与课程概览
前端
编程老船长2 小时前
网页设计基础 第二讲:安装与配置 VSCode 开发工具,创建第一个 HTML 页面
前端