浅析 @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> = {},
  ) {}
相关推荐
webxue2 天前
NestJS配置环境变量、读取Yaml配置的保姆级教程
node.js·nestjs
超级无敌暴龙兽5 天前
微服务架构的基础与实践:构建灵活的分布式系统
微服务·node.js·nestjs
寻找奶酪的mouse7 天前
【NestJS全栈之旅】应用篇:通用爬虫服务三两事儿
前端·后端·nestjs
_jiang9 天前
nestjs 入门实战最强篇
redis·typescript·nestjs
敲代码的彭于晏11 天前
【Nest.js 10】JWT+Redis实现登录互踢
前端·后端·nestjs
前端小王hs25 天前
Nest通用工具函数执行顺序
javascript·后端·nestjs
明远湖之鱼1 个月前
从入门到入门学习NestJS
前端·后端·nestjs
吃葡萄不吐番茄皮1 个月前
从零开始学 NestJS(一):为什么要学习 Nest
前端·nestjs
东方小月1 个月前
Vue3+NestJS实现权限管理系统(六):接口按钮权限控制
前端·后端·nestjs
白雾茫茫丶1 个月前
Nest.js 实战 (十四):如何获取客户端真实 IP
nginx·nestjs