环境变量的来源渠道
Node 开发过程中,环境变量的来源一般大致可分为两种:命令行和环境变量文件。环境变量文件本质上也是被读取、解析,最终和命令行一样加载到 process.env
这个对象上。
对于数量较少的环境变量,可以直接通过 process.env
来使用,但是当项目规模膨胀后,环境变量可能多达数十个、数百个,仅仅通过 process.env
来使用,并不够方便,而且还要面临类型转换、env 缺失等一系列问题。
因此,在 Nestjs 上,提供了一套 Config 解决方案。
forRoot
加载
一开始,forRoot
会直接加载 load
属性中的 env 文件
tsx
// lib/config.module.ts
static forRoot(options: ConfigModuleOptions = {}): DynamicModule {
let validatedEnvConfig: Record<string, any> | undefined = undefined;
let config = options.ignoreEnvFile ? {} : this.loadEnvFile(options);
而 loadEnvFile
这个方法本质上就是读取对应的文件,并用 dotenv
这个库来解析
tsx
// lib/config.module.ts
private static loadEnvFile(
options: ConfigModuleOptions,
): Record<string, any> {
const envFilePaths = Array.isArray(options.envFilePath)
? options.envFilePath
: [options.envFilePath || resolve(process.cwd(), '.env')];
let config: ReturnType<typeof dotenv.parse> = {};
for (const envFilePath of envFilePaths) {
if (fs.existsSync(envFilePath)) {
config = Object.assign(
dotenv.parse(fs.readFileSync(envFilePath)),
config,
);
if (options.expandVariables) {
const expandOptions: DotenvExpandOptions =
typeof options.expandVariables === 'object'
? options.expandVariables
: {};
config =
expand({ ...expandOptions, parsed: config }).parsed || config;
}
}
}
return config;
}
之后,会根据 ignoreEnvVars
这个属性来决定是否加载命令行里已存在的 env
变量。根据解析的顺序也能看出,命令行中的 env
优先级是高于通过 load
加载的 env
文件的
tsx
// lib/config.module.ts
if (!options.ignoreEnvVars) {
config = {
...config,
...process.env,
};
}
处理
至此,env
的两大来源已经加载完毕,之后则是根据配置处理,并以 Nest 的 Provider
的形式暴露
tsx
// lib/config.module.ts
if (options.validate) {
const validatedConfig = options.validate(config);
validatedEnvConfig = validatedConfig;
this.assignVariablesToProcess(validatedConfig);
} else 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);
} else {
this.assignVariablesToProcess(config);
}
首先会根据 validate
和 validationSchema
来判断是否需要进行校验,如果有则先校验。校验后的配置会保存到 validatedEnvConfig
这个变量中。
无论是否校验,都会通过 assignVariablesToProcess
这个方法将已读取的环境变量保存到 process.env
上
tsx
// lib/config.module.ts
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}`;
}
});
}
输出
每一个 load
的 config
都会通过 createConfigProvider
这个工厂函数转成一个 Provider
,而 configProviderTokens
会在最后 return 的 exports
数组中使用
tsx
// lib/config.module.ts
const isConfigToLoad = options.load && options.load.length;
const providers = (options.load || [])
.map(factory =>
createConfigProvider(factory as ConfigFactory & ConfigFactoryKeyHost),
)
.filter(item => item) as FactoryProvider[];
const configProviderTokens = providers.map(item => item.provide);
实际上这个工厂函数就是一个简单的包装
tsx
// lib/utils/create-config-factory.util.ts
export function createConfigProvider(
factory: ConfigFactory & ConfigFactoryKeyHost,
): FactoryProvider {
return {
provide: factory.KEY || getConfigToken(uuid()),
useFactory: factory,
inject: [],
};
}
然后创建一个 ConfigService
的 Provider
,并 push 进刚才的 providers
里
tsx
// lib/config.module.ts
const configServiceProvider = {
provide: ConfigService,
useFactory: (configService: ConfigService) => {
if (options.cache) {
(configService as any).isCacheEnabled = true;
}
return configService;
},
inject: [CONFIGURATION_SERVICE_TOKEN, ...configProviderTokens],
};
providers.push(configServiceProvider);
校验过的环境变量的 Provider
tsx
// lib/config.module.ts
if (validatedEnvConfig) {
const validatedEnvConfigLoader = {
provide: VALIDATED_ENV_LOADER,
useFactory: (host: Record<string, any>) => {
host[VALIDATED_ENV_PROPNAME] = validatedEnvConfig;
},
inject: [CONFIGURATION_TOKEN],
};
providers.push(validatedEnvConfigLoader);
}
CONFIGURATION_TOKEN
来自 config-host
module,实际上是一个返回空对象的工厂函数
tsx
// lib/config-host.module.ts
@Global()
@Module({
providers: [
{
provide: CONFIGURATION_TOKEN,
useFactory: () => ({}), // 一个返回空对象的工厂函数
},
{
provide: CONFIGURATION_SERVICE_TOKEN,
useClass: ConfigService,
},
],
exports: [CONFIGURATION_TOKEN, CONFIGURATION_SERVICE_TOKEN],
})
export class ConfigHostModule {}
触发 hook
tsx
// lib/config-host.module.ts
this.environmentVariablesLoadedSignal();
private static environmentVariablesLoadedSignal: () => void;
private static readonly _envVariablesLoaded = new Promise<void>(
resolve => (ConfigModule.environmentVariablesLoadedSignal = resolve),
);
返回生成的 module。
tsx
// lib/config-host.module.ts
return {
module: ConfigModule,
global: options.isGlobal,
providers: isConfigToLoad
? [
...providers,
/**
* 处理 load 部分的内容
*/
{
provide: CONFIGURATION_LOADER,
useFactory: (
host: Record<string, any>,
...configurations: Record<string, any>[]
) => {
configurations.forEach((item, index) =>
this.mergePartial(host, item, providers[index]),
);
},
inject: [CONFIGURATION_TOKEN, ...configProviderTokens],
},
]
: providers,
exports: [ConfigService, ...configProviderTokens],
};
forFeature
forFeature
可以为单个 module "定制"访问的环境变量,通常和 registerAs 这个函数使用来生成 name space
registerAs
PARTIAL_CONFIGURATION_PROPNAME
实际上就是 KEY,在使用中的 .KEY 就是在这定义的
AS_PROVIDER_METHOD_KEY
实际上就是 asProvider
然后返回处理过的 configFactory
tsx
// lib/utils/register-as.util.ts
export function registerAs<
TConfig extends ConfigObject,
TFactory extends ConfigFactory = ConfigFactory<TConfig>,
>(
token: string,
configFactory: TFactory,
): TFactory & ConfigFactoryKeyHost<ReturnType<TFactory>> {
const defineProperty = (key: string, value: unknown) => {
Object.defineProperty(configFactory, key, {
configurable: false,
enumerable: false,
value,
writable: false,
});
};
defineProperty(PARTIAL_CONFIGURATION_KEY, token);
defineProperty(PARTIAL_CONFIGURATION_PROPNAME, getConfigToken(token));
defineProperty(AS_PROVIDER_METHOD_KEY, () => ({
imports: [ConfigModule.forFeature(configFactory)],
useFactory: (config: unknown) => config,
inject: [getConfigToken(token)],
}));
return configFactory as TFactory & ConfigFactoryKeyHost<ReturnType<TFactory>>;
}
forFeature
forFeature
实际上就做了三件事:
- 包装
registerAs
的config
- 提供
configService
- 合并
config
到CONFIGURATION_TOKEN
代表的对象上
tsx
// lib/config-host.module.ts
static forFeature(config: ConfigFactory): DynamicModule {
/**
* 处理 registerAs 部分的 config 变量
*/
const configProvider = createConfigProvider(
config as ConfigFactory & ConfigFactoryKeyHost,
);
/**
* 导入 config service
*/
const serviceProvider = {
provide: ConfigService,
useFactory: (configService: ConfigService) => configService,
inject: [CONFIGURATION_SERVICE_TOKEN, configProvider.provide],
};
return {
module: ConfigModule,
providers: [
configProvider,
serviceProvider,
{
provide: CONFIGURATION_LOADER,
useFactory: (
host: Record<string, any>,
partialConfig: Record<string, any>,
) => {
this.mergePartial(host, partialConfig, configProvider);
},
inject: [CONFIGURATION_TOKEN, configProvider.provide],
},
],
exports: [ConfigService, configProvider.provide],
};
}
CONFIGURATION_TOKEN
在 forRoot
和 forFeature
最后返回的 providers
里,都有这样相似的部分:
tsx
// forRoot
{
provide: CONFIGURATION_LOADER,
useFactory: (
host: Record<string, any>,
...configurations: Record<string, any>[]
) => {
configurations.forEach((item, index) =>
this.mergePartial(host, item, providers[index]),
);
},
inject: [CONFIGURATION_TOKEN, ...configProviderTokens],
},
tsx
// forFeature
{
provide: CONFIGURATION_LOADER,
useFactory: (
host: Record<string, any>,
partialConfig: Record<string, any>,
) => {
this.mergePartial(host, partialConfig, configProvider);
},
inject: [CONFIGURATION_TOKEN, configProvider.provide],
},
本质上,它们都是在把加载的环境变量合并到 CONFIGURATION_TOKEN
表示的空对象上
tsx
// lib/config.module.ts
private static mergePartial(
host: Record<string, any>, // CONFIGURATION_TOKEN
item: Record<string, any>, // configProvider.provide
provider: FactoryProvider, // configProvider
) {
const factoryRef = provider.useFactory; // configProvider.useFactory
const token = getRegistrationToken(factoryRef);
mergeConfigObject(host, item, token);
}
tsx
// lib/utils/get-registration-token.util.ts
export function getRegistrationToken(config: Record<string, any>) {
// PARTIAL_CONFIGURATION_KEY 之前在 registerAs定义过
return config[PARTIAL_CONFIGURATION_KEY];
}
tsx
// lib/utils/merge-configs.util.ts
export function mergeConfigObject(
host: Record<string, any>,
partial: Record<string, any>,
token?: string,
) {
if (token) {
// set(object, path, value)
set(host, token, partial);
return partial;
}
Object.assign(host, partial);
}
而 CONFIGURATION_TOKEN
会在 ConfigService
里被注入、消费
tsx
// lib/config.service.ts
constructor(
@Optional()
@Inject(CONFIGURATION_TOKEN)
private readonly internalConfig: Record<string, any> = {},
) {}