本系列文已出版成書「NestJS 基礎必學實務指南:使用強大且易擴展的 Node.js 框架打造網頁應用程式」,感謝 iT 邦幫忙與博碩文化的協助。如果對 NestJS 有興趣、覺得這個系列文對你有幫助的話,歡迎前往購書,你的支持是我最大的寫作動力!
前一篇我們運用 Dynamic Module 與 dotenv
設計了一個簡單的環境變數管理模組,但什麼是環境變數?又為什麼要做環境變數的管理?那有沒有現成的輪子可以使用?接下來會一一告訴各位!
一套系統通常會執行在各個不同環境上,最簡單的區分為:開發環境與正式環境,會這樣區別的原因是我們不希望在測試系統的時候去影響到正式環境的資料,所以會將資料庫等配置分成兩組,也就會有兩組資料庫的連接資訊需要被記錄與使用,這時候要仔細想想該如何做好這些敏感資訊的配置又能快速切換環境,將資訊直接寫在程式碼裡頭絕對是不理想的方式,於是就有 環境變數(Environment Variable) 的概念。
環境變數與一般變數不同的地方在於,環境變數是透過程式碼以外的地方做指定,這種變數可以直接在作業系統上設定,也可以透過指令的方式做設定,以 node.js 為例,可以直接在指令中做配置:
$ NODE_ENV=production node index.js
如此一來,便可以在 process.env
取得環境變數,但如果每次都要這樣輸入與調用實在很難管理,於是就有環境變數檔的概念出現,在 node.js 最常用的就是 .env
檔,其設計方式很簡單,等號的左邊為 key
值,右邊為 value
:
USERNAME=HAO
在 Nest 中,可以使用官方製作的 ConfigModule
來讀取並管理這些環境變數,當然,要自行設計也可以,透過 Dynamic Module 的概念來實作即可。
既然有造好的輪子可以使用,且前一篇也有簡單的帶過如何用 Dynamic Module 實作,故這篇就專門介紹官方實作的套件。該套件並不是內建的,需要額外安裝,透過 npm
進行安裝即可:
提醒:若前一篇有安裝過
dotenv
可以先行移除。
$ npm install @nestjs/config --save
ConfigModule
也是使用 Dynamic Module 概念設計的,我們只需要在 AppModule
中調用其 forRoot
方法即可使用,以下方為例,修改 app.module.ts
:
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [
ConfigModule.forRoot()
],
controllers: [
AppController
],
providers: [
AppService
]
})
export class AppModule {
}
接著,在專案路徑下新增 .env
檔,並設置其內容:
USERNAME=HAO
注意:是新增在專案路徑下,與
package.json
同層級,非src
。
修改 app.controller.ts
,在 AppController
的 contructor
注入 ConfigService
,讓 getHello
透過 ConfigService
的 get
方法取出 USERNAME
並回傳:
import { Controller, Get } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Controller()
export class AppController {
constructor(
private readonly configService: ConfigService
) {
}
@Get()
getHello() {
const username = this.configService.get('USERNAME');
return { username };
}
}
透過瀏覽器查看 http://localhost:3000:
預設狀態下,ConfigModule
會從專案路徑下取 .env
檔來做為環境變數檔,但我們常常會需要為不同環境配置多個檔案,這時候就可以透過自訂環境變數檔來處理。ConfigModule
的 forRoot
靜態方法有提供 envFilePath
參數來配置指定的 .env
檔,以下方為例,我們去讀取 development.env
:
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [
ConfigModule.forRoot({
envFilePath: 'development.env'
})
],
controllers: [
AppController
],
providers: [
AppService
]
})
export class AppModule {
}
將剛才的 .env
檔的名稱變更為 development.env
,並重新啟動 Nest App,接著透過瀏覽器查看 http://localhost:3000:
還有一種情況是本地測試使用的環境變數與其他環境下測試用的環境變數不相同,這時候可以使用優先權的方式做載入,假設本地端使用的環境變數檔名為 development.local.env
,而其他環境下使用的環境變數檔名為 development.env
,那就可以在 envFilePath
配置一個陣列,其內容為檔案名稱,越前面的優先權越高。這裡我們先建立一個 development.local.env
並添加下方內容:
USERNAME=local_tester
修改一下 app.module.ts
,讓 development.local.env
的優先級別大於 development.env
:
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [
ConfigModule.forRoot({
envFilePath: ['development.local.env', 'development.env']
})
],
controllers: [
AppController
],
providers: [
AppService
]
})
export class AppModule {
}
透過瀏覽器查看 http://localhost:3000 會看到 username
為 local_tester
:
有些複雜的情境可以透過工廠模式來處理環境變數,比如:假設有配置 development.env
,但有些比較不敏感的資訊可以直接使用預設值,故不需要在檔案裡面做相關配置,只需要在工廠函式裡做配置即可。我們在 src
資料夾下創建一個名為 config
的資料夾,並在裡面建立 configuration.factory.ts
:
修改 configuration.factory.ts
的內容,讓 PORT
採用預設值 3000
:
export default () => ({
PORT: process.env.PORT || 3000
});
接著,修改 app.module.ts
的內容,添加 load
參數至 forRoot
靜態方法中,其接受的型別為陣列,內容即工廠函式:
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import configurationFactory from './config/configuration.factory';
@Module({
imports: [
ConfigModule.forRoot({
envFilePath: ['development.local.env', 'development.env'],
load: [configurationFactory]
})
],
controllers: [
AppController
],
providers: [
AppService
]
})
export class AppModule {
}
注意:
load
參數接受陣列是因為它可以使用多個工廠函式來處理環境變數。
修改 app.controller.ts
,讓 getHello
透過 ConfigService
的 get
方法取出 USERNAME
與 PORT
並回傳:
import { Controller, Get } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Controller()
export class AppController {
constructor(
private readonly configService: ConfigService
) {
}
@Get()
getHello() {
const username = this.configService.get('USERNAME');
const port = this.configService.get('PORT');
return { username, port };
}
}
透過瀏覽器查看 http://localhost:3000:
由於環境變數在配置的時候是採用 =
來劃分 key
與 value
的,並不能在 value
的地方延伸出下一個層級,所以環境變數層級是 扁平 的,沒有辦法按照類別做歸類,以下方為例,假設環境變數檔 development.local.env
裡面有下方資訊:
DB_HOST=example.com
DB_PASSWORD=12345678
PORT=3000
可以很明顯看出 DB_HOST
與 DB_PASSWORD
皆屬於資料庫的配置項目,但層級上與其他配置項目卻是相同的,大致上會像這樣:
{
"DB_HOST": "example.com",
"DB_PASSWORD": "12345678",
"PORT": "3000"
}
我們的理想情況會是下方這樣,相同類型的資料被歸在一個 命名空間(Namespace) 裡:
{
"database": {
"host": "example.com",
"password": "12345678"
},
"port": "3000"
}
雖然無法在環境變數檔做好這樣的配置,但可以透過工廠函式來做處理。修改 configuration.factory.ts
,透過 registerAs
這個函式來指定其命名空間,第一個參數即命名空間,第二個參數為 Callback,回傳的內容即整理好的物件:
import { registerAs } from '@nestjs/config';
export default registerAs('database', () => ({
host: process.env.DB_HOST,
password: process.env.DB_PASSWORD
}));
修改 app.controller.ts
,從程式碼可以發現,如果要取出命名空間內的某項環境變數的話,透過 .
的方式取得即可,就跟操作 Object
資料一樣:
import { Controller, Get } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Controller()
export class AppController {
constructor(
private readonly configService: ConfigService
) {
}
@Get()
getHello() {
const database = this.configService.get('database');
const db_host = this.configService.get('database.host'); // 取得 database 裡的 host
const port = this.configService.get('PORT');
return { database, db_host, port };
}
}
透過瀏覽器查看 http://localhost:3000:
有時候會在環境變數檔裡配置 port,要能夠使用環境變數檔裡的 port 作為啟動 Nest 的 port,就必須在 main.ts
做處理,但要怎麼取得 ConfigService
呢?其實 app
這個實例有提供一個 get
方法,可以取出其參照:
import { NestFactory } from '@nestjs/core';
import { ConfigService } from '@nestjs/config';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const configService = app.get(ConfigService); // 取得 ConfigService
const port = configService.get('port');
await app.listen(port);
}
bootstrap();
假設有兩個環境變數是存在依賴關係的,具體內容如下:
APP_DOMAIN=example.com
APP_REDIRECT_URL=example.com/redirect_url
可以看出 APP_REDIRECT_URL
包含了 APP_DOMAIN
,但環境變數檔並沒有宣告變數的功能,這樣在管理上會比較麻煩,還好 Nest 有實作一個功能來彌補,透過指定 forRoot
物件參數中的 expandVariables
為 true
來解析環境變數檔,讓環境變數檔像有變數宣告功能一樣,透過${...}
來嵌入指定的環境變數。下方為 development.local.env
的內容:
APP_DOMAIN=example.com
APP_REDIRECT_URL=${APP_DOMAIN}/redirect_url
修改一下 app.module.ts
的內容:
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [
ConfigModule.forRoot({
envFilePath: ['development.local.env', 'development.env'],
expandVariables: true // 開啟環境變數檔變數嵌入功能
})
],
controllers: [
AppController
],
providers: [
AppService
]
})
export class AppModule {
}
修改 app.controller.ts
,讓 getHello
回傳 APP_DOMAIN
與 APP_REDIRECT_URL
:
import { Controller, Get } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Controller()
export class AppController {
constructor(
private readonly configService: ConfigService
) {
}
@Get()
getHello() {
const app_domain = this.configService.get('APP_DOMAIN');
const redirect_url = this.configService.get('APP_REDIRECT_URL');
return { app_domain, redirect_url };
}
}
透過瀏覽器查看 http://localhost:3000:
如果 ConfigModule
會在多個模組中使用的話,可以配置 isGlobal
為 true
將其配置為全域模組,這樣就不需要在其他模組中引入 ConfigModule
:
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [
ConfigModule.forRoot({
envFilePath: ['development.local.env', 'development.env'],
isGlobal: true
})
],
controllers: [
AppController
],
providers: [
AppService
]
})
export class AppModule {
}
環境變數的配置絕對是必要的,在有很多執行環境的情況下,更需要使用像 ConfigModule
這樣的管理模組來降低維護成本。這裡附上今天的懶人包:
ConfigModule
。ConfigModule
使用 Dynamic Module 的概念實作。.env
檔來配置環境變數。envFilePath
來指定自訂的環境變數檔。envFilePath
可以按照優先權做排序。load
搭配來處理環境變數。main.ts
中取出 ConfigService
來獲得環境變數。expandVariables
讓環境變數檔有嵌入變數的功能。isGlobal
讓 ConfigModule
提升為全域模組。想咨询一下,工廠函式配置命名空間的情况下,是否可以引用.env文档中配置的数据呢?
顺便~官网提供了yaml做配置文件可以创建上下级结构的配置文件
你好,基本上配置命名空間的時候,就會讓開發者去配置哪些環境變數要歸類在該命名空間下,而這些環境變數會從 .env
中提取的。
謝謝你的補充,我在文中確實沒有特別提到 yaml
的配置方式,對於喜歡 yaml
的開發者而言應該是蠻實用的功能
nest官方的config模組在對接巢狀組態結構跟環境變數的方式必須自己明確定義,有些繁瑣。
所以我們公司後來模仿Asp.net core的組態方式,另外寫了給nestjs專案使用的config模組。