Construction | 张小伦的网络日志

给Nest应用加上自定义配置

Posted on:2023-03-16 09:25
    Node.js
    JavaScrtipt

使用飞书机器人串起前端研发流程 一文中提高的飞书机器人初版发布上线,目前运行状况良好。文章末尾提到的程序健壮性,今天给它补上。

在目前的实现中,有一些配置项人工维护在代码中,开发和上线需要来回注释一些代码。其实可以通过增加环境变量和配置文件来解决。在 Node.js 应用程序中,通常使用 .env 文件,保存键值对,其中每个键代表一个特定值,以表示每个环境。在不同的环境中运行应用程序只需交换正确的 .env 文件即可。

过去在使用Koa作为框架开发Node.js 项目时,我习惯在项目中使用不同的yaml文件来设置不同环境的配置,通过process.env 判断环境变量,加载基础配置和对应的环境配置,聚合之后提供给Koa的实例。

|-- apps
|-- configs 
|----| config.base.yml
|----| config.daily.yml
|----| config.pre.yml
|----| config.prod.yml

在Nest中可以创建一个 ConfigModule,它提供了一个 ConfigService 来加载对应的 .env 文件。先安装依赖,然后注入。

npm i --save @nestjs/config
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';

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

上面的代码将从默认位置(项目根目录)加载和解析 .env 文件,将 .env 文件中的键/值对与分配给 process.env 的环境变量合并,并将结果存储在可以通过 ConfigService 访问的私有结构中。 forRoot() 方法注册 ConfigService Provider,这个Provider读取这些分析/合并的配置变量的 get() 方法。 @nestjs/config 依赖于dotenv,所以在处理冲突时也是按照dotenv的规则来解决。当键作为环境变量存在于运行时环境中(例如,通过 OS shell 导出,如 export DATABASE_USER=test )和 .env 文件中时,运行时环境变量优先。示例 .env 文件如下所示:

DATABASE_USER=test
DATABASE_PASSWORD=test

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

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

要注意的是,如果在多个文件中找到变量,则第一个文件优先。通常来说使用.env 来管理不同环境的配置足以。对于更复杂的项目和更复杂的配置,使用Nest提供的自定义配置文件的能力来管理会更加合适一些。

自定义配置文件导出返回配置对象的工厂函数。配置对象可以是任意嵌套的纯 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() 方法的选项对象的 load 属性加载此文件:

import configuration from './config/configuration';

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

export class AppModule {}

在其他模块中访问Config,需要将ConfigService注入到对应的模块。

// webhooks.service.ts
@Module({
  imports: [ConfigModule],
  // ...
})

// webhooks.controller.ts
import { ConfigService } from '@nestjs/config';
...
constructor(private configService: ConfigService) {}
...

// 在其他地方使用
const dbUser = this.configService.get<string>('DATABASE_USER');
const dbHost = this.configService.get<string>('database.host');
const dbHost = this.configService.get<string>('database.host', '设置默认值localhost');

我还是更愿意使用yaml来管理我的配置,虽然需要编写一些额外的代码。

npm i js-yaml
npm i -D @types/js-yaml
// config/configuration.ts
import { readFileSync } from 'fs';
import * as yaml from 'js-yaml';
import { join } from 'path';

const env = process.env.NODE_ENV || 'dev';
const YAML_CONFIG_FILENAME = `config.${env}.yaml`;

export default () => {
  return yaml.load(
    readFileSync(join(__dirname, YAML_CONFIG_FILENAME), 'utf8'),
  ) as Record<string, any>;
};

另外,Nest CLI 在执行编译时,默认不会将非TS类型的文件复制到 dist 目录,所以还需要在nest-cli.json中增加assets的设置。要注意的是,assets配置是相对当前项目的入口src目录。在我的项目中,config文件夹目录是src目录的子目录,所以配置如下:

{
  ...
  "compilerOptions": {
		...
    "assets": [
       "config/*.yaml",
    ],
	  ...
  },
	...
}

在 Nest 中,ConfigModule 也是Module,如前文提到的若想使用,必先导入。若使用配置的Module太多,一个个导入也挺麻烦的。如果想偷懒,可以使用 isGlobal:trueapp.module 中将ConfigModule注册为全局。

ConfigModule.forRoot({
  isGlobal: true,
	load: [configuration]
});