## 配置
應用程序通常在不同的**環境**中運行。根據環境的不同,應該使用不同的配置設置。例如,通常本地環境依賴于特定的數據庫憑據,僅對本地 DB 實例有效。生產環境將使用一組單獨的 DB 憑據。由于配置變量會更改,所以最佳實踐是將[配置變量](https://12factor.net/config)存儲在環境中。
外部定義的環境變量通過 `process.env global` 在` Node.js` 內部可見。 我們可以嘗試通過在每個環境中分別設置環境變量來解決多個環境的問題。 這會很快變得難以處理,尤其是在需要輕松模擬或更改這些值的開發和測試環境中。
在 `Node.js` 應用程序中,通常使用 `.env` 文件,其中包含鍵值對,其中每個鍵代表一個特定的值,以代表每個環境。 在不同的環境中運行應用程序僅是交換正確的`.env` 文件的問題。
在 `Nest` 中使用這種技術的一個好方法是創建一個 `ConfigModule` ,它暴露一個 `ConfigService` ,根據 `$NODE_ENV` 環境變量加載適當的 `.env` 文件。雖然您可以選擇自己編寫這樣的模塊,但為方便起見,Nest 提供了開箱即用的`@ nestjs/config`軟件包。 我們將在本章中介紹該軟件包。
### 安裝
要開始使用它,我們首先安裝所需的依賴項。
```bash
$ npm i --save @nestjs/config
```
> **注意** `@nestjs/config` 內部使用 [dotenv](https://github.com/motdotla/dotenv) 實現。
> **筆記**`@nestjs/config`需要 TypeScript 4.1 或更高版本。
### 開始使用
安裝完成之后,我們需要導入`ConfigModule`模塊。通常,我們在根模塊`AppModule`中導入它,并使用`.forRoot()`靜態方法導入它的配置。
```typescript
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [ConfigModule.forRoot()],
})
export class AppModule {}
```
上述代碼將從默認位置(項目根目錄)載入并解析一個`.env`文件,從`.env`文件和`process.env`合并環境變量鍵值對,并將結果存儲到一個可以通過`ConfigService`訪問的私有結構。`forRoot()`方法注冊了`ConfigService`提供者,后者提供了一個`get()`方法來讀取這些解析/合并的配置變量。由于`@nestjs/config`依賴[dotenv](https://github.com/motdotla/dotenv),它使用該包的規則來處理沖突的環境變量名稱。當一個鍵同時作為環境變量(例如,通過操作系統終端如`export DATABASE_USER=test`導出)存在于運行環境中以及`.env`文件中時,以運行環境變量優先。
一個樣例`.env`文件看起來像這樣:
```json
DATABASE_USER=test
DATABASE_PASSWORD=test
```
#### 自定義 env 文件路徑
默認情況下,程序在應用程序的根目錄中查找`.env`文件。 要為`.env`文件指定另一個路徑,請配置`forRoot()`的配置對象 envFilePath 屬性(可選),如下所示:
```typescript
ConfigModule.forRoot({
envFilePath: '.development.env',
});
```
您還可以像這樣為.env 文件指定多個路徑:
```typescript
ConfigModule.forRoot({
envFilePath: ['.env.development.local', '.env.development'],
});
```
如果在多個文件中發現同一個變量,則第一個變量優先。
#### 禁止加載環境變量
如果您不想加載.env 文件,而是想簡單地從運行時環境訪問環境變量(如 OS shell 導出,例如`export DATABASE_USER = test`),則將`options`對象的`ignoreEnvFile`屬性設置為`true`,如下所示 :
```typescript
ConfigModule.forRoot({
ignoreEnvFile: true,
});
```
#### 全局使用
當您想在其他模塊中使用`ConfigModule`時,需要將其導入(這是任何 Nest 模塊的標準配置)。 或者,通過將`options`對象的`isGlobal`屬性設置為`true`,將其聲明為[全局模塊](https://docs.nestjs.com/modules#global-modules),如下所示。 在這種情況下,將`ConfigModule`加載到根模塊(例如`AppModule`)后,您無需在其他模塊中導入它。
```typescript
ConfigModule.forRoot({
isGlobal: true,
});
```
#### 自定義配置文件
對于更復雜的項目,您可以利用自定義配置文件返回嵌套的配置對象。 這使您可以按功能對相關配置設置進行分組(例如,與數據庫相關的設置),并將相關設置存儲在單個文件中,以幫助獨立管理它們
自定義配置文件導出一個工廠函數,該函數返回一個配置對象。配置對象可以是任意嵌套的普通 JavaScript 對象。`process.env`對象將包含完全解析的環境變量鍵/值對(具有如上所述的`.env`文件和已解析和合并的外部定義變量)。因為您控制了返回的配置對象,所以您可以添加任何必需的邏輯來將值轉換為適當的類型、設置默認值等等。例如:
```typescript
// 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`屬性來加載這個文件:
```typescript
import configuration from './config/configuration';
@Module({
imports: [
ConfigModule.forRoot({
load: [configuration],
}),
],
})
export class AppModule {}
```
`ConfigModule` 注冊一個 `ConfigService` ,并將其導出為在其他消費模塊中可見。此外,我們使用 `useValue` 語法(參見自定義提供程序)來傳遞到 `.env` 文件的路徑。此路徑將根據 `NODE_ENV` 環境變量中包含的實際執行環境而不同(例如,'開發'、'生產'等)。 > info **注意** 分配給`load`屬性的值是一個數組,允許您加載多個配置文件 (e.g. `load: [databaseConfig, authConfig]`)
使用自定義配置文件,我們還可以管理自定義文件,例如 YAML 文件。以下是使用 YAML 格式的配置示例:
~~~yaml
http:
host: 'localhost'
port: 8080
db:
postgres:
url: 'localhost'
port: 5432
database: 'yaml-db'
sqlite:
database: 'sqlite.db'
~~~
要讀取和解析 YAML 文件,我們可以利用該`js-yaml`包。
~~~bash
$ npm i js-yaml
$ npm i -D @types/js-yaml
~~~
安裝包后,我們使用`yaml#load`函數來加載我們剛剛在上面創建的 YAML 文件。
>config/configuration.ts
~~~typescript
import { readFileSync } from 'fs';
import * as yaml from 'js-yaml';
import { join } from 'path';
const YAML_CONFIG_FILENAME = 'config.yaml';
export default () => {
return yaml.load(
readFileSync(join(__dirname, YAML_CONFIG_FILENAME), 'utf8'),
) as Record<string, any>;
};
~~~
> **筆記:** Nest CLI 在構建過程中不會自動將您的“資產”(非 TS 文件)移動到 dist 文件夾。為確保您的 YAML 文件被復制,您必須在 `nest-cli.json` 文件的 `compilerOptions#assets` 對象中指定此項。例如,如果 `config` 文件夾與 `src` 文件夾處于同一級別,則添加值為`assets`的 `compilerOptions`
```
"assets": [{"include": "../config/*.yaml", "outDir": "./dist/config"}]
```
[在這里](https://docs.nestjs.com/cli/monorepo#assets)閱讀更多。
### 使用 `ConfigService`
現在您可以簡單地在任何地方注入 `ConfigService` ,并根據傳遞的密鑰檢索特定的配置值。 要從 `ConfigService` 訪問環境變量,我們需要注入它。因此我們首先需要導入該模塊。與任何提供程序一樣,我們需要將其包含模塊`ConfigModule`導入到將使用它的模塊中(除非您將傳遞給`ConfigModule.forRoot()`方法的 options 對象中的`isGlobal`屬性設置為`true`)。 如下所示將其導入功能模塊。
```typescript
// feature.module.ts
@Module({
imports: [ConfigModule],
...
})
```
然后我們可以使用標準的構造函數注入:
```typescript
constructor(private configService: ConfigService) {}
```
>**提示:**`ConfigService`是從`@nestjs/config`包 中導入的。
在我們的類中使用它:
要從 `ConfigService` 訪問環境變量,我們需要注入它。因此我們首先需要導入該模塊。
```typescript
// 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()`方法通過傳遞變量名來獲得一個簡單的環境變量。您可以通過傳遞類型來執行 TypeScript 類型提示,如上所示(例如,`get<string>(…)`)。`get()`方法還可以遍歷一個嵌套的自定義配置對象(通過自定義配置文件創建,如上面的第二個示例所示)。`get()`方法還接受一個可選的第二個參數,該參數定義一個默認值,當鍵不存在時將返回該值,如下所示:
```typescript
// use "localhost" when "database.host" is not defined
const dbHost = this.configService.get<string>('database.host', 'localhost');
```
`ConfigService`有兩個可選的泛型(類型參數)。第一個是幫助防止訪問不存在的配置屬性。如下所示使用它:
~~~typescript
interface EnvironmentVariables {
PORT: number;
TIMEOUT: string;
}
// somewhere in the code
constructor(private configService: ConfigService<EnvironmentVariables>) {
const port = this.configService.get('PORT', { infer: true });
// TypeScript Error: this is invalid as the URL property is not defined in EnvironmentVariables
const url = this.configService.get('URL', { infer: true });
}
~~~
將` infer` 屬性設置為`true`,`ConfigService#get`方法將根據接口自動推斷屬性類型,例如,`typeof port === "number"`(如果您沒有使用 `TypeScript` 中的 `strictNullChecks` 標志),因為 `PORT` 在 `EnvironmentVariables` 接口中有一個數字類型。
此外,使用推斷功能,您可以推斷嵌套自定義配置對象的屬性的類型,即使使用點表示法,如下所示:
~~~typescript
constructor(private configService: ConfigService<{ database: { host: string } }>) {
const dbHost = this.configService.get('database.host', { infer: true })!;
// typeof dbHost === "string" |
// +--> non-null assertion operator
}
~~~
第二個泛型依賴于第一個泛型,充當類型斷言以消除 `ConfigService·`的方法在 `strictNullChecks` 開啟時可以返回的所有未定義類型。例如:
~~~typescript
// ...
constructor(private configService: ConfigService<{ PORT: number }, true>) {
// ^^^^
const port = this.configService.get('PORT', { infer: true });
// ^^^ The type of port will be 'number' thus you don't need TS type assertions anymore
}
~~~
#### 配置命名空間
`ConfigModule`模塊允許您定義和加載多個自定義配置文件,如上面的自定義配置文件所示。您可以使用嵌套的配置對象來管理復雜的配置對象層次結構,如本節所示。或者,您可以使用`registerAs()`函數返回一個“帶名稱空間”的配置對象,如下所示:
```typescript
export default registerAs('database', () => ({
host: process.env.DATABASE_HOST,
port: process.env.DATABASE_PORT || 5432,
}));
```
與自定義配置文件一樣,在您的`registerAs()`工廠函數內部,`process.env`對象將包含完全解析的環境變量鍵/值對(帶有`.env`文件和已定義并已合并的外部定義變量)
> **注意** `registerAs` 函數是從 `@nestjs/config` 包導出的。
使用`forRoot()`的`load`方法載入命名空間的配置,和載入自定義配置文件方法相同:
```typescript
// config/database.config.ts
import databaseConfig from './config/database.config';
@Module({
imports: [
ConfigModule.forRoot({
load: [databaseConfig],
}),
],
})
export class AppModule {}
```
然后我們可以使用標準的構造函數注入,并在我們的類中使用它: 現在,要從數據庫命名空間獲取`host`的值,請使用符號`.`。使用`'database'`作為屬性名稱的前綴,該屬性名稱對應于命名空間的名稱(作為傳遞給`registerAs()`函數的第一個參數)
```typescript
const dbHost = this.configService.get<string>('database.host');
```
一個合理的替代方案是直接注入`'database'`的命名空間,我們將從強類型中獲益:
```typescript
constructor(
@Inject(databaseConfig.KEY)
private dbConfig: ConfigType<typeof databaseConfig>,
) {}
```
> **注意** `ConfigType` 函數是從 `@nestjs/config` 包導出的。
## 緩存環境變量[#](#cache-environment-variables)
由于訪問 process.env 可能很慢,您可以設置傳遞給 `ConfigModule.forRoot()` 的選項對象的緩存屬性,以提高 `ConfigService#get` 方法在處理存儲在 `process.env` 中的變量時的性能。
~~~typescript
ConfigModule.forRoot({
cache: true,
});
~~~
## 部分注冊
到目前為止,我們已經使用`forRoot()`方法在根模塊(例如,`AppModule`)中處理了配置文件。也許您有一個更復雜的項目結構,其中特定于功能的配置文件位于多個不同的目錄中。與在根模塊中加載所有這些文件不同,`@nestjs/config`包提供了一個稱為部分注冊的功能,它只引用與每個功能模塊相關聯的配置文件。使用特性模塊中的`forFeature()`靜態方法來執行部分注冊,如下所示:
```typescript
import databaseConfig from './config/database.config';
@Module({
imports: [ConfigModule.forFeature(databaseConfig)],
})
export class DatabaseModule {}
```
> 您可以選擇將 `ConfigModule` 聲明為全局模塊,而不是在每個模塊中導入 `ConfigModule`。
>**警告**:在某些情況下,您可能需要使用`onModuleInit()`鉤子通過部分注冊來訪問加載的屬性,而不是在構造函數中。這是因為`forFeature()`方法是在模塊初始化期間運行的,而模塊初始化的順序是不確定的。如果您以這種方式訪問由另一個模塊在構造函數中加載的值,則配置所依賴的模塊可能尚未初始化。`onModuleInit() `方法只在它所依賴的所有模塊被初始化之后運行,因此這種技術是安全的。
## 架構驗證[#](#schema-validation)
如果未提供所需的環境變量或它們不符合某些驗證規則,則標準做法是在應用程序啟動期間引發異常。 `@nestjs/config` 包支持兩種不同的方式來做到這一點:
* [Joi](https://github.com/sideway/joi)內置驗證器。使用 Joi,您可以定義對象模式并根據它驗證 JavaScript 對象。
* `validate()`將環境變量作為輸入的自定義函數。
要使用 Joi,我們必須安裝 Joi 包:
~~~bash
$ npm install --save joi
~~~
> **注意**最新版本`joi`要求您運行 Node v12 或更高版本。對于舊版本的節點,請安裝`v16.1.8`.這主要是在發布之后`v17.0.2`在構建時導致錯誤。有關詳細信息,請參閱[他們的 17.0.0 發行說明](https://github.com/sideway/joi/issues/2262)。
現在我們可以定義一個 Joi 驗證模式并通過方法的選項對象的`validationSchema`屬性傳遞它`forRoot()`,如下所示:
>app.module.ts
~~~typescript
import * as Joi from '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 {}
~~~
默認情況下,所有模式鍵都被認為是可選的。 在這里,如果我們不在環境(.env 文件或進程環境)中提供這些變量,我們將使用為 `NODE_ENV` 和 `PORT` 設置的默認值。 或者我們可以使用 `required() `驗證方法來要求必須在環境(.env 文件或進程環境)中定義一個值。 在這種情況下,如果我們不在環境中提供變量,驗證步驟將引發異常。 有關如何構建驗證模式的更多信息,請參閱 Joi 驗證方法。[Joi 驗證方法。](https://joi.dev/api/?v=17.3.0#example)
默認情況下,允許未知環境變量(其鍵不存在于模式中的環境變量)并且不會觸發驗證異常。默認情況下,會報告所有驗證錯誤。您可以通過選項對象的`validationOptions`鍵傳遞選項對象來更改這些行為`forRoot()`。這個選項對象可以包含任何由[Joi 驗證選項](https://joi.dev/api/?v=17.3.0#anyvalidatevalue-options)提供的標準驗證選項屬性。例如,要反轉上面的兩個設置,請傳遞如下選項:
>app.module.ts
~~~typescript
import * as Joi from '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`軟件包使用以下默認設置:
* `allowUnknown`: 控制是否允許環境變量中的未知鍵。默認為`true`
* `abortEarly`:如果為真,則在第一個錯誤時停止驗證;如果為 false,則返回所有錯誤。默認為`false`.
>請注意,一旦您決定傳遞一個`validationOptions`對象,您未明確傳遞的任何設置都將默認為`Joi`標準默認值(而不是`@nestjs/config`默認值)。例如,如果您`allowUnknowns`在自定義`validationOptions`對象中未指定,它將具有`Joi`默認值`false`.因此,在您的自定義對象中指定**這兩個設置可能是最安全的。**
#### 自定義驗證功能[#](#custom-validate-function)
或者,您可以指定一個**同步**`validate`函數,該函數接受一個包含環境變量的對象(來自 env 文件和進程)并返回一個包含經過驗證的環境變量的對象,以便您可以在需要時轉換/改變它們。如果函數拋出錯誤,它將阻止應用程序引導。
在此示例中,我們將繼續使用`class-transformer`和`class-validator`包。首先,我們必須定義:
* 具有驗證約束的類,
* 一個使用`plainToClass`and函數的驗證`validateSync`函數。
>env.validation.ts
~~~typescript
import { plainToClass } from 'class-transformer';
import { IsEnum, IsNumber, validateSync } from 'class-validator';
enum Environment {
Development = "development",
Production = "production",
Test = "test",
Provision = "provision",
}
class EnvironmentVariables {
@IsEnum(Environment)
NODE_ENV: Environment;
@IsNumber()
PORT: number;
}
export function validate(config: Record<string, unknown>) {
const validatedConfig = plainToClass(
EnvironmentVariables,
config,
{ enableImplicitConversion: true },
);
const errors = validateSync(validatedConfig, { skipMissingProperties: false });
if (errors.length > 0) {
throw new Error(errors.toString());
}
return validatedConfig;
}
~~~
有了這個,使用該`validate`函數作為 的配置選項`ConfigModule`,如下所示:
>app.module.ts
~~~typescript
import { validate } from './env.validation';
@Module({
imports: [
ConfigModule.forRoot({
validate,
}),
],
})
export class AppModule {}
~~~
## 自定義 getter 函數[#](#custom-getter-functions)
`ConfigService``get()`定義了一個通過鍵檢索配置值的通用方法。我們還可以添加`getter`一些函數來實現更自然的編碼風格:
~~~typescript
@Injectable()
export class ApiConfigService {
constructor(private configService: ConfigService) {}
get isAuthEnabled(): boolean {
return this.configService.get('AUTH_ENABLED') === 'true';
}
}
~~~
現在我們可以使用 getter 函數,如下所示:
>app.service.ts
~~~typescript
@Injectable()
export class AppService {
constructor(apiConfigService: ApiConfigService) {
if (apiConfigService.isAuthEnabled) {
// Authentication is enabled
}
}
}
~~~
#### 可擴展變量[#](#expandable-variables)
該`@nestjs/config`包支持環境變量擴展。使用這種技術,您可以創建嵌套的環境變量,其中一個變量在另一個變量的定義中被引用。例如:
~~~json
APP_URL=mywebsite.com
SUPPORT_EMAIL=support@${APP_URL}
~~~
通過這種結構,變量``SUPPORT_EMAIL `解析為`“support@mywebsite.com”`。請注意使用 `${...}`語法來觸發解析 `SUPPORT_EMAIL` 定義內的變量 `APP_URL` 的值。
> **提示**:對于此功能,`@nestjs/config`包內部使用[dotenv-expand](https://github.com/motdotla/dotenv-expand)。
`expandVariables`使用傳遞給 的`forRoot()`方法的選項對象中的屬性啟用環境變量擴展`ConfigModule`,如下所示:
>app.module.ts
~~~typescript
@Module({
imports: [
ConfigModule.forRoot({
// ...
expandVariables: true,
}),
],
})
export class AppModule {}
~~~
#### 在`main.ts`[中使用#](#using-in-the-maints)
雖然我們的配置存儲在服務中,但它仍然可以在`main.ts`文件中使用。這樣,您可以使用它來存儲變量,例如應用程序端口或 CORS 主機。
要訪問它,您必須使用該`app.get()`方法,然后服務引用之后使用:
~~~typescript
const configService = app.get(ConfigService);
~~~
然后,您可以像往常一樣使用它,`get`方法是使用配置鍵調用該方法:
~~~typescript
const port = configService.get('PORT');
~~~
- 介紹
- 概述
- 第一步
- 控制器
- 提供者
- 模塊
- 中間件
- 異常過濾器
- 管道
- 守衛
- 攔截器
- 自定義裝飾器
- 基礎知識
- 自定義提供者
- 異步提供者
- 動態模塊
- 注入作用域
- 循環依賴
- 模塊參考
- 懶加載模塊
- 應用上下文
- 生命周期事件
- 跨平臺
- 測試
- 技術
- 數據庫
- Mongo
- 配置
- 驗證
- 緩存
- 序列化
- 版本控制
- 定時任務
- 隊列
- 日志
- Cookies
- 事件
- 壓縮
- 文件上傳
- 流式處理文件
- HTTP模塊
- Session(會話)
- MVC
- 性能(Fastify)
- 服務器端事件發送
- 安全
- 認證(Authentication)
- 授權(Authorization)
- 加密和散列
- Helmet
- CORS(跨域請求)
- CSRF保護
- 限速
- GraphQL
- 快速開始
- 解析器(resolvers)
- 變更(Mutations)
- 訂閱(Subscriptions)
- 標量(Scalars)
- 指令(directives)
- 接口(Interfaces)
- 聯合類型
- 枚舉(Enums)
- 字段中間件
- 映射類型
- 插件
- 復雜性
- 擴展
- CLI插件
- 生成SDL
- 其他功能
- 聯合服務
- 遷移指南
- Websocket
- 網關
- 異常過濾器
- 管道
- 守衛
- 攔截器
- 適配器
- 微服務
- 概述
- Redis
- MQTT
- NATS
- RabbitMQ
- Kafka
- gRPC
- 自定義傳輸器
- 異常過濾器
- 管道
- 守衛
- 攔截器
- 獨立應用
- Cli
- 概述
- 工作空間
- 庫
- 用法
- 腳本
- Openapi
- 介紹
- 類型和參數
- 操作
- 安全
- 映射類型
- 裝飾器
- CLI插件
- 其他特性
- 遷移指南
- 秘籍
- CRUD 生成器
- 熱重載
- MikroORM
- TypeORM
- Mongoose
- 序列化
- 路由模塊
- Swagger
- 健康檢查
- CQRS
- 文檔
- Prisma
- 靜態服務
- Nest Commander
- 問答
- Serverless
- HTTP 適配器
- 全局路由前綴
- 混合應用
- HTTPS 和多服務器
- 請求生命周期
- 常見錯誤
- 實例
- 遷移指南
- 發現
- 誰在使用Nest?