前言
通常开发环境会包含环境:开发(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里引入并加载
可以看到打印的信息有包含了
.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);
可以看到在
.development.env中配置的环境变量就被打印出来,并覆盖了.env文件里的配置
config
configconfig - npm (npmjs.com) 它允许创建多个配置文件,可以根据不同环境加载相应的配置,并对默认(公共)配置覆盖合并\
配置文件通常存放在项目根目录下的
/config目录中 主配置文件(默认配置)通常命名为 default.json
需要创建特定的配置文件 如development.json,test.json,production.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'));
可以看到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
在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文件)
import { ConfigModule } from '@nestjs/config';
...
@Module({
imports: [ConfigModule.forRoot()],
...
})
AppController中注入依赖
private configService: ConfigService
pnpm start:dev启动项目,访问3000端口,DB_HOST的值被打印出来了
指定一个或多个文件路径也是可以的,通过ForRoot配置对象里的
envFilePath属性来设置
const envFilePath = [`.${process.env.NODE_ENV || 'development'}.env`, '.env'];
重新启动项目访问3000端口
分别输出了DB_HOST,DB_USERNAME,DB_HOST的值
这里也是前面的配置会覆盖后面的配置
官方方案 进阶用法
.env里配置满足大多数情况下使用的场景,配置层级简单,如果配置项是层级嵌套的,需要灵活去配置(yaml,json或其他类型的文件)。键值对的形式就不太适用了
在forRoot配置里可以加载自定义配置文件,通过load属性来设置,数组中的每一项是一个函数,支持异步方法,函数的返回值就是配置项
安装js-yaml这个包用于解析yaml格式文件
pnpm install js-yaml
在根目录创建config,并添加yaml文件(同上述中的yaml配置文件,在这里不需要去指定某个文件名)
在src下创建configuration.ts
process.env.NODE_ENV获取当前的环境path.join获取对应的文件路径fs.readFileSync读取文件中的内容yaml.load将 YAML 字符转换为 JavaScript 对象- 安装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';
在AppController中打印输入
在load里和envFilePath的配置加载优先级是相反的:后面的覆盖前面的
可以在load里添加一些相同参数的数据试下
() => ({
DB: {
USER: 'root-test',
PASSWORD: 'root',
HOST: 'localhost-dev',
PORT: 3306,
TYPE: 'yaml',
},
}),
可以看到 第一项数据的值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,
}),
再次运行项目
DB_HOST校验不通过,设置的值是localhost,我们的校验规则是ip或是域名,改成127.0.0.1试下
项目正常的启动起来了
前面我们提到除了配置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,
}),
这样通过
load的配置验证就跟我们预期的结果一致了
多环境配置总结
- 使用环境变量来区分不同环境,常用的是
process.env.NODE_ENV,针对不同环境创建不同的配置文件 - 配合一些库例如
dotenv、config帮助我们加载和合并特定环境的配置 - 像一些简单的配置可以使用.env键值对形式配置,对于有层级嵌套关系的可以考虑yaml,json等类型的文件格式