本系列文已出版成書「NestJS 基礎必學實務指南:使用強大且易擴展的 Node.js 框架打造網頁應用程式」,感謝 iT 邦幫忙與博碩文化的協助。如果對 NestJS 有興趣、覺得這個系列文對你有幫助的話,歡迎前往購書,你的支持是我最大的寫作動力!
前面有介紹過 Module 的一些基本使用方式,然而有一項非常強大的功能沒有被提及,就是 動態模組(Dynamic Module),它可以用很簡單的方式去客製化 Provider 的內容,使該 Module 的 Provider 動態化,什麼意思呢?簡單來說,就是我們希望這個 Module 是可以透過外部傳入參數去設置 Provider 的內容,與一般 靜態模組(Static Module) 不同的地方在於,靜態模組建立後 Provider 即建立完畢,若要更改 Provider 相關配置則要變動這個 Module 內部的程式碼;動態模組則是將可能會變動的部分 參數化,讓使用者在使用此 Module 時,可以透過其提供的 靜態方法 來帶入參數,讓 Provider 接受該參數並建立 Module。
用生活中的例子來說明的話,靜態模組就像一個專用遙控器,在沒有去改寫內部的規則之前,它只能針對特定設備做控制;動態模組就像一個萬用遙控器,同樣是控制設備,但只需要根據特定的操作就能去控制不同的設備。
動態模組是很常使用的功能,其中,最常遇到的情境就是環境變數管理,設計一個 Module 專門處理環境變數,這樣的情境非常適合使用動態模組來處理,原因是管理環境變數的邏輯通常是不變的,會變的部分僅僅是讀取環境變數的檔案路徑等,透過動態模組的機制成功將其抽離成共用元件,降低耦合度。
注意:關於環境變數的介紹會在下篇做更詳細的說明。
這篇我們會運用動態模組與 dotenv 來實作一套簡單的環境變數管理模組,名稱定為 ConfigurationModule
。
注意:
dotenv
是一套用於管理環境變數的套件,詳細內容可以參考官方文件。
目標是讓 ConfigurationModule
提供一個靜態方法 forRoot
,它可以接受一個包含 key
值為 path
的物件參數,path
即 .env
檔的相對路徑,透過 forRoot
將參數帶給 ConfigurationService
來處理 .env
的檔案並管理解析出來的變數。首先,透過 npm
安裝 dotenv
:
$ npm install dotenv --save
透過 CLI 產生 ConfigurationModule
與 ConfigurationService
:
$ 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
,名稱通常會使用forRoot
或register
。
從上方程式碼可以看出 @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));
}
}
設計完 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
的值:
Dynamic Module 是非常好用且實用的功能,經常運用在資料庫、環境變數管理等功能,不過需要對 Nest 的依賴注入機制有一定程度的了解,在基礎穩固之後學習上比較不會有問題。這裡附上今天的懶人包:
DynamicModule
型別的物件。DynamicModule
必須包含 module
參數。forRoot
或 register
。