⚙️⚙任何一个系统,如果你不对它进行干涉和控制,它的混乱程度都会自然增大。
一、简介
应用程序经常在不同的环境中运行。将配置变量存储到环境变量中成了和服需求的做法。在 Node.js 中通过 process.env
访问环境变量。在 Node.js 中通常使用 .env
文件来解决不同的环境变量的问题,不同的环境中加载不同的环境变量。NestJS 中提供了 ConfigModule
与 ConfigService
加载和消费 .env
文件,页而已对配置进行增强。
本文会涉及到 yaml 的文件的加载,其他类型其实一致,都是被解析成了 JS 对象。
二、前置知识点
1. 简单的 JS 对象
const config = {/* your config */}
2. 处理敏感数据
当存在敏感数据时,使用环境变量 env 文件定义敏感数据,不写硬代码。
pnpm install dotenv
- 定义 env 文件
port = 1234
host = localhost
- 使用
import * as dotenv from 'dotenv'
dotenv.config()
const { host, port } = process.env // 消费 host port
3. 常用的配置方式
当配置内容比较多,比较杂乱的时候,通常使用针对一种类型进行单文件配置(例如:配置mongodb.config.yaml
数据库), 针对不同的文件类型有其对应的 JS 解释器:
解释器 | npm 包说明 |
---|---|
yaml/yml 文件 |
js-yaml yaml 文件最受欢迎 |
xml 文件 |
xml-js |
json 文件 |
JS 环境一般内置支持,可以使用 JS 对象替换 |
toml 文件 |
toml-node |
4. NestJS 中配置
NestJS 配置需要符合 NestJS 框架的特点,NestJS 提供了 @nestjs/config
包。
三、Nest 配置需要考虑哪些问题?
思考 | 说明 |
---|---|
1. 配置范式符合 NestJS 框架 | 符合 NestJS 编程范式 Module/Controller-Resolver/Service 编写特点,能通过依赖注入的方式访问配置 |
2. 安全,敏感数据 | 内置了 dotenv 将敏感的数据存在环境变量中,解决敏感安全问题 |
3. 良好的类型推断 | 对于 TS 开发由良好的类型提示 |
4. 配置校验能力 | 对当前运行环境由良好的校验能力,能够发现当前环境配置是否符合规则 |
5. 优良的可测试性 | 我们的配置应该具有良好的测试性。 |
6. 渐进式 api | 因为 NestJS 可以复杂配置,所以需要渐进式的 api 学习配置 |
7. 在不同的范围内使用 | 通过 NestJS Service 等分层的方式来访问 |
四、安装依赖
pnpm install @nestjs/config -S
注意:nestjs 中内置了 dotenv
和 dotenv-expand
,需要使用 env 能力,直接使用配置 ConfigModule
即可。
pnpm install js-yaml joi class-validator class-transformer cross-env
五、渐进式 API 使用
1. 简单开始
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [ConfigModule.forRoot()],
})
export class AppModule {}
2. 注入指定环境变量
# .env.development
host=127.0.0.1
port=3000
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [ConfigModule.forRoot({
envFilePath: ".env.development", // 指定文件 或者 ".env.development.local"
})],
})
export class AppModule {}
3. 忽略环境变量,使用运行时
通过 npm 脚本在运行时指定运行时环境变量
{
"scripts": {
"start:dev": "cross-env host=localhost port=1234 nest start --watch",
}
}
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [ConfigModule.forRoot({
ignoreEnvFile: true, // 使用运行时,忽略配置文件
})],
})
export class AppModule {}
4. 全局注册
在全部NestJS 模块中使用。
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [ConfigModule.forRoot({
isGlobal: true, // 全局注册
})],
})
export class AppModule {}
5. 使用 load 函数
// load函数
export default () => ({
host: process.env.host,
port: parseInt(process.env.port, 10) || 5432,
});
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import config from '@/config/index'
@Module({
imports: [ConfigModule.forRoot({
envFilePath: [".env", ".env.development"], // 读取数组中的第一个
load: [config],
})],
})
export class AppModule {}
6. 缓存
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [ConfigModule.forRoot({
cache: true
})],
})
export class AppModule {}
7. 类型安全
import { registerAs } from '@nestjs/config';
const { port, host } = process.env;
const appConfig = registerAs('app', () => {
return {
port: parseInt(port, 10) || 3333,
host,
};
});
export default appConfig;
export type IAppConfig = ReturnType<typeof appConfig>;
使用 IAppConfig 类型保证类型安全,在其他位置使用时,引入词类型即可。
8. 校验
validationSchema 配合 Joi 校验库,在代码运行时保证配置正确。
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AppConfigService } from './app-config.service';
import * as Joi from 'joi';
@Module({
imports: [
ConfigModule.forRoot({
envFilePath: ['.env', '.env.development'],
validationSchema: Joi.object({
host: Joi.string().valid('localhost', '127.0.0.1').default('localhost'),
port: Joi.number().default(8000),
}),
validationOptions: {
allowUnknow: false,
abortEarly: true,
},
}),
],
providers: [ConfigService, AppConfigService],
})
9. 自定义校验函数
除了给定的 validationSchema, 还可以更加灵活使用校验函数。
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AppConfigService } from './app-config.service';
import { validate } from './validate'
@Module({
imports: [
ConfigModule.forRoot({
envFilePath: ['.env', '.env.development'],
validate,
}),
],
providers: [ConfigService, AppConfigService],
})
export class AppConfigModule {}
10. 可测试性
本质上 NestJS 中内置了 Jest
能力,对于测试环境变量非常简单。以下是测试环境变量的断言测试和快照测试:
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AppConfigService } from './app-config/app-config.service';
import { AppConfigModule } from './app-config/app-config.module';
describe('AppController', () => {
let appConfigService: AppConfigService;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
imports: [AppConfigModule],
controllers: [AppController],
providers: [AppService, AppConfigService],
}).compile();
appConfigService = app.get<AppConfigService>(AppConfigService);
});
describe('app config service', () => {
it('port is nember 8000', () => {
expect(appConfigService.get('port')).toBe(8000);
});
it('host is string localhost', () => {
expect(appConfigService.get('host')).toBe('localhost');
});
});
describe('app config snapshot', () => {
it('port is nember 8000', () => {
expect(appConfigService.get('port')).toMatchInlineSnapshot(`8000`);
});
it('host is string localhost', () => {
expect(appConfigService.get('host')).toMatchInlineSnapshot(`"localhost"`);
});
});
});
六、配置实战
以 mongodb 数据库为例:
import { registerAs } from '@nestjs/config';
const {
MONGO_PORT: port,
MONGO_HOST: host,
MONGO_DB_NAME: db,
MONGO_USERNAME: user,
MONGO_PASSWORD: pass,
} = process.env;
const mongoConfig = registerAs('mongodb', () => {
return {
uri: `mongodb://${host}:${port}/${db}`,
user,
pass,
useNewUrlParser: true,
useUnifiedTopology: true,
};
});
export default mongoConfig;
在主模块中注册:
import { MiddlewareConsumer, Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
// self modules
import { AppController } from './app.controller';
import { AppService } from './app.service';
import mongoConfigFactory from '@/config/mongo.config';
@Module({
imports: [
ConfigModule,
MongooseModule.forRootAsync({
useFactory: mongoConfigFactory,
}),
],
controllers: [AppController],
providers: [AppService, ConfigService],
})
export class AppModule {}
七、微服务配置
NestJS 中微服务,使用很简单,在主模块中注入配置模块,并且启动全局配置,即可在不同的微服务中配置。如果微服务有了分布式需求,可以使用具有分布式配置中心功能工具:
- Consul 使用服务标识和传统网络实践来帮助组织安全地连接在任何环境中运行的应用程序。
- etcd 分布式、可靠的键值存储,用于存储分布式系统中最关键的数据。使用 Go 编写。
- ZooKeeper ZooKeeper 是一个分布式的协调服务,也可用于配置管理。你可以使用 ZooKeeper 存储和管理配置信息,并使用 ZooKeeper 客户端从应用程序中获取配置
这些工具都已经跳出 JavaScript/TypeScript 语言范畴,本文主要讨论 NestJS 配置,这里不再说明。
八、源码分析
1. 源码依赖
"dependencies": {
"dotenv": "16.1.4",
"dotenv-expand": "10.0.0",
"lodash": "4.17.21",
"uuid": "9.0.0"
}
2. 源码 index.ts
export * from "./config.module";
export * from "./config.service";
export * from "./types";
export * from "./utils";
export * from "./interfaces";
3. 关注 Module 中 forRoot 方法
forRoot 方法中正对不同的 options 配置,做了不同的选择:
- 忽略 env 文件,直接从 process.env 中合并 config
if (!options.ignoreEnvVars) {
config = {
...config,
...process.env,
};
}
- 有验证函数
if (options.validate) {
const validatedConfig = options.validate(config);
validatedEnvConfig = validatedConfig;
this.assignVariablesToProcess(validatedConfig);
}
- 有验证Schema
if (options.validationSchema) {
const validationOptions = this.getSchemaValidationOptions(options);
const { error, value: validatedConfig } =
options.validationSchema.validate(config, validationOptions);
if (error) {
throw new Error(`Config validation error: ${error.message}`);
}
validatedEnvConfig = validatedConfig;
this.assignVariablesToProcess(validatedConfig);
}
- 其他就是走了分配逻辑
private static assignVariablesToProcess(config: Record<string, unknown>) {
if (!isObject(config)) {
return;
}
const keys = Object.keys(config).filter(key => !(key in process.env));
keys.forEach(key => {
const value = config[key];
if (typeof value === 'string') {
process.env[key] = value;
} else if (typeof value === 'boolean' || typeof value === 'number') {
process.env[key] = `${value}`;
}
});
}
4. 关注 Service 中 get 方法
get 时一个多态,在获取环境变量的使用此方法。
get<T = any>(propertyPath: KeyOf<K>): ValidatedResult<WasValidated, T>;
get<T = K, P extends Path<T> = any, R = PathValue<T, P>>(
propertyPath: P,
options: ConfigGetOptions,
): ValidatedResult<WasValidated, R>;
// ...
九、其它解决方案
nestjs-config 使用 glob 特性的管理你的 NestJS 配置
pnpm add nestjs-config
指定配置配置文件和模块
/src
├── app.module.ts
├── config
│ ├── express.ts
│ ├── graphql.ts
│ └── grpc.ts
导入中主模块中
import { Module } from '@nestjs/common';
import { ConfigModule } from 'nestjs-config';
import * as path from 'path';
@Module({
imports: [
ConfigModule.load(path.resolve(__dirname, 'config', '**/!(*.d).{ts,js}')),
],
})
export class AppModule {}
- nest-typed-config 直观、类型安全的 Nest 框架配置模块
- nestjs-easyconfig 随时随地管理 nestjs 配置?
十、小结
在 NestJS 中配置方式多种多样,每一种类型的配置适合一种环境。总体上要符合的特点,对敏感词汇不直接暴露,符合 NestJS 的范式,类型安全,良好的测试,支持灵活的文件类型,也介绍了 NestJS 之外,具有分布式特性,服务于微服务的工具类似于 Consul 等工具。