多应用+插件架构,代码干净,二开方便,首家独创一键云编译技术,文档视频完善,免费商用码云13.8K 广告
## 配置 应用程序通常在不同的**环境**中运行。根据环境的不同,应该使用不同的配置设置。例如,通常本地环境依赖于特定的数据库凭据,仅对本地 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'); ~~~