iT邦幫忙

2021 iThome 鐵人賽

DAY 18
0
Modern Web

NestJS 帶你飛!系列 第 18

[NestJS 帶你飛!] DAY18 - Lifecycle Hooks

什麼是 Lifecycle Hook?

在開始介紹之前,先來了解一下何謂 生命週期 (Lifecycle),最簡單的舉例是人,人從出生到死亡就是一個完整的生命週期。那什麼是 生命週期鉤子 (Lifecycle hook) ?即在生命週期中某個時間點會觸發的事件,比如:小明在出生之後被賦予了國民身份、 5 歲的時候會上幼稚園等。在程式設計領域中,也有所謂的生命週期,最簡單的分法為:開始到結束,有些框架甚至會設計 Lifecycle Hook 來針對不同時間點觸發不同的事件,比如說:啟動時先呼叫 API、關閉時留下 Log 資訊等。

Nest Lifecycle Hooks

Nest 也有設計 Lifecycle Hook,按照順序排列共分成下方五個時間點:

  1. Module 初始化階段 (onModuleInit)
  2. Nest App 啟動階段 (onApplicationBootstrap)
  3. Module 銷毀階段 (onModuleDestroy)
  4. Nest App 關閉前 (beforeApplicationShutdown)
  5. Nest App 關閉階段 (onApplicationShutdown)

這裡可以看出 Lifecycle Hooks 發生在「啟動」與「關閉」這兩個時間點,而它們可以在 modulescontrollersinjectables 被觸發。需特別注意的是,在關閉時間點的 Hook 必須要在 bootstrap 執行時調用 app.enableShutdownHooks() 來開啟此功能,會在執行 app.close() 或收到系統關閉訊號時 (Ctrl + C) 被觸發。

注意:由於關閉時執行的 Hook 會消耗較多的效能在監聽事件上,故預設是不啟用的。

onModuleInit

onModuleInit 會在該模組的依賴項目處理完畢時被調用。假設 Nest App 有 AppModuleTodoModule,並在 AppModule 引入了 TodoModule
https://ithelp.ithome.com.tw/upload/images/20210522/20119338VHekXphWyY.png

AppModule 會先被載入,載入的時候會去讀取它的依賴項目:TodoModuleAppControllerAppService,當這些依賴項目處理完畢後,也就是依賴項目會先呼叫 onModuleInitAppModule 才會調用其 onModuleInit,順序如下圖所示:
https://ithelp.ithome.com.tw/upload/images/20210522/201193384jhaImOilE.png

使用方式很簡單,假如我們要在 AppModule 使用 onModuleInit,就讓它實作 OnModuleInit 介面,並添加 onModuleInit 方法:

import { Module, OnModuleInit } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [
  ],
  controllers: [
    AppController
  ],
  providers: [
    AppService
  ]
})
export class AppModule implements OnModuleInit {
  onModuleInit(): void {
    console.log('[AppModule]: initial event!');
  }
}

如此一來,便會在初始化階段於終端機顯示下方字串:

[AppModule]: initial event!

onApplicationBootstrap

onApplicationBootstrap 在 Nest App 初始化所有模組後進行調用,會發生在連線建立前。與 onModuleInit 的執行順序相同,會先執行依賴項目的 onApplicationBootstrap

假如我們要在 AppModule 使用 onApplicationBootstrap,就讓它實作 OnApplicationBootstrap 介面,並添加 onApplicationBootstrap 方法:

import { Module, OnApplicationBootstrap } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [
  ],
  controllers: [
    AppController
  ],
  providers: [
    AppService
  ]
})
export class AppModule implements OnApplicationBootstrap {
  onApplicationBootstrap() {
    console.log('[AppModule]: bootstrap event!');
  }
}

如此一來,便會在啟動後之階段於終端機顯示下方字串:

[AppModule]: bootstrap event!

onModuleDestroy

onModuleDestroy 在接收到系統關閉訊號或 app.close() 時調用。與 onModuleInit 的執行順序 不同,會從 AppModule 的 Controller 與 Provider 開始執行 onModuleDestroy,執行完之後 AppModule 就會觸發 onModuleDestroy,接著其依賴項目才會依序執行該 Hook,順序如下圖所示:
https://ithelp.ithome.com.tw/upload/images/20210522/201193387H40KuPQXA.png

注意:在 Nest 第 8 版中,onModuleDestroy 的執行順序與 onModuleInit 相同,感謝熱心的 mihuartuanr 提醒。

假如我們要在 AppModule 使用 onModuleDestroy,就讓它實作 OnModuleDestroy 介面,並添加 onModuleDestroy 方法:

import { Module, OnModuleDestroy } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [
    TodoModule,
    UserModule
  ],
  controllers: [
    AppController
  ],
  providers: [
    AppService
  ]
})
export class AppModule implements OnModuleDestroy {
  onModuleDestroy(): void {
    console.log('[AppModule]: destroy event!');
  }
}

如此一來,便會在關閉時於終端機顯示下方字串:

[AppModule]: destroy event!

beforeApplicationShutdown

beforeApplicationShutdown 在 Nest App 關閉所有連線之前調用,並會觸發 app.close()。與 onModuleInit 的執行順序相同,會先執行依賴項目的 beforeApplicationShutdown

假如我們要在 AppModule 使用 beforeApplicationShutdown,就讓它實作 BeforeApplicationShutdown 介面,並添加 beforeApplicationShutdown 方法:

import { BeforeApplicationShutdown, Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [
  ],
  controllers: [
    AppController
  ],
  providers: [
    AppService
  ]
})
export class AppModule implements BeforeApplicationShutdown {
  beforeApplicationShutdown(): void {
    console.log('[AppModule]: before shutdown event!');
  }
}

如此一來,便會在關閉連線前之階段於終端機顯示下方字串:

[AppModule]: before shutdown event!

onApplicationShutdown

onApplicationShutdown 在 Nest App 關閉所有連接時進行調用。與 onModuleInit 的執行順序相同,會先執行依賴項目的 onApplicationShutdown

假如我們要在 AppModule 使用 onApplicationShutdown,就讓它實作 OnApplicationShutdown 介面,並添加 onApplicationShutdown 方法:

import { Module, OnApplicationShutdown } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [
  ],
  controllers: [
    AppController
  ],
  providers: [
    AppService
  ]
})
export class AppModule implements OnApplicationShutdown {
  onApplicationShutdown(): void {
    console.log('[AppModule]: shutdown event!');
  }
}

如此一來,便會在關閉時於終端機顯示下方字串:

[AppModule]: shutdown event!

小結

運用 Lifecycle Hooks 可以有效地在適當時機點做適當的動作,以關閉時調用的 Hook 來說,通常會用在 Kubernetes 等服務上。這裡附上今天的懶人包:

  1. Nest 的 Lifecycle Hooks 共有五個,主要是在「啟動」與「關閉」這兩個時間點觸發。
  2. 五個 Hook 分別為:onModuleInitonApplicationBootstraponModuleDestroybeforeApplicationShutdownonApplicationShutdown
  3. 「關閉」時調用的 Hook 需要透過 app.enableShutdownHooks() 來啟用此功能。

上一篇
[NestJS 帶你飛!] DAY17 - Injection Scopes
下一篇
[NestJS 帶你飛!] DAY19 - Module Reference
系列文
NestJS 帶你飛!32

1 則留言

0
mihuartuanr
iT邦新手 5 級 ‧ 2021-11-12 16:33:28

您好:

onModuleDestroy 在接收到系統關閉訊號或 app.close() 時調用。與 onModuleInit 的執行順序 不同,會從 AppModule 的 Controller 與 Provider 開始執行 onModuleDestroy,執行完之後 AppModule 就會觸發 onModuleDestroy,接著其依賴項目才會依序執行該 Hook

这里我做了如下试验:

// app.module.ts
@Module({
  imports: [
    TodoModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule
  implements
    OnModuleInit,
    OnModuleDestroy,
    OnApplicationBootstrap,
    BeforeApplicationShutdown,
    OnApplicationShutdown
{
  onModuleInit(): void {
    console.log('[AppModule]: init');
  }
  onModuleDestroy(): void {
    console.log('[AppModule]: destroy');
  }
  onApplicationBootstrap(): void {
    console.log('[AppModule]: bootstrap');
  }
  beforeApplicationShutdown(): void {
    console.log('[AppModule]: before shutdown event!');
  }
  onApplicationShutdown(): void {
    console.log('[AppModule]: shutdown event!');
  }
}
// todo.module.ts
@Module({
  controllers: [TodoController],
  providers: [HANDSOME_HAO],
})
export class TodoModule
  implements
    NestModule,
    OnModuleInit,
    OnModuleDestroy,
    OnApplicationBootstrap,
    BeforeApplicationShutdown,
    OnApplicationShutdown
{
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware, TestMiddleware, AddUserMiddleware)
      .forRoutes('/todo');
  }
  onModuleInit(): void {
    console.log('[TodoModule]: init');
  }
  onModuleDestroy(): void {
    console.log('[TodoModule]: destroy');
  }
  onApplicationBootstrap(): void {
    console.log('[TodoModule]: bootstrap');
  }
  beforeApplicationShutdown(): void {
    console.log('[TodoModule]: before shutdown event!');
  }
  onApplicationShutdown(): void {
    console.log('[TodoModule]: shutdown event!');
  }
}

在通过Ctrl + C终止程序时,顺序如下:

[TodoController]: init
[TodoModule]: init
[AppController]: init
[AppService]: init
[AppModule]: init
[TodoController]: bootstrap
[TodoModule]: bootstrap
[AppController]: bootstrap
[AppService]: bootstrap
[AppModule]: bootstrap
[Nest] 14140  - 2021-11-12 17:21:22     LOG [NestApplication] Nest application successfully started +15ms
[TodoController]: destroy
[TodoModule]: destroy
[AppController]: destroy
[AppService]: destroy
[AppModule]: destroy
[TodoController]: before shutdown event!
[TodoModule]: before shutdown event!
[AppController]: before shutdown event!
[AppService]: before shutdown event!
[AppModule]: before shutdown event!
[TodoController]: shutdown event!
[TodoModule]: shutdown event!
[AppController]: shutdown event!
[AppService]: shutdown event!
[AppModule]: shutdown event!

看执行输出,onModuleDestroy與 onModuleInit 的執行順序相同,是不是我对这块的理解有问题呢?

HAO iT邦新手 3 級 ‧ 2021-11-13 11:10:01 檢舉

你好,經查證發現這是 Nest 版本差異所致,由於我在 trace source code 以及親自實驗時是用 v7,後來升上 v8 沒有注意到執行順序改變了,我會在文章註明一下,謝謝提醒!
另外,我有去 trace 什麼時候調整的,可以參考這個 commitPR,有問題歡迎互相交流!

我要留言

立即登入留言