应用程序通常运行在不同的环境中。根据环境的不同,应使用不同的配置设置。例如,通常本地环境依赖于特定的数据库凭据,仅对本地数据库实例有效。生产环境将使用一组单独的数据库凭据。由于配置变量发生变化,最佳实践是将配置变量存储在环境中。
外部定义的环境变量通过全局变量在 Node.js 中可见 process.env。我们可以尝试通过在每个环境中单独设置环境变量来解决多环境的问题。这很快就会变得笨拙,特别是在需要轻松模拟和/或更改这些值的开发和测试环境中。
在 Node.js 应用程序中,通常使用.env 文件来保存键值对,其中每个键代表一个特定值,以代表每个环境。在不同的环境中运行应用程序只需交换正确的.env 文件即可。
在 Nest 中使用此技术的一个好方法是创建一个 ConfigModule 公开 ConfigService 加载适当.env 文件的 。虽然您可以选择自己编写这样的模块,但为了方便起见,Nest 提供了@nestjs/config 开箱即用的包。我们将在当前章节中介绍这个包。
安装
要使用 NestJs 提供的开箱即用的配置功能,我们必须先要安装所需插件:
shell
npm i --save @nestjs/config
说明:@nestjs/config
包内部使用了 dotenv。
开始使用
当我们安装好@nestjs/config
插件后,我们就可以在AppModule
中引入ConfigModule
并使用.forRoot()
方法对配置文件进行相应的处理。具体的代码实例如下:
typescript
import { Module } from "@nestjs/common";
import { CommodityModule } from "./module/commodity.module";
import { AccountModule } from "./module/account.module";
import { ConfigModule } from "@nestjs/config";
@Module({
imports: [CommodityModule, AccountModule, ConfigModule.forRoot()],
})
export class AppModule {}
上述的例子中系统会将从项目根目录加载并解析 .env
文件,并且把.env
文件中的键/值对分配给process.env
并对环境变量进行合并,最终会将合并的结果储存在ConfigService
的私有结构中。
上述的例子中我们可以看到我们在 AppModule 中声明了forRoot()
方法,forRoot 主要是会把 ConfigService 注册到系统中,并且 ConfigService 会提供get()
方法来让我们解析和合并配置变量。在合并配置变量的时候总会遇到同名的请求,这时@nestjs/config
会依赖于 dotenv
来解决重名的情况。而解决冲突的优先级如下:
- 运行时环境变量优先
- .env 配置文件
自定义环境文件路径
在 NestJs 中默认情况,应用程序会在根目录下查找.env
的文件作为环境配置文件来加载,假如需要重新制定另外一个配置文件的话,可以在forRoot
方法中使用envFilePath
属性来进行声明,具体的例子如下:
typescript
ConfigModule.forRoot({
envFilePath: ".env.development",
});
您还可以为.env 文件指定多个路径,如下所示:
typescript
ConfigModule.forRoot({
envFilePath: [".env.development.local", ".env.development"],
});
如果在多个文件中找到一个变量,则第一个优先。
禁用环境变量加载
如果您不想加载.env 文件,而是想简单地从运行时环境访问环境变量(与 OS shell 导出一样 export DATABASE_USER=test),请将选项对象的 ignoreEnvFile 属性设置为 true,如下所示:
typescript
ConfigModule.forRoot({
ignoreEnvFile: true,
});
全局使用模块
当你要在其他模块中获取环境配置变量的话,可以在其他模块中导入ConfigModule
(与任何 Nest 模块的标准一样)。或者可以使用isGlobal
属性开启全局属性,这样一旦设置为 true 后,配置文件的参数就会加载到根模块上,就不用再导入其他模块,这样就可以直接使用。开启全局的实例如下:
typescrpt
ConfigModule.forRoot({
isGlobal: true,
});
自定义配置文件
当项目比较大的时候,我们可能会配置很多参数,例如多数据库、缓存、集群等相关的数据。这样把所有相关配置都写在一个文件中不利于后期维护,所以我们可以利用自定义配置文件来返回嵌套配置对象。在 NestJs 中它允许我们按照功能对相关配置进行设置(例如:数据库相关设置)分组,并将相关设置放在不同的文件中进行单独管理。
自定义配置文件导出返回配置对象的工厂函数。配置对象可以是任意嵌套的纯 JavaScript 对象。由于您控制返回的配置对象,因此您可以添加任何所需的逻辑以将值转换为适当的类型、设置默认值等。具体实例如下:
typescript
export default () => ({
port: parseInt(process.env.PORT ? process.env.PORT : "8080", 10) || 3000,
database: {
host: process.env.DATABASE_HOST,
port:
parseInt(
process.env.DATABASE_PORT ? process.env.DATABASE_PORT : "3306",
10
) || 5432,
},
});
加载自定义配置文件,可以在forRoot
方法中进行加载:
typescript
@Module({
imports: [
CommodityModule,
AccountModule,
ConfigModule.forRoot({
load: [configuration],
}),
],
})
export class AppModule {}
注意:分配给属性的值 load 是一个数组,允许您加载多个配置文件(例如 load: [databaseConfig, authConfig])
除了可以使用使用对象的格式外还可以使用YAML
文件来实现配置文件的编写,要使用之前我们必须安装相关的依赖,具体安装依赖如下:
shell
$ npm i js-yaml
$ npm i -D @types/js-yaml
YAML 文件实例:
yaml
http:
host: "localhost"
port: 8080
db:
postgres:
url: "localhost"
port: 5432
database: "yaml-db"
sqlite:
database: "sqlite.db"
加载解析 YAML 文件的 JS 代码如下:
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>;
};
使用 ConfigService
当我们要使用ConfigService
的话,首先需要注入ConfigService
。与其他提供者一样,我们要先把ConfigModule
导入到对应的模块中然后才能使用,除非我们把开启了全局的配置。
具体实例如下:
typescript
// 在CommodityModule中注入ConfigModule
@Global()
@Module({
imports: [ConfigModule],
controllers: [CommodityController],
providers: [CommodityService],
exports: [CommodityService],
})
export class CommodityModule {}
// 在控制器中注入ConfigService
@Controller("commodity")
@UseInterceptors(LoggingInterceptor, TransformInterceptor)
export class CommodityController {
constructor(
private commodityService: CommodityService,
private configService: ConfigService
) {}
@Post("/save")
saveCommodity(@Body() commodity: Commodity) {
console.log(this.configService.get<string>("DATABASE_USER"));
console.log(this.configService.get<string>("DATABASE_PASSWORD"));
console.log(this.configService.get<number>("port"));
this.commodityService.create(commodity);
}
}
如上所示,使用 configService.get() 方法通过传递变量名称来获取一个简单的环境变量。您可以通过传递类型来进行 TypeScript 类型提示,如上所示(例如 get(...))。get() 方法还可以遍历嵌套的自定义配置对象(通过自定义配置文件创建)。
您还可以使用接口作为类型提示来获取整个嵌套的自定义配置对象,具体实例如下:
typescript
interface DatabaseConfig {
host: string;
port: number;
}
const dbConfig = this.configService.get<DatabaseConfig>("database");
const port = dbConfig.port;
get()
方法其实可以传入两个参数,第二个参数是可选的,它的作用主要是定义默认值,当键值不存在的时候就会返回默认值。具体实例如下:
typescript
const dbHost = this.configService.get<string>("database.host", "localhost");
ConfigService 有两个可选的泛型(类型参数)。
- 第一个是帮助防止访问不存在的配置属性。使用方法如下图所示:
typescript
//
interface EnvironmentVariables {
PORT: number;
TIMEOUT: string;
}
constructor(private configService: ConfigService<EnvironmentVariables>) {
const port = this.configService.get('PORT', { infer: true });
const url = this.configService.get('URL', { infer: true }); // URL参数并不属于配置文件中的参数,这里会报错
}
当infer
属性为 true 时,configService 的 get 方法会自动根据接口定义的参数类型进行判断,假如不符合类型的话就会报错。configService 的 get 方法除了能够检测字符串外还可以检测对象的属性类型,例如:
typescript
constructor(private configService: ConfigService<{ database: { host: string } }>) {
const dbHost = this.configService.get('database.host', { infer: true });
}
- 第二个泛型依赖于第一个泛型,充当 TS 类型断言,当设置为 tue 后,TS 会在编写代码的时候进行类型检测,当类型不匹配的时候则会报错。
typescript
constructor(private configService: ConfigService<{ PORT: number }, true>) {
const port = this.configService.get('PORT', { infer: true });
配置命名空间
在上述例子中我们使用的是对象嵌套关系来组织配置的,在 NestJS 中还提供了另外一种方式来帮我们分类管理配置,那就是命名空间
。具体实例如下:
typescript
import { registerAs } from "@nestjs/config";
export default registerAs("database", () => ({
host: process.env.DATABASE_HOST,
port: process.env.DATABASE_PORT || 5432,
}));
使用命名空间来实现自定义配置后,参数的合并规则和加载方式都跟上述的自定义方式保持一致。要获取数据的话,可以使用命名空间名称.对应的属性
来获取。具体的实例如下:
typescript
const dbHost = this.configService.get<string>("database.host");
在上述的例子中我们使用configService
对象来获取配置参数,但是当我们使用命名空间
方式来定义的配置文件的时候我们就可以直接注入命名空间
来获取数据。具体的实例如下:
typescript
@Controller("commodity")
export class CommodityController {
constructor(
private commodityService: CommodityService,
@Inject(databaseConfig.KEY)
private dbConfig: ConfigType<typeof databaseConfig>
) {}
@Post("/save")
saveCommodity(@Body() commodity: Commodity) {
console.log(this.dbConfig.host);
this.commodityService.create(commodity);
}
}
缓存环境变量
由于访问 process.env 可能会很慢,因此您可以设置传递给 ConfigModule.forRoot() 的选项对象的缓存属性,以提高 ConfigService#get 方法在处理 process.env 中存储的变量时的性能。具体设置实例如下:
typescript
ConfigModule.forRoot({
cache: true,
});
部分注册
到目前为止,我们已经在根模块(例如 AppModule)中处理了配置文件和使用 forRoot() 方法。也许您有一个更复杂的项目结构,特定于功能的配置文件位于多个不同的目录中。在 NestJS 中还提供了一种称为部分注册的功能,而不是在根模块中加载所有这些文件,它仅引用与每个功能模块关联的配置文件。如果要使用此功能的话,主要使用 forFeature() 静态方法来执行此部分注册。具体实例如下:
typescript
import databaseConfig from "./config/database.config";
@Module({
imports: [ConfigModule.forFeature(databaseConfig)],
})
export class DatabaseModule {}