[Nest.js]-多环境配置管理

1,922 阅读4分钟

前言

通常开发环境会包含环境:开发(development)、测试(test)和生产(production)。而每个环境可能需要不同的配置,像数据库连接,密钥等。这些敏感信息不会硬编码在代码里,把这些敏感信息在环境变量中设置,从而提高安全性。随着项目的增长,配置项可能会增加。通过环境配置文件管理,也可以更容易的管理和更新这些配置,所以多环境配置是必要的。

多环境配置常用的两种方案

创建项目

mkdir config-test 
cd ./config-test
pnpm init

  • 安装cross-env
pnpm i cross-env

它可以设置修改跨平台的环境变量,因为在Windows、macOS、Linux 系统在设置环境变量时有不同的方式

  • 在package.json脚本中设置运行命令
...
"scripts": {
  "dev": "cross-env NODE_ENV=development node index.js",
  "prod": "cross-env NODE_ENV=production node index.js"
}
...

dotenv

dotenvdotenv - npm (npmjs.com) 是以键值对形式来去配置,它会默认加载 .env 文件中的环境变量到 process.env

安装dotenv

pnpm i dotenv

在根目录创建.env配置文件

DB_HOST = localhost
DB_PORT = 3306

在index.js里引入并加载

image.png 可以看到打印的信息有包含了.env配置文件里的环境变量,如果是多环境呢? 我们在根目录创建.development.env并添加以下配置

DB_HOST = localhost-dev
DB_PORT = 3306

调整index.js文件中dotenv=>config配置对象
path:可选默认情况下查找.env文件,也可以指定读取的.env文件,也可以传递一个数组指定多个文件路径,如果在多个文件中设置了同一个变量,由左至右,第一个优先级最高

const dotenv = require('dotenv');
const envpath = [`.${process.env.NODE_ENV || 'development'}.env`, '.env'];
'.env',
  dotenv.config({
    path: envpath,
  });
console.log('DB=>', process.env.DB_HOST, process.env.DB_PORT);

image.png 可以看到在.development.env中配置的环境变量就被打印出来,并覆盖了.env文件里的配置

config

configconfig - npm (npmjs.com) 它允许创建多个配置文件,可以根据不同环境加载相应的配置,并对默认(公共)配置覆盖合并\

配置文件通常存放在项目根目录下的 /config 目录中 主配置文件(默认配置)通常命名为 default.json
需要创建特定的配置文件 如 development.jsontest.jsonproduction.json 等。这些配置将覆盖默认配置中的值。

安装config

pnpm i config

json配置文件

在项目根目录中创建config文件夹,新增default.json,production.json 分别添加以下配置信息

// default.json
{
  "DB": {
    "USER": "root",
    "PASSWORD": "root",
    "HOST": "localhost",
    "PORT": 3306
  }
}
// production.json
{
  "DB": {
    "USER": "root",
    "PASSWORD": "root",
    "HOST": "localhost-prod"
  }
}

在index.js中导入config,并打印config中的配置

const config = require('config');
console.log('DB=>', config.get('DB'));

image.png 可以看到default.json和production.json自动做了合并处理

yaml配置文件

config不能直接读取yaml格式文件需要安装js-yaml 这个包,再将config文件夹下json文件改写成yml格式

## default.yml
DB:
  USER: "oot"
  PASSWORD: "root"
  HOST: "localhost"
  PORT: 3306
  TYPE: "yaml"
## production.yml
DB:
  USER: "root"
  PASSWORD: "root"
  HOST: "localhost-prod"

执行pnpm run prod

image.png

在nestjs项目环境中配置

使用Nest CLI创建项目,如果没有安装的,全局安装下@nestjs/cli(先检查node版本是否(>= 12, v13 版本除外))

nest new config-demo-nestjs -p pnpm

同样的,安装cross-env,在package.json配置运行脚本

"scripts": {
    "start:dev": "cross-env NODE_ENV=development nest start --watch",
    "start:prod": "cross-env NODE_ENV=production node dist/main",
  },

官方方案使用 @nestjs/config来配置

安装配置依赖包@nestjs/config

内部使用 dotenv 实现

pnpm install @nestjs/config      

在根目录添加.env, .development.env文件

// .env
DB_HOST = localhost
DB_PORT = 3306
// .development.env
DB_HOST = localhost-dev
DB_USERNAME = root
DB_PASSWORD = example
DB_PORT = 3306

通常来讲这些配置信息需要全局进行配置,我们在AppModule中导入使用,ConfigModule中的forRoot方法(不做任何配置默认读取.env文件)

image.png

import { ConfigModule } from '@nestjs/config';
...

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

AppController中注入依赖

private configService: ConfigService

pnpm start:dev启动项目,访问3000端口,DB_HOST的值被打印出来了 image.png 指定一个或多个文件路径也是可以的,通过ForRoot配置对象里的envFilePath属性来设置

const envFilePath = [`.${process.env.NODE_ENV || 'development'}.env`, '.env'];

image.png 重新启动项目访问3000端口 分别输出了DB_HOST,DB_USERNAME,DB_HOST的值 这里也是前面的配置会覆盖后面的配置 image.png

官方方案 进阶用法

.env里配置满足大多数情况下使用的场景,配置层级简单,如果配置项是层级嵌套的,需要灵活去配置(yaml,json或其他类型的文件)。键值对的形式就不太适用了

在forRoot配置里可以加载自定义配置文件,通过load属性来设置,数组中的每一项是一个函数,支持异步方法,函数的返回值就是配置项

安装js-yaml这个包用于解析yaml格式文件

pnpm install js-yaml

在根目录创建config,并添加yaml文件(同上述中的yaml配置文件,在这里不需要去指定某个文件名)

在src下创建configuration.ts

  1. process.env.NODE_ENV获取当前的环境
  2. path.join获取对应的文件路径
  3. fs.readFileSync读取文件中的内容
  4. yaml.load将 YAML 字符转换为 JavaScript 对象
  5. 安装lodash,使用merge方法两个配置文件数据合并
import * as fs from 'fs';
import * as path from 'path';
import * as yaml from 'js-yaml';
import * as _ from 'lodash';
const basisfilename = 'default.yml';
const envpathfileName = `${process.env.NODE_ENV || 'development'}.yml`;


function readYamlFile(fileName) {
   // 获取文件路径 
  const filePath = path.join(__dirname, '../config', fileName);
  const file = fs.readFileSync(filePath, 'utf8');
  const data = yaml.load(file);
  return data;
}

export default () => {
    // 通过lodash中的merge方法合并
  return _.merge(readYamlFile(basisfilename), readYamlFile(envpathfileName));
};

在AppController中引入

import configuration from './configuration';

image.png

在AppController中打印输入 image.png

load里和envFilePath的配置加载优先级是相反的:后面的覆盖前面的

可以在load里添加一些相同参数的数据试下

() => ({
  DB: {
    USER: 'root-test',
    PASSWORD: 'root',
    HOST: 'localhost-dev',
    PORT: 3306,
    TYPE: 'yaml',
  },
}),

image.png 可以看到 第一项数据的值DB=>USER就被覆盖掉了

使用第三方包 config来配置

配置方式可参考上述中的json配置文件

使用Joi来对配置文件的参数进行校验

安装 Joi

pnpm install joi

在AppModule引入

import * as Joi from 'joi';

创建一个 Joi 模式,用于描述和校验配置对象的结构 添加相应的验证参数以及规则 joi.dev - 17.12.3 API Reference

const configSchema = Joi.object({
// 环境变量是`valid`中的其中之一
  NODE_ENV: Joi.string()
    .valid('development', 'production', 'test')
    .default('development'),
   // 端口号默认3306
  DB_PORT: Joi.number().default(3306),
  // ip或是域名
  DB_HOST: Joi.alternatives().try(Joi.string().ip(), 
Joi.string().domain()),
  DB_TYPE: Joi.string().valid('mysql', 'mongoose', 
'postgres').default('mysql'),
  DB_USERNAME: Joi.string().required(),
  DB_PASSWORD: Joi.string().required(),
});

添加到foRoot配置项validationSchema

const envFilePath = [`.${process.env.NODE_ENV || 'development'}.env`, '.env'];
...
    ConfigModule.forRoot({
      envFilePath,
      validationSchema: configSchema,
    }),

再次运行项目

image.png DB_HOST校验不通过,设置的值是localhost,我们的校验规则是ip或是域名,改成127.0.0.1试下

image.png 项目正常的启动起来了

前面我们提到除了配置envFilePath的指定.env文件来设置,还可以自定义配置文件 但通过load加载的变量不会被validateScheme校验 load方法需要自己加入验证

首先我们要获取到获取配置对象,需要将嵌套的配置对象扁平化为单层对象,其中嵌套的键通过下划线(_)连接(跟上面创建的configSchema做好对对应)

使用configSchema.validate(values, {...})这是 joi 库的一个方法,用于验证扁平化后的配置对象

如果 joi 的验证失败,会抛出一个错误,显示失败的原因

如果验证通过,加载函数返回的配置对象

因为是手动验证,validationSchema 就不需要配置了

// 将嵌套的配置对象扁平化为单层对象
function flattenObject(
  obj: { [key: string]: string | number },
  prefix = '',
  separator = '_',
) {
  return _.reduce(
    obj,
    (result, value, key) => {
      const flatKey = prefix ? `${prefix}${separator}${key}` : key;
      if (_.isObject(value) && !_.isArray(value)) {
        _.assign(result, flattenObject(value, flatKey, separator));
      } else {
        result[flatKey] = value;
      }
      return result;
    },
    {},
  );
}
...
 ConfigModule.forRoot({
      // envFilePath,
      load: [
        configuration,
        async () => {
          const DB = (await configuration()) as {
            [key: string]: string | number;
          };
          const values = flattenObject(DB);
          console.log(values);
          const { error } = configSchema.validate(values, {
            // 允许未知的环境变量
            allowUnknown: true,
            // 如果有错误,不要立即停止,而是收集所有错误
            abortEarly: false,
          });
          if (error) {
            throw new Error(`Validation failed ${error.message}`);
          }
          return DB;
        },
      ],
      // validationSchema: configSchema,
    }),

image.png 这样通过load的配置验证就跟我们预期的结果一致了

多环境配置总结

  1. 使用环境变量来区分不同环境,常用的是process.env.NODE_ENV,针对不同环境创建不同的配置文件
  2. 配合一些库例如dotenvconfig帮助我们加载和合并特定环境的配置
  3. 像一些简单的配置可以使用.env键值对形式配置,对于有层级嵌套关系的可以考虑yaml,json等类型的文件格式