NestJS 配置

2023-09-08 16:58 更新

應(yīng)用程序通常在不同的環(huán)境中運行。根據(jù)環(huán)境的不同,應(yīng)該使用不同的配置設(shè)置。例如,通常本地環(huán)境依賴于特定的數(shù)據(jù)庫憑據(jù),僅對本地 DB 實例有效。生產(chǎn)環(huán)境將使用一組單獨的 DB 憑據(jù)。由于配置變量會更改,所以最佳實踐是將配置變量存儲在環(huán)境中。

外部定義的環(huán)境變量通過 process.env global 在 Node.js 內(nèi)部可見。 我們可以嘗試通過在每個環(huán)境中分別設(shè)置環(huán)境變量來解決多個環(huán)境的問題。 這會很快變得難以處理,尤其是在需要輕松模擬或更改這些值的開發(fā)和測試環(huán)境中。

在 Node.js 應(yīng)用程序中,通常使用 .env 文件,其中包含鍵值對,其中每個鍵代表一個特定的值,以代表每個環(huán)境。 在不同的環(huán)境中運行應(yīng)用程序僅是交換正確的.env 文件的問題。

在 Nest 中使用這種技術(shù)的一個好方法是創(chuàng)建一個 ConfigModule ,它暴露一個 ConfigService ,根據(jù) $NODE_ENV 環(huán)境變量加載適當(dāng)?shù)?nbsp;.env 文件。雖然您可以選擇自己編寫這樣的模塊,但為方便起見,Nest 提供了開箱即用的@ nestjs/config軟件包。 我們將在本章中介紹該軟件包。

安裝

要開始使用它,我們首先安裝所需的依賴項。

$ npm i --save @nestjs/config

注意 @nestjs/config 內(nèi)部使用 dotenv 實現(xiàn)。

開始使用

安裝完成之后,我們需要導(dǎo)入ConfigModule模塊。通常,我們在根模塊AppModule中導(dǎo)入它,并使用.forRoot()靜態(tài)方法導(dǎo)入它的配置。

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [ConfigModule.forRoot()],
})
export class AppModule {}

上述代碼將從默認(rèn)位置(項目根目錄)載入并解析一個.env文件,從.env文件和process.env合并環(huán)境變量鍵值對,并將結(jié)果存儲到一個可以通過ConfigService訪問的私有結(jié)構(gòu)。forRoot()方法注冊了ConfigService提供者,后者提供了一個get()方法來讀取這些解析/合并的配置變量。由于@nestjs/config依賴dotenv,它使用該包的規(guī)則來處理沖突的環(huán)境變量名稱。當(dāng)一個鍵同時作為環(huán)境變量(例如,通過操作系統(tǒng)終端如export DATABASE_USER=test導(dǎo)出)存在于運行環(huán)境中以及.env文件中時,以運行環(huán)境變量優(yōu)先。

一個樣例.env文件看起來像這樣:

DATABASE_USER=test
DATABASE_PASSWORD=test

自定義 env 文件路徑

默認(rèn)情況下,程序在應(yīng)用程序的根目錄中查找.env文件。 要為.env文件指定另一個路徑,請配置forRoot()的配置對象 envFilePath 屬性(可選),如下所示:

ConfigModule.forRoot({
  envFilePath: '.development.env',
});

您還可以像這樣為.env 文件指定多個路徑:

ConfigModule.forRoot({
  envFilePath: ['.env.development.local', '.env.development'],
});

如果在多個文件中發(fā)現(xiàn)同一個變量,則第一個變量優(yōu)先。

禁止加載環(huán)境變量

如果您不想加載.env 文件,而是想簡單地從運行時環(huán)境訪問環(huán)境變量(如 OS shell 導(dǎo)出,例如export DATABASE_USER = test),則將options對象的ignoreEnvFile屬性設(shè)置為true,如下所示 :

ConfigModule.forRoot({
  ignoreEnvFile: true,
});

全局使用

當(dāng)您想在其他模塊中使用ConfigModule時,需要將其導(dǎo)入(這是任何 Nest 模塊的標(biāo)準(zhǔn)配置)。 或者,通過將options對象的isGlobal屬性設(shè)置為true,將其聲明為全局模塊,如下所示。 在這種情況下,將ConfigModule加載到根模塊(例如AppModule)后,您無需在其他模塊中導(dǎo)入它。

ConfigModule.forRoot({
  isGlobal: true,
});

自定義配置文件

對于更復(fù)雜的項目,您可以利用自定義配置文件返回嵌套的配置對象。 這使您可以按功能對相關(guān)配置設(shè)置進(jìn)行分組(例如,與數(shù)據(jù)庫相關(guān)的設(shè)置),并將相關(guān)設(shè)置存儲在單個文件中,以幫助獨立管理它們

自定義配置文件導(dǎo)出一個工廠函數(shù),該函數(shù)返回一個配置對象。配置對象可以是任意嵌套的普通 JavaScript 對象。process.env對象將包含完全解析的環(huán)境變量鍵/值對(具有如上所述的.env文件和已解析和合并的外部定義變量)。因為您控制了返回的配置對象,所以您可以添加任何必需的邏輯來將值轉(zhuǎn)換為適當(dāng)?shù)念愋汀⒃O(shè)置默認(rèn)值等等。例如:

// config/configuration.ts
export default () => ({
  port: parseInt(process.env.PORT, 10) || 3000,
  database: {
    host: process.env.DATABASE_HOST,
    port: parseInt(process.env.DATABASE_PORT, 10) || 5432
  }
});

我們使用傳遞給ConfigModule.forRoot()方法的 options 對象的load屬性來加載這個文件:

import configuration from './config/configuration';

@Module({
  imports: [
    ConfigModule.forRoot({
      load: [configuration],
    }),
  ],
})
export class AppModule {}

ConfigModule 注冊一個 ConfigService ,并將其導(dǎo)出為在其他消費模塊中可見。此外,我們使用 useValue 語法(參見自定義提供程序)來傳遞到 .env 文件的路徑。此路徑將根據(jù) NODE_ENV 環(huán)境變量中包含的實際執(zhí)行環(huán)境而不同(例如,’開發(fā)’、’生產(chǎn)’等)。 > info 注意 分配給load屬性的值是一個數(shù)組,允許您加載多個配置文件 (e.g. load: [databaseConfig, authConfig])

使用 ConfigService

現(xiàn)在您可以簡單地在任何地方注入 ConfigService ,并根據(jù)傳遞的密鑰檢索特定的配置值。 要從 ConfigService 訪問環(huán)境變量,我們需要注入它。因此我們首先需要導(dǎo)入該模塊。與任何提供程序一樣,我們需要將其包含模塊ConfigModule導(dǎo)入到將使用它的模塊中(除非您將傳遞給ConfigModule.forRoot()方法的 options 對象中的isGlobal屬性設(shè)置為true)。 如下所示將其導(dǎo)入功能模塊。

// feature.module.ts
@Module({
  imports: [ConfigModule],
  ...
})

然后我們可以使用標(biāo)準(zhǔn)的構(gòu)造函數(shù)注入:

constructor(private configService: ConfigService) {}

在我們的類中使用它:

要從 ConfigService 訪問環(huán)境變量,我們需要注入它。因此我們首先需要導(dǎo)入該模塊。

// get an environment variable
const dbUser = this.configService.get<string>('DATABASE_USER');
// get a custom configuration value
const dbHost = this.configService.get<string>('database.host');

如上所示,使用configService.get()方法通過傳遞變量名來獲得一個簡單的環(huán)境變量。您可以通過傳遞類型來執(zhí)行 TypeScript 類型提示,如上所示(例如,get<string>(…))。get()方法還可以遍歷一個嵌套的自定義配置對象(通過自定義配置文件創(chuàng)建,如上面的第二個示例所示)。get()方法還接受一個可選的第二個參數(shù),該參數(shù)定義一個默認(rèn)值,當(dāng)鍵不存在時將返回該值,如下所示:

// use "localhost" when "database.host" is not defined
const dbHost = this.configService.get<string>('database.host', 'localhost');

配置命名空間

ConfigModule模塊允許您定義和加載多個自定義配置文件,如上面的自定義配置文件所示。您可以使用嵌套的配置對象來管理復(fù)雜的配置對象層次結(jié)構(gòu),如本節(jié)所示?;蛘?,您可以使用registerAs()函數(shù)返回一個“帶名稱空間”的配置對象,如下所示:

export default registerAs('database', () => ({
  host: process.env.DATABASE_HOST,
  port: process.env.DATABASE_PORT || 5432,
}));

與自定義配置文件一樣,在您的registerAs()工廠函數(shù)內(nèi)部,process.env對象將包含完全解析的環(huán)境變量鍵/值對(帶有.env文件和已定義并已合并的外部定義變量)

注意 registerAs 函數(shù)是從 @nestjs/config 包導(dǎo)出的。

使用forRoot()的load方法載入命名空間的配置,和載入自定義配置文件方法相同:

// config/database.config.ts
import databaseConfig from './config/database.config';

@Module({
  imports: [
    ConfigModule.forRoot({
      load: [databaseConfig],
    }),
  ],
})
export class AppModule {}

然后我們可以使用標(biāo)準(zhǔn)的構(gòu)造函數(shù)注入,并在我們的類中使用它: 現(xiàn)在,要從數(shù)據(jù)庫命名空間獲取host的值,請使用符號.。使用'database'作為屬性名稱的前綴,該屬性名稱對應(yīng)于命名空間的名稱(作為傳遞給registerAs()函數(shù)的第一個參數(shù))

const dbHost = this.configService.get<string>('database.host');

一個合理的替代方案是直接注入'database'的命名空間,我們將從強(qiáng)類型中獲益:

constructor(
  @Inject(databaseConfig.KEY)
  private dbConfig: ConfigType<typeof databaseConfig>,
) {}

注意 ConfigType 函數(shù)是從 @nestjs/config 包導(dǎo)出的。

部分注冊

到目前為止,我們已經(jīng)使用forRoot()方法在根模塊(例如,AppModule)中處理了配置文件。也許您有一個更復(fù)雜的項目結(jié)構(gòu),其中特定于功能的配置文件位于多個不同的目錄中。與在根模塊中加載所有這些文件不同,@nestjs/config包提供了一個稱為部分注冊的功能,它只引用與每個功能模塊相關(guān)聯(lián)的配置文件。使用特性模塊中的forFeature()靜態(tài)方法來執(zhí)行部分注冊,如下所示:

import databaseConfig from './config/database.config';

@Module({
  imports: [ConfigModule.forFeature(databaseConfig)],
})
export class DatabaseModule {}

您可以選擇將 ConfigModule 聲明為全局模塊,而不是在每個模塊中導(dǎo)入 ConfigModule。 > info 警告在某些情況下,您可能需要使用onModuleInit()鉤子通過部分注冊來訪問加載的屬性,而不是在構(gòu)造函數(shù)中。這是因為forFeature()方法是在模塊初始化期間運行的,而模塊初始化的順序是不確定的。如果您以這種方式訪問由另一個模塊在構(gòu)造函數(shù)中加載的值,則配置所依賴的模塊可能尚未初始化。onModuleInit() 方法只在它所依賴的所有模塊被初始化之后運行,因此這種技術(shù)是安全的

Schema驗證

一個標(biāo)準(zhǔn)實踐是如果在應(yīng)用啟動過程中未提供需要的環(huán)境變量或它們不滿足特定的驗證規(guī)則時拋出異常。@nestjs/config包讓我們可以使用Joi npm 包來提供這種類型驗證。使用 Joi,你可以定義一個對象Schema對象并驗證對應(yīng)的JavaScript對象。 Install Joi (and its types, for TypeScript users): 安裝 Joi(Typescript 用戶還需要安裝其類型申明)

$ npm install --save @hapi/joi
$ npm install --save-dev @types/hapi__joi

注意 最新版本的“@hapi/joi”要求您運行 Node v12 或更高版本。對于較老版本的 node,請安裝“v16.1.8”。這主要是在“v17.0.2”發(fā)布之后,它會在構(gòu)建期間導(dǎo)致錯誤。更多信息請參考他們的文檔github issue

現(xiàn)在,我們可以定義一個 Joi 驗證模式,并通過forRoot()方法的options對象的validationSchema屬性傳遞它,如下所示

// app.module.ts
import * as Joi from '@hapi/joi';

@Module({
  imports: [
    ConfigModule.forRoot({
      validationSchema: Joi.object({
        NODE_ENV: Joi.string().valid('development', 'production', 'test', 'provision').default('development'),
        PORT: Joi.number().default(3000),
      }),
    }),
  ],
})
export class AppModule {}

由于我們?yōu)?nbsp;NODE_ENV 和 PORT 設(shè)置了默認(rèn)值,因此如果不在環(huán)境文件中提供這些變量,驗證將不會失敗。然而, 我們需要明確提供 API_AUTH_ENABLED。如果我們的 .env 文件中的變量不是模式( schema )的一部分, 則驗證也會引發(fā)錯誤。此外,Joi 還會嘗試將 env 字符串轉(zhuǎn)換為正確的類型。

默認(rèn)情況下,允許使用未知的環(huán)境變量(其鍵不在模式中出現(xiàn)的環(huán)境變量),并且不會觸發(fā)驗證異常。默認(rèn)情況下,將報告所有驗證錯誤。您可以通過通過forRoot() options 對象的validationOptions鍵傳遞一個 options 對象來更改這些行為。此選項對象可以包含由 Joi 驗證選項提供的任何標(biāo)準(zhǔn)驗證選項屬性。例如,要反轉(zhuǎn)上面的兩個設(shè)置,像這樣傳遞選項:

// app.module.ts
import * as Joi from '@hapi/joi';

@Module({
  imports: [
    ConfigModule.forRoot({
      validationSchema: Joi.object({
        NODE_ENV: Joi.string().valid('development', 'production', 'test', 'provision').default('development'),
        PORT: Joi.number().default(3000),
      }),
      validationOptions: {
        allowUnknown: false,
        abortEarly: true,
      },
    }),
  ],
})
export class AppModule {}

@nestjs/config包使用默認(rèn)設(shè)置:

  • allowUnknown:控制是否允許環(huán)境變量中未知的鍵。默認(rèn)為true。
  • abortEarly:如果為true,在遇到第一個錯誤時就停止驗證;如果為false,返回所有錯誤。默認(rèn)為false。

注意,一旦您決定傳遞validationOptions對象,您沒有顯式傳遞的任何設(shè)置都將默認(rèn)為Joi標(biāo)準(zhǔn)默認(rèn)值(而不是@nestjs/config默認(rèn)值)。例如,如果在自定義validationOptions對象中保留allowUnknowns未指定,它的Joi默認(rèn)值將為false。因此,在自定義對象中指定這兩個設(shè)置可能是最安全的。

自定義 getter 函數(shù)

ConfigService定義了一個通用的get()方法來通過鍵檢索配置值。我們還可以添加getter函數(shù)來啟用更自然的編碼風(fēng)格:

@Injectable()
export class ApiConfigService {
  constructor(private configService: ConfigService) {}

  get isAuthEnabled(): boolean {
    return this.configService.get('AUTH_ENABLED') === 'true';
  }
}

現(xiàn)在我們可以像下面這樣使用getter函數(shù):

// app.service.ts
@Injectable()
export class AppService {
  constructor(apiConfigService: ApiConfigService) {
    if (apiConfigService.isAuthEnabled) {
      // Authentication is enabled
    }
  }
}

擴(kuò)展變量

@nestjs/config包支持環(huán)境變量擴(kuò)展。使用這種技術(shù),您可以創(chuàng)建嵌套的環(huán)境變量,其中一個變量在另一個變量的定義中引用。例如:

APP_URL=mywebsite.com
SUPPORT_EMAIL=support@${APP_URL}

通過這種構(gòu)造,變量SUPPORT_EMAIL解析為support@mywebsite.com。注意${…}語法來觸發(fā)解析變量APP_URL在SUPPORT_EMAIL定義中的值。

提示 對于這個特性,@nestjs/config 包內(nèi)部使用dotenv-expand實現(xiàn)。 使用傳遞給ConfigModule的forRoot()方法的 options 對象中的expandVariables屬性來啟用環(huán)境變量展開,如下所示:

// app.module.ts
@Module({
  imports: [
    ConfigModule.forRoot({
      // ...
      expandVariables: true,
    }),
  ],
})
export class AppModule {}

在main.ts中使用

雖然我們的配置是存儲在服務(wù)中的,但它仍然可以在 main.ts 文件中使用。通過這種方式,您可以使用它來存儲諸如應(yīng)用程序端口或 CORS 主機(jī)之類的變量。

要訪問它,您必須使用app.get()方法,然后是服務(wù)引用:

const configService = app.get(ConfigService);

然后你可以像往常一樣使用它,通過調(diào)用帶有配置鍵的 get 方法:

const port = configService.get('PORT');


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號