NestJS 配置
应用程序通常在不同的环境中运行。根据环境的不同,应该使用不同的配置设置。例如,通常本地环境依赖于特定的数据库凭据,仅对本地 DB 实例有效。生产环境将使用一组单独的 DB 凭据。由于配置变量会更改,所以最佳实践是将配置变量存储在环境中。
外部定义的环境变量通过 process.env global 在 Node.js 内部可见。 我们可以尝试通过在每个环境中分别设置环境变量来解决多个环境的问题。 这会很快变得难以处理,尤其是在需要轻松模拟或更改这些值的开发和测试环境中。
在 Node.js 应用程序中,通常使用 .env 文件,其中包含键值对,其中每个键代表一个特定的值,以代表每个环境。 在不同的环境中运行应用程序仅是交换正确的.env 文件的问题。
在 Nest 中使用这种技术的一个好方法是创建一个 ConfigModule ,它暴露一个 ConfigService ,根据 $NODE_ENV 环境变量加载适当的 .env 文件。虽然您可以选择自己编写这样的模块,但为方便起见,Nest 提供了开箱即用的@ nestjs/config软件包。 我们将在本章中介绍该软件包。
安装
要开始使用它,我们首先安装所需的依赖项。
$ npm i --save @nestjs/config
注意 @nestjs/config 内部使用 dotenv 实现。
开始使用
安装完成之后,我们需要导入ConfigModule模块。通常,我们在根模块AppModule中导入它,并使用.forRoot()静态方法导入它的配置。
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,它使用该包的规则来处理冲突的环境变量名称。当一个键同时作为环境变量(例如,通过操作系统终端如export DATABASE_USER=test导出)存在于运行环境中以及.env文件中时,以运行环境变量优先。
一个样例.env文件看起来像这样:
DATABASE_USER=test
DATABASE_PASSWORD=test
自定义 env 文件路径
默认情况下,程序在应用程序的根目录中查找.env文件。 要为.env文件指定另一个路径,请配置forRoot()的配置对象 envFilePath 属性(可选),如下所示:
ConfigModule.forRoot({
envFilePath: '.development.env',
});
您还可以像这样为.env 文件指定多个路径:
ConfigModule.forRoot({
envFilePath: ['.env.development.local', '.env.development'],
});
如果在多个文件中发现同一个变量,则第一个变量优先。
禁止加载环境变量
如果您不想加载.env 文件,而是想简单地从运行时环境访问环境变量(如 OS 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
}
});
我们使用传递给ConfigModule.forRoot()方法的 options 对象的load属性来加载这个文件:
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])
使用 ConfigService
现在您可以简单地在任何地方注入 ConfigService ,并根据传递的密钥检索特定的配置值。 要从 ConfigService 访问环境变量,我们需要注入它。因此我们首先需要导入该模块。与任何提供程序一样,我们需要将其包含模块ConfigModule导入到将使用它的模块中(除非您将传递给ConfigModule.forRoot()方法的 options 对象中的isGlobal属性设置为true)。 如下所示将其导入功能模块。
// feature.module.ts
@Module({
imports: [ConfigModule],
...
})
然后我们可以使用标准的构造函数注入:
constructor(private configService: ConfigService) {}
在我们的类中使用它:
要从 ConfigService 访问环境变量,我们需要注入它。因此我们首先需要导入该模块。
// 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()方法还接受一个可选的第二个参数,该参数定义一个默认值,当键不存在时将返回该值,如下所示:
// use "localhost" when "database.host" is not defined
const dbHost = this.configService.get<string>('database.host', 'localhost');
配置命名空间
ConfigModule模块允许您定义和加载多个自定义配置文件,如上面的自定义配置文件所示。您可以使用嵌套的配置对象来管理复杂的配置对象层次结构,如本节所示。或者,您可以使用registerAs()函数返回一个“带名称空间”的配置对象,如下所示:
export default registerAs('database', () => ({
host: process.env.DATABASE_HOST,
port: process.env.DATABASE_PORT || 5432,
}));
与自定义配置文件一样,在您的registerAs()工厂函数内部,process.env对象将包含完全解析的环境变量键/值对(带有.env文件和已定义并已合并的外部定义变量)
注意 registerAs 函数是从 @nestjs/config 包导出的。
使用forRoot()的load方法载入命名空间的配置,和载入自定义配置文件方法相同:
// config/database.config.ts
import databaseConfig from './config/database.config';
@Module({
imports: [
ConfigModule.forRoot({
load: [databaseConfig],
}),
],
})
export class AppModule {}
然后我们可以使用标准的构造函数注入,并在我们的类中使用它: 现在,要从数据库命名空间获取host的值,请使用符号.。使用'database'作为属性名称的前缀,该属性名称对应于命名空间的名称(作为传递给registerAs()函数的第一个参数)
const dbHost = this.configService.get<string>('database.host');
一个合理的替代方案是直接注入'database'的命名空间,我们将从强类型中获益:
constructor(
@Inject(databaseConfig.KEY)
private dbConfig: ConfigType<typeof databaseConfig>,
) {}
注意 ConfigType 函数是从 @nestjs/config 包导出的。
部分注册
到目前为止,我们已经使用forRoot()方法在根模块(例如,AppModule)中处理了配置文件。也许您有一个更复杂的项目结构,其中特定于功能的配置文件位于多个不同的目录中。与在根模块中加载所有这些文件不同,@nestjs/config包提供了一个称为部分注册的功能,它只引用与每个功能模块相关联的配置文件。使用特性模块中的forFeature()静态方法来执行部分注册,如下所示:
import databaseConfig from './config/database.config';
@Module({
imports: [ConfigModule.forFeature(databaseConfig)],
})
export class DatabaseModule {}
您可以选择将 ConfigModule 声明为全局模块,而不是在每个模块中导入 ConfigModule。 > info 警告在某些情况下,您可能需要使用onModuleInit()钩子通过部分注册来访问加载的属性,而不是在构造函数中。这是因为forFeature()方法是在模块初始化期间运行的,而模块初始化的顺序是不确定的。如果您以这种方式访问由另一个模块在构造函数中加载的值,则配置所依赖的模块可能尚未初始化。onModuleInit() 方法只在它所依赖的所有模块被初始化之后运行,因此这种技术是安全的
Schema验证
一个标准实践是如果在应用启动过程中未提供需要的环境变量或它们不满足特定的验证规则时抛出异常。@nestjs/config包让我们可以使用Joi npm 包来提供这种类型验证。使用 Joi,你可以定义一个对象Schema对象并验证对应的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”发布之后,它会在构建期间导致错误。更多信息请参考他们的文档和github issue
现在,我们可以定义一个 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 {}
由于我们为 NODE_ENV 和 PORT 设置了默认值,因此如果不在环境文件中提供这些变量,验证将不会失败。然而, 我们需要明确提供 API_AUTH_ENABLED。如果我们的 .env 文件中的变量不是模式( schema )的一部分, 则验证也会引发错误。此外,Joi 还会尝试将 env 字符串转换为正确的类型。
默认情况下,允许使用未知的环境变量(其键不在模式中出现的环境变量),并且不会触发验证异常。默认情况下,将报告所有验证错误。您可以通过通过forRoot() options 对象的validationOptions键传递一个 options 对象来更改这些行为。此选项对象可以包含由 Joi 验证选项提供的任何标准验证选项属性。例如,要反转上面的两个设置,像这样传递选项:
// 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包使用默认设置:
- allowUnknown:控制是否允许环境变量中未知的键。默认为true。
- abortEarly:如果为true,在遇到第一个错误时就停止验证;如果为false,返回所有错误。默认为false。
注意,一旦您决定传递validationOptions对象,您没有显式传递的任何设置都将默认为Joi标准默认值(而不是@nestjs/config默认值)。例如,如果在自定义validationOptions对象中保留allowUnknowns未指定,它的Joi默认值将为false。因此,在自定义对象中指定这两个设置可能是最安全的。
自定义 getter 函数
ConfigService定义了一个通用的get()方法来通过键检索配置值。我们还可以添加getter函数来启用更自然的编码风格:
@Injectable()
export class ApiConfigService {
constructor(private configService: ConfigService) {}
get isAuthEnabled(): boolean {
return this.configService.get('AUTH_ENABLED') === 'true';
}
}
现在我们可以像下面这样使用getter函数:
// app.service.ts
@Injectable()
export class AppService {
constructor(apiConfigService: ApiConfigService) {
if (apiConfigService.isAuthEnabled) {
// Authentication is enabled
}
}
}
扩展变量
@nestjs/config包支持环境变量扩展。使用这种技术,您可以创建嵌套的环境变量,其中一个变量在另一个变量的定义中引用。例如:
APP_URL=mywebsite.com
SUPPORT_EMAIL=support@${APP_URL}
通过这种构造,变量SUPPORT_EMAIL解析为[email protected]。注意${…}语法来触发解析变量APP_URL在SUPPORT_EMAIL定义中的值。
提示 对于这个特性,@nestjs/config 包内部使用dotenv-expand实现。 使用传递给ConfigModule的forRoot()方法的 options 对象中的expandVariables属性来启用环境变量展开,如下所示:
// 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');
更多建议: