Skip to main content

配置

应用程序通常在不同的环境中运行。根据环境的不同,应使用不同的配置设置。 例如,通常本地环境依赖于特定的数据库凭据,仅对本地数据库实例有效。 生产环境将使用一组单独的数据库凭据。 由于配置变量可能会更改,最佳实践是将配置变量存储在环境中。

在Node.js中,外部定义的环境变量通过 process.env 全局变量可见。 我们可以尝试通过在每个环境中分别设置环境变量来解决多环境的问题。 这可能会很难处理,特别是在开发和测试环境中,其中这些值需要轻松模拟和/或更改。

在Node.js应用程序中,通常使用.env文件,其中包含键值对,其中每个键表示特定的值, 以表示每个环境。然后,只需替换正确的.env文件,就可以在不同的环境中运行应用程序。

在Nest中使用这种技术的一个好方法是创建一个ConfigModule,该模块公开一个ConfigService, 该服务加载适当的.env文件。虽然您可以选择自己编写这样的模块,但为了方便, Nest提供了@nestjs/config包,我们将在当前章节中介绍这个包。

安装

要开始使用它,我们首先安装所需的依赖项。

npm i --save @nestjs/config
tip

@nestjs/config包内部使用dotenv

note

@nestjs/config需要TypeScript 4.1或更高版本

入门

安装过程完成后,我们可以导入 ConfigModule。 通常,我们将其导入到根 AppModule 中,并使用 .forRoot() 静态方法控制其行为。 在此步骤中,环境变量键/值对将被解析和解决。 稍后,我们将看到在其他功能模块中如何访问 ConfigModuleConfigService 类的几个选项。

app.module.ts
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,它使用该软件包的规则来解决环境变量名称的冲突。 当一个键既存在于运行时环境中作为环境变量(例如,通过操作系统 shell 导出,如 export DATABASE_USER=test), 又存在于 .env 文件中时,运行时环境变量优先。

一个示例的 .env 文件看起来像这样:

DATABASE_USER=test
DATABASE_PASSWORD=test

自定义 .env 文件路径

默认情况下,该软件包在应用程序的根目录中查找 .env 文件。要指定 .env 文件的其他路径, 请设置传递给 forRoot() 的(可选的)options 对象的 envFilePath 属性,如下所示:

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

您还可以像这样指定多个 .env 文件的路径:

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

如果在多个文件中找到一个变量,则第一个文件优先。

禁用 env 变量加载

如果不想加载 .env 文件,而是希望直接从运行时环境中访问环境变量(与通过操作系统 shell 导出的环境变量一样, 例如 export DATABASE_USER=test),请将 options 对象的 ignoreEnvFile 属性设置为 true, 如下所示:

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

全局使用模块

当您想在其他模块中使用 ConfigModule 时,您需要导入它(与任何 Nest 模块一样)。 或者,通过将 options 对象的 isGlobal 属性设置为 true,将其声明为全局模块,如下所示。 在这种情况下,一旦在根模块中加载了 ConfigModule(例如,AppModule),就不需要在其他模块中导入它。

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

自定义配置文件

对于更复杂的项目,您可以使用自定义配置文件返回嵌套的配置对象。 这允许您按功能(例如,与数据库相关的设置)对相关的配置设置进行分组, 并将相关的设置存储在单独的文件中,以帮助独立管理它们。

自定义配置文件导出一个返回配置对象的工厂函数。配置对象可以是任意嵌套的普通 JavaScript 对象。 process.env 对象将包含完全解析的环境变量键/值对(与上面描述的 .env 文件和外部定义的变量解析和合并一样)。 由于您控制返回的配置对象,因此可以添加任何必需的逻辑以将值转换为适当的类型,设置默认值等。例如:

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
}
});

我们使用 options 对象传递给 ConfigModule.forRoot() 方法的 load 属性加载此文件:

import configuration from './config/configuration';

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

赋值给 load 属性的值是一个数组,允许您加载多个配置文件 (例如 load: [databaseConfig, authConfig]

使用自定义配置文件,我们还可以管理自定义文件,如 YAML 文件。以下是使用 YAML 格式的配置的示例:

http:
host: 'localhost'
port: 8080

db:
postgres:
url: 'localhost'
port: 5432
database: 'yaml-db'

sqlite:
database: 'sqlite.db'

要读取和解析 YAML 文件,我们可以利用 js-yaml 软件包。

npm i js-yaml
npm i -D @types/js-yaml

软件包安装后,我们使用 yaml#load 函数加载我们刚刚创建的 YAML 文件。

config/configuration.ts
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>;
};
note

Nest CLI 在构建过程中不会自动将“资产”(非 TS 文件)移动到 dist 文件夹中。 为了确保您的 YAML 文件被复制,您必须在 nest-cli.json 文件的 compilerOptions#assets 对象中指定这一点。 例如,如果 config 文件夹与 src 文件夹位于同一级别,则添加 compilerOptions#assets, 并设置值 "assets": [{"include": "../config/*.yaml", "outDir": "./dist/config"}]。 在这里阅读更多信息。

使用 ConfigService

要从 ConfigService 中访问配置值,首先需要注入 ConfigService。 与任何提供者一样,我们需要将包含它的模块(ConfigModule)导入到将使用它的模块中 (除非将 options 对象传递给 ConfigModule.forRoot() 方法并将其 isGlobal 属性设置为 true)。 在功能模块中导入它如下所示。

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

然后我们可以使用标准构造函数注入它:

constructor(private configService: ConfigService) {}
tip

ConfigService 是从 @nestjs/config 软件包中导入的。

并在我们的类中使用它:

// 获取环境变量
const dbUser = this.configService.get<string>('DATABASE_USER');

// 获取自定义配置值
const dbHost = this.configService.get<string>('database.host');

如上所示,使用 configService.get() 方法通过传递变量名称获取简单的环境变量。 可以通过传递类型来进行 TypeScript 类型提示,如上所示(例如,get<string>(...))。 get() 方法还可以遍历通过 Custom configuration file 创建的嵌套自定义配置对象, 如上面的第二个示例所示。

您还可以使用接口作为类型提示获取整个嵌套的自定义配置对象:

interface DatabaseConfig {
host: string;
port: number;
}

const dbConfig = this.configService.get<DatabaseConfig>('database');

// 现在您可以使用 `dbConfig.port` 和 `dbConfig.host`
const port = dbConfig.port;

get() 方法还接受一个可选的第二个参数,定义当键不存在时返回的默认值,如下所示:

// 在未定义 "database.host" 时使用 "localhost"
const dbHost = this.configService.get<string>('database.host', 'localhost');

ConfigService 有两个可选的泛型(类型参数)。第一个用于帮助防止访问不存在的配置属性。如下所示使用它:

interface EnvironmentVariables {
PORT: number;
TIMEOUT: string;
}

// 代码中的某处
constructor(private configService: ConfigService<EnvironmentVariables>) {
const port = this.configService.get('PORT', { infer: true });

// TypeScript 错误:这是无效的,因为 EnvironmentVariables 中未定义 URL 属性
const url = this.configService.get('URL', { infer: true });
}

使用 infer 属性设置为 true 时,ConfigService#get 方法将根据接口自动推断属性类型, 因此,例如,typeof port === "number"(如果没有使用 TypeScript 的 strictNullChecks 标志) 因为 EnvironmentVariables 接口中 PORT 的类型为 number

此外,使用 infer 功能,即使使用点符号表示法,也可以推断嵌套自定义配置对象的属性类型,如下所示:

constructor(private configService: ConfigService<{ database: { host: string } }>) {
const dbHost = this.configService.get('database.host', { infer: true })!;
// typeof dbHost === "string" |
// +--> 非空断言运算符
}

第二个泛型依赖于第一个,充当类型断言,以消除 ConfigService 的方法在 strictNullChecks 打开时可能返回的所有 undefined 类型。 例如:

// ...
constructor(private configService: ConfigService<{ PORT: number }, true>) {
// ^^^^
const port = this.configService.get('PORT', { infer: true });
// ^^^ port 的类型将是 'number',因此您不再需要 TS 类型断言了
}

配置命名空间

ConfigModule 允许您定义和加载多个自定义配置文件,如上文的 Custom configuration files 所示。 您可以使用嵌套配置对象管理复杂的配置对象层次结构,就像该部分中所示的那样。 或者,您可以使用 registerAs() 函数返回一个“命名空间”配置对象,如下所示:

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

与自定义配置文件一样,在 registerAs() 工厂函数中, process.env 对象将包含完全解析的环境变量键/值对(通过如上所述解析和合并的 .env 文件和外部定义的变量)。

tip

registerAs 函数是从@nestjs/config 软件包中导出的。

使用 forRoot() 方法的 options 对象的 load 属性加载带有命名空间的配置, 方式与加载自定义配置文件相同:

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

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

现在,要从database命名空间获取 host 值,请使用点表示法。 使用 'database' 作为属性名称的前缀,对应于 registerAs() 函数的第一个参数中的命名空间名称:

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

一个合理的替代方法是直接注入数据库命名空间。这使我们能够受益于强类型:

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

ConfigType 是从 @nestjs/config 软件包中导出的。

缓存环境变量

由于访问 process.env 可能很慢,您可以将 options 对象的 cache 属性设置为 true, 以提高 ConfigService#get 方法对存储在 process.env 中的变量的性能。

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

部分注册

到目前为止,我们在根模块(例如,AppModule)中处理了配置文件,使用 forRoot() 方法。 也许您有一个更复杂的项目结构,具有位于多个不同目录中的特定功能的配置文件。 与其在根模块中加载所有这些文件,@nestjs/config 软件包提供了一种称为部分注册的功能, 该功能仅引用与每个功能模块相关联的配置文件。在功能模块中使用 forFeature() 静态方法执行此部分注册, 如下所示:

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

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

在某些情况下,您可能需要在 onModuleInit() 钩子中访问通过部分注册加载的属性,而不是在构造函数中。 这是因为 forFeature() 方法在模块初始化期间运行,模块初始化的顺序是不确定的。 如果在构造函数中通过另一个模块访问以这种方式加载的值,配置所依赖的模块可能尚未初始化。 onModuleInit() 方法仅在其依赖的所有模块都初始化后才运行,因此该技术是安全的。

模式验证

在应用程序启动期间,如果未提供所需的环境变量或者它们不符合某些验证规则,通常会引发异常。 @nestjs/config 软件包提供了两种不同的方法来执行此操作:

  1. Joi 内置验证器。使用 Joi,您可以定义对象模式并针对 JavaScript 对象进行验证。
  2. 一个接受环境变量作为输入的自定义 validate() 函数。

要使用 Joi,我们必须安装 Joi 软件包:

npm install --save joi

现在,我们可以定义一个 Joi 验证模式,并通过 forRoot() 方法的 options 对象的 validationSchema 属性传递它, 如下所示:

app.module.ts
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 {}

默认情况下,所有模式键都被视为可选的。在这里,我们为 NODE_ENVPORT 设置了默认值, 如果在环境(.env 文件或进程环境)中未提供这些变量,将使用这些默认值。 或者,我们可以使用 required() 验证方法,要求在环境中必须定义一个值。 在这种情况下,如果我们在环境中没有提供变量,验证步骤将引发异常。 有关如何构建验证模式的详细信息,请参见 Joi 验证方法

默认情况下,允许未知的环境变量(其键在模式中不存在)并且不触发验证异常。 默认情况下,报告所有验证错误。您可以通过通过 forRoot() 选项对象的 validationOptions 键传递选项对象来更改这些行为。 此选项对象可以包含 Joi 验证选项提供的任何标准验证选项属性。 例如,要颠倒上面的两个设置,传递以下选项:

app.module.ts
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:如果为 true,在第一个错误时停止验证;如果为 false,则返回所有错误。默认为false

请注意,一旦决定传递 validationOptions 对象, 您未显式传递的任何设置都将默认为 Joi 的标准默认值(而不是 @nestjs/config 的默认值)。 例如,如果在自定义 validationOptions 对象中未指定 allowUnknowns,则它将具有 Joi 默认值 false。 因此,最好在自定义对象中指定这两个设置。

自定义验证函数

或者,您可以指定一个同步validate函数,该函数接受一个包含环境变量(来自 env 文件和进程)的对象, 并返回一个包含经过验证的环境变量的对象,以便在需要时进行转换/变异。如果函数抛出错误,它将阻止应用程序引导。

在此示例中,我们将使用 class-transformerclass-validator软件包。 首先,我们必须定义:

  • 一个带有验证约束的类,
  • 一个 validate 函数,该函数利用 plainToInstancevalidateSync 函数。
env.validation.ts
import { plainToInstance } 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 = plainToInstance(
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
import { validate } from './env.validation';

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

自定义 getter 函数

ConfigService 定义了一个通用的 get() 方法,用于通过键检索配置值。 我们还可以添加 getter 函数以启用更自然的编码风格:

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

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

现在我们可以像下面这样使用 getter 函数:

env.validation.ts
@Injectable()
export class AppService {
constructor(apiConfigService: ApiConfigService) {
if (apiConfigService.isAuthEnabled) {
// 认证已启用
}
}
}

环境变量加载钩子

如果模块配置依赖于环境变量,并且这些变量是从 .env 文件加载的, 您可以使用 ConfigModule.envVariablesLoaded 钩子来确保在与 process.env 对象交互之前加载了该文件, 如以下示例所示:

export async function getStorageModule() {
await ConfigModule.envVariablesLoaded;
return process.env.STORAGE === 'S3' ? S3StorageModule : DefaultStorageModule;
}

这种构造保证在 ConfigModule.envVariablesLoaded Promise 解析后,所有配置变量都已加载。

条件模块配置

有时您可能希望根据环境变量条件加载模块,并在 .env文件中指定条件。 幸运的是,@nestjs/config 提供了一个 ConditionalModule,允许您做到这一点。

@Module({
imports: [ConfigModule.forRoot(), ConditionalModule.registerWhen(FooModule, 'USE_FOO')],
})
export class AppModule {}

上述模块仅在 .env 文件中对于 env 变量 USE_FOO 没有 false 值时加载 FooModule。 您还可以自己传递自定义条件,即接收 process.env 引用并返回一个 boolean 的函数, 供 ConditionalModule 处理:

@Module({
imports: [ConfigModule.forRoot(), ConditionalModule.registerWhen(FooBarModule, (env: NodeJS.ProcessEnv) => !!env['foo'] && !!env['bar'])],
})
export class AppModule {}

重要的是确保在使用 ConditionalModule 时,应用程序中还加载了 ConfigModule, 以便正确引用和使用 ConfigModule.envVariablesLoaded 钩子。 如果在 5 秒或由用户在 registerWhen 方法的第三个选项参数中设置的毫秒超时内未将该钩子翻转为 true, 则 ConditionalModule 将引发错误,Nest 将中止启动应用程序。

可扩展变量

@nestjs/config 软件包支持环境变量扩展。 通过此技术,您可以创建嵌套的环境变量,其中一个变量在另一个的定义中被引用。例如:

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

使用这种构造,变量 SUPPORT_EMAIL 解析为 'support@mywebsite.com'。 请注意使用 ${...} 语法触发在 SUPPORT_EMAIL 定义内解析变量 APP_URL 的值。

tip

对于此功能,@nestjs/config 软件包内部使用 dotenv-expand

使用 expandVariables 属性在传递给 ConfigModuleforRoot() 方法的选项对象中启用环境变量扩展, 如下所示:

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

main.ts 中使用

虽然我们的配置存储在服务中,但仍然可以在 main.ts 文件中使用它。 这样,您可以使用它来存储诸如应用程序端口或 CORS 主机之类的变量。

要访问它,必须使用 app.get() 方法,然后是服务引用:

const configService = app.get(ConfigService);

然后可以像往常一样使用它,通过使用带有配置键的 get 方法:

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