在写后端的时候,我们一般提倡配置文件分离.
所以.env
就可以很方面来维护我们的环境变量,
封装对应的工厂函数也能组合更复杂的配置!
比如我们用镜像(Docker),就可以外部映射配置文件目录;
达到不同环境使用差异化配置的需求!(运行时加载是允许的!)
其他不多说,往下可以看看我的配置分离思路~~
# @nestjs/config 内置了dotenv
yarn add @nestjs/config joi
yarn add -D @types/hapi__joi
export interface ConfigModuleOptions {
isGlobal?: boolean; // 启用这个会作用于整个大系统(全局module),而非仅你当前注入的module!
ignoreEnvFile?: boolean; // 若是开启这个就会忽略.env文件的读取!!
ignoreEnvVars?: boolean; // 忽略系统级变量注入,默认是关闭(会读取系统变量)
envFilePath?: string | string[];// .env文件的去,基于运行时根路径找(process.cwd)
encoding?: string; // 文件编码,推荐utf-8,容错率高!
validationSchema?: any; // 可以校验所有传入自定义环境变量(没关闭系统变量也会追加进来)
validationOptions?: Record<string, any>;
load?: Array<ConfigFactory>; // 加载环境变量的工厂函数,可以用于组合复杂的配置
expandVariables?: boolean; // 支持环境变量嵌套变量,
}
{
比如环境变量 APP_NAME=HHH
拓展变量就是这样写法,跟字符串模板类似 APP_VERSION=${APP_NAME}-V1
基于 https://github.com/motdotla/dotenv-expand 实现的
"expandVariables":true
}
我倾向于把所有环境变量配置放到根目录config目录,
这样有什么好处呢?配置集中化,映射也很方便(比如用了Docker)
指定volume就可以了..不同环境互不干涉(开发,测试,生产!)
├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── README.md
├── config # 这里
│ └── env
│ ├── dev.local.env
│ ├── http.env
│ └── report.env
├── nest-cli.json
├── package.json
├── src
│ ├── app.controller.spec.ts
│ ├── app.controller.ts
│ ├── app.dto.ts
│ ├── app.module.ts
│ ├── app.service.ts
│ ├── config
│ │ ├── env
│ │ ├── http-status-code.msg.ts
│ │ └── module
│ ├── main.ts
│ └── utils
│ ├── apm-init.ts
│ ├── get-dir-all-file-name-arr.ts
│ └── terminal-help-text-console.ts
├── test
│ ├── app.e2e-spec.ts
│ └── jest-e2e.json
├── tsconfig.build.json
├── tsconfig.json
├── yarn-error.log
└── yarn.lock
比如我把swagger,axios这些都抽出来了
import * as Joi from '@hapi/joi';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import envReportConfig from './config/env/report.config';
import envSwaggerConfig from './config/env/swagger.config';
import { getDirAllFileNameArr } from './utils/get-dir-all-file-name-arr';
@Module({
imports: [
ConfigModule.forRoot({
encoding: 'utf-8',
envFilePath: [...getDirAllFileNameArr()],
expandVariables: true, // 开启嵌套变量
ignoreEnvVars: true,
load: [envReportConfig, envSwaggerConfig],
validationSchema: Joi.object({
H3_APM_SERVER_URL: Joi.string().default(''),
H3_LATEINOS_REPORT_URL: Joi.string().default(''),
SERVE_LISTENER_PORT: Joi.number().default(3000),
SWAGGER_SETUP_PATH: Joi.string().default('api-docs'),
SWAGGER_ENDPOINT_PREFIX: Joi.string().default('api/v1'),
SWAGGER_UI_TITLE: Joi.string().default('Swagger文档标题'),
SWAGGER_UI_TITLE_DESC: Joi.string().default('赶紧改相关配置啊~~'),
SWAGGER_API_VERSION: Joi.string().default('1.0'),
HTTP_TIMEOUT: Joi.number().default(5000),
HTTP_MAX_REDIRECTS: Joi.number().default(5),
NODE_ENV: Joi.string()
.valid('development', 'production', 'test', 'provision')
.default('development'),
}),
validationOptions: {
allowUnknown: false, // 控制是否允许环境变量中未知的键。默认为true。
abortEarly: true, // 如果为true,在遇到第一个错误时就停止验证;如果为false,返回所有错误。默认为false。
},
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
这里面有四处可以说一下
这里只用到了joi的基础语法,比如默认转换格式,添加默认值. 匹配数组有效值,不匹配默认用默认值~ 若是环境变量使用异常,或者转换异常就会抛出类似的错误
这个可以用来加载组合的配置函数, 比如你一些配置分散在多个.env中, 然后需要组装成一个对象传入,方便使用! 具体项目中的例子,先定义!
// env configuration
import { registerAs } from '@nestjs/config';
export interface EnvSwaggerOptions {
title: string;
setupUrl: string;
desc?: string;
prefix: string;
version: string;
}
export default registerAs(
'EnvSwaggerOptions',
(): EnvSwaggerOptions => ({
title: process.env.SWAGGER_UI_TITLE,
desc: process.env.SWAGGER_UI_TITLE_DESC,
version: process.env.SWAGGER_API_VERSION,
setupUrl: process.env.SWAGGER_SETUP_PATH,
prefix: process.env.SWAGGER_ENDPOINT_PREFIX,
}),
);
然后使用,比如我们在项目主入口用!
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { AppModule } from './app.module';
import { ConfigService } from '@nestjs/config';
import { EnvSwaggerOptions } from './config/env/swagger.config';
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from './common/pipes/validataion.pipe';
import { terminalHelpTextConsole } from './utils/terminal-help-text-console';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
cors: false,
logger: false,
});
// app.get 可以获取到对应初始化成功的实例!
const configService = app.get(ConfigService);
// configService.get可以获取到我们封装的配置对象或者系统变量!
const swaggerOptions = configService.get<EnvSwaggerOptions>(
'EnvSwaggerOptions',
);
const options = new DocumentBuilder()
.setTitle(swaggerOptions.title)
.setDescription(swaggerOptions.desc)
.setVersion(swaggerOptions.version)
.addBearerAuth()
.build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup(swaggerOptions.setupUrl, app, document, {
customSiteTitle: swaggerOptions.title,
swaggerOptions: {
docExpansion: 'list',
filter: true,
showRequestDuration: true,
},
});
await app.listen(configService.get('SERVE_LISTENER_PORT'));
}
bootstrap().then(() => {
// 这个东西就是自己拼接的东东,启动成功后在终端输出点东东实现及效果图如下
terminalHelpTextConsole();
});
import * as chalk from 'chalk';
type paramType = {
Port: string | number;
DocUrl: string;
ApiPrefix: string;
};
const defaultParam: paramType = {
Port: process.env.SERVE_LISTENER_PORT,
DocUrl: process.env.SWAGGER_SETUP_PATH,
ApiPrefix: process.env.SWAGGER_ENDPOINT_PREFIX,
};
/**
* 打印相关的帮助信息到终端
* @param params
*/
export function terminalHelpTextConsole(params = defaultParam): void {
const Host = `http://localhost`;
console.log(
chalk.red.bold('Swagger文档链接:'.padStart(16)),
chalk.green.underline(`${Host}:${params.Port}/${params.DocUrl}`),
);
console.log(
chalk.red.bold('Restful接口链接:'.padStart(16)),
chalk.green.underline(`${Host}:${params.Port}/${params.ApiPrefix}`),
);
}
我不喜欢手动去维护可能越来越多的配置文件, 所以我写了个函数来一次性拿到第一级所有文件名拼接成数组; 判定是否为文件且后缀为.env
// get-dir-all-file-name-arr.ts
import * as fs from 'fs';
import * as path from 'path';
// 默认存放env文件的文件夹路径
const directory = path.resolve(process.cwd(), 'config/env');
type optionsType = {
dirPath?: string;
prefix?: string;
};
/**
* 返回目录下所有文件的文件名(字符串数组形式)
* @typedef {Object} options 参数选项
* @param {string} options.dirPath 目录路径
* @param {string} options.prefix 给每一个匹配项增加前缀文本
* @return {string[]} 不传参数默认返回/config/env下所有文件拼接的数组
*/
export function getDirAllFileNameArr(options?: optionsType): string[] {
const params = { dirPath: directory, prefix: 'config/env/', ...options };
const results = [];
try {
for (const dirContent of fs.readdirSync(params.dirPath)) {
const dirContentPath = path.resolve(directory, dirContent);
console.log(dirContentPath);
if (fs.statSync(dirContentPath).isFile()) {
if (dirContent.endsWith('.env')) {
if (params.prefix) {
results.push(`${params.prefix}${dirContent}`);
} else {
results.push(dirContent);
}
}
}
}
return results;
} catch (error) {
return results;
}
}
// output
/**
* [
* 'config/env/dev.local.env',
* 'config/env/http.env',
* 'config/env/report.env'
* ]
*/
在main.js的bootstrap内输出process.env就可以看到了
至此,一个维护性还不错的姿势已经落实; 有不对之处请留言,会及时修正,谢谢阅读!