iT邦幫忙

2021 iThome 鐵人賽

DAY 15
0
Modern Web

NestJS 帶你飛!系列 第 15

[NestJS 帶你飛!] DAY15 - Dynamic Module

前面有介紹過 Module 的一些基本使用方式,然而有一項非常強大的功能沒有被提及,就是 動態模組(Dynamic Module),它可以用很簡單的方式去客製化 Provider 的內容,使該 Module 的 Provider 動態化,什麼意思呢?簡單來說,就是我們希望這個 Module 是可以透過外部傳入參數去設置 Provider 的內容,與一般 靜態模組(Static Module) 不同的地方在於,靜態模組建立後 Provider 即建立完畢,若要更改 Provider 相關配置則要變動這個 Module 內部的程式碼;動態模組則是將可能會變動的部分 參數化,讓使用者在使用此 Module 時,可以透過其提供的 靜態方法 來帶入參數,讓 Provider 接受該參數並建立 Module。

https://ithelp.ithome.com.tw/upload/images/20210512/20119338XnHqmcLaeS.png

用生活中的例子來說明的話,靜態模組就像一個專用遙控器,在沒有去改寫內部的規則之前,它只能針對特定設備做控制;動態模組就像一個萬用遙控器,同樣是控制設備,但只需要根據特定的操作就能去控制不同的設備。

設計 Dynamic Module

動態模組是很常使用的功能,其中,最常遇到的情境就是環境變數管理,設計一個 Module 專門處理環境變數,這樣的情境非常適合使用動態模組來處理,原因是管理環境變數的邏輯通常是不變的,會變的部分僅僅是讀取環境變數的檔案路徑等,透過動態模組的機制成功將其抽離成共用元件,降低耦合度。

注意:關於環境變數的介紹會在下篇做更詳細的說明。

這篇我們會運用動態模組與 dotenv 來實作一套簡單的環境變數管理模組,名稱定為 ConfigurationModule

注意dotenv 是一套用於管理環境變數的套件,詳細內容可以參考官方文件

目標是讓 ConfigurationModule 提供一個靜態方法 forRoot,它可以接受一個包含 key 值為 path 的物件參數,path.env 檔的相對路徑,透過 forRoot 將參數帶給 ConfigurationService 來處理 .env 的檔案並管理解析出來的變數。首先,透過 npm 安裝 dotenv

$ npm install dotenv --save

透過 CLI 產生 ConfigurationModuleConfigurationService

$ nest generate module common/configuration
$ nest generate service common/configuration

接著打開 configuration.module.ts,替 ConfigurationModule 添加一個 forRoot 靜態方法,回傳的值即為 DynamicModule,而 DynamicModule 其實就是一個物件,與 @Module 裝飾器內的參數大致相同,不同的是必須要帶上 module 參數,其值為 ConfigurationModule 本身,另外,還有 global 參數可以使產生出來的 Module 變成全域:

import { DynamicModule, Module } from '@nestjs/common';
import { ConfigurationService } from './configuration.service';

@Module({})
export class ConfigurationModule {

  static forRoot(): DynamicModule {
    return {
      providers: [
        ConfigurationService
      ],
      module: ConfigurationModule,
      global: true
    };
  }

}

注意:靜態方法可以自行設計,但回傳值必須為同步或非同步 DynamicModule,名稱通常會使用 forRootregister

從上方程式碼可以看出 @Module 的參數淨空了,這是為什麼呢?因為我們只使用動態模組,所以沒有特別設計靜態模組的部分,但如果要設計也是可以的。

接下來要在 forRoot 設計包含 key 值為 path 的物件參數,並將 path 取出,運用 Value Provider 的方式將該值記錄下來。先在 configuration 資料夾下新增 constants 資料夾,並在裡面建立 token.const.ts 來管理 token

export const ENV_PATH = 'ENV_PATH';

調整 configuration.module.ts

import { DynamicModule, Module } from '@nestjs/common';
import { ConfigurationService } from './configuration.service';
import { ENV_PATH } from './constants/token.const';

@Module({})
export class ConfigurationModule {

  static forRoot(options: { path: string }): DynamicModule {
    return {
      providers: [
        {
          provide: ENV_PATH,
          useValue: options.path
        },
        ConfigurationService
      ],
      exports: [
        ConfigurationService
      ],
      module: ConfigurationModule,
      global: true
    };
  }

}

最後就是設計 ConfigurationService 的內容了,在 constructor 注入剛才設計的環境變數路徑 ENV_PATH,接著設計 setEnvironment 去讀取並解析 .env 檔,然後寫入 config 屬性中,最後設計一個 get(key: string) 的方法來提取要用的環境變數:

import { Inject, Injectable } from '@nestjs/common';

import * as fs from 'fs';
import * as path from 'path';
import * as dotenv from 'dotenv';

import { ENV_PATH } from './constants/token.const';

@Injectable()
export class ConfigurationService {

  private config: any;

  constructor(
    @Inject(ENV_PATH) private readonly path: string
  ) {
    this.setEnvironment();
  }

  public get(key: string): string {
    return this.config[key];
  }

  private setEnvironment(): void {
    const filePath = path.resolve(__dirname, '../../', this.path);
    this.config = dotenv.parse(fs.readFileSync(filePath));
  }

}

使用 Dynamic Module

設計完 ConfigurationModule 以後,先在專案路徑下新增 development.env 檔,並設定裡面的內容:

USERNAME=HAO

注意:是新增在專案路徑下,與 package.json 同層級,非 src

接著,調整 app.module.ts 的內容:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigurationModule } from './common/configuration/configuration.module';

@Module({
  imports: [
    ConfigurationModule.forRoot({ 
      path: `../${process.env.NODE_ENV || 'development'}.env`
    })
  ],
  controllers: [
    AppController
  ],
  providers: [
    AppService
  ]
})
export class AppModule {
}

調整 app.controller.ts 的內容,在 constructor 注入 ConfigurationService,並改寫 getHello 回傳值:

import { Controller, Get } from '@nestjs/common';
import { ConfigurationService } from './common/configuration/configuration.service';

@Controller()
export class AppController {
  constructor(
    private readonly configService: ConfigurationService
  ) {
  }

  @Get()
  getHello() {
    return { username: this.configService.get('USERNAME') };
  }
}

透過瀏覽器查看 http://localhost:3000,會得到 USERNAME 的值:
https://ithelp.ithome.com.tw/upload/images/20210515/20119338UwcodEJlcJ.png

小結

Dynamic Module 是非常好用且實用的功能,經常運用在資料庫、環境變數管理等功能,不過需要對 Nest 的依賴注入機制有一定程度的了解,在基礎穩固之後學習上比較不會有問題。這裡附上今天的懶人包:

  1. Dynamic Module 是運用靜態方法回傳一個 DynamicModule 型別的物件。
  2. 善用 Dynaic Module 來抽離共用元件。
  3. DynamicModule 必須包含 module 參數。
  4. 靜態方法名稱通常取為 forRootregister

上一篇
[NestJS 帶你飛!] DAY14 - Custom Decorator
下一篇
[NestJS 帶你飛!] DAY16 - Configuration
系列文
NestJS 帶你飛!32

尚未有邦友留言

立即登入留言