浅析 @nestjs/config

环境变量的来源渠道

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);
    }

首先会根据 validatevalidationSchema 来判断是否需要进行校验,如果有则先校验。校验后的配置会保存到 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}`;
      }
    });
  }

输出

每一个 loadconfig 都会通过 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: [],
  };
}

然后创建一个 ConfigServiceProvider,并 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 实际上就做了三件事:

  1. 包装 registerAsconfig
  2. 提供 configService
  3. 合并 configCONFIGURATION_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

forRootforFeature 最后返回的 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> = {},
  ) {}
相关推荐
Eric_见嘉2 天前
NestJS 🧑‍🍳 厨子必修课(九):API 文档 Swagger
前端·后端·nestjs
XiaoYu200210 天前
第3章 Nest.js拦截器
前端·ai编程·nestjs
XiaoYu200211 天前
第2章 Nest.js入门
前端·ai编程·nestjs
实习生小黄12 天前
NestJS 调试方案
后端·nestjs
当时只道寻常15 天前
NestJS 如何配置环境变量
nestjs
濮水大叔1 个月前
VonaJS是如何做到文件级别精确HMR(热更新)的?
typescript·node.js·nestjs
ovensi1 个月前
告别笨重的 ELK,拥抱轻量级 PLG:NestJS 日志监控实战指南
nestjs
ovensi1 个月前
Docker+NestJS+ELK:从零搭建全链路日志监控系统
后端·nestjs
Gogo8161 个月前
nestjs 的项目启动
nestjs