前言

在 NestJS 的日常开发中,@nestjs/config 是我们最常用的模块之一。大多数开发者对它的印象可能还停留在“读取 .env 环境变量并注入到 ConfigService 中”。

其实,@nestjs/config 提供了许多非常实用的进阶功能。本文将介绍其中三个实用的拓展用法:扩展变量(Expandable variables)环境变量加载钩子(Environment variables loaded hook)条件模块配置(Conditional module configuration)


1. 扩展变量(Expandable variables)

场景与痛点

在配置项目时,有些环境变量是相互关联的。例如,你的应用 URL 依赖端口号,或者数据库的完整连接字符串(DSN)依赖主机名、用户名和密码:

DB_HOST=localhost
DB_PORT=5432
DB_USER=root
DB_PASSWORD=secret
# 如果每次修改端口或密码,都要手动同步修改下面这行,会非常繁琐且容易出错
DB_URL=postgresql://root:secret@localhost:5432/mydb

解决方案

NestJS 支持变量拓展(Variable Expansion)。你可以通过 ${VAR_NAME} 语法在 .env 中引用其他已定义的变量。该特性默认关闭,只需在 ConfigModule 初始化时设置 expandVariables: true 即可启用。

代码示例

首先,在项目根目录下修改 .env 文件:

PORT=3000
APP_URL=http://localhost:${PORT}

DB_HOST=localhost
DB_PORT=5432
DB_USER=myuser
DB_PASSWORD=mypassword
DB_NAME=mydatabase
DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}

接着,在 AppModule 中启用该功能:

// app.module.ts
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";

@Module({
    imports: [
        ConfigModule.forRoot({
            expandVariables: true, // 开启变量展开支持
        }),
    ],
})
export class AppModule {}

现在,当你通过 ConfigService 获取 DATABASE_URL 时,它会自动解析为 postgresql://myuser:mypassword@localhost:5432/mydatabase。这减少了配置文件的冗余,维护起来也更不容易出错。


2. 环境变量加载钩子(Environment variables loaded hook)

场景与痛点

NestJS 在启动时,会按照模块的声明顺序依次解析。
如果某个模块的配置高度依赖系统环境变量,且这些变量必须从 .env 文件中异步读取,那么在应用初始化初期(如执行某些动态模块导出函数时),process.env 可能还没有来得及被填充。这会导致读取到的值为 undefined

解决方案

NestJS 提供了 ConfigModule.envVariablesLoaded 钩子。这是一个 Promise,它会保证在 .env 文件被完全读取并合并到 process.env 后才处于 resolved 状态。我们可以在自定义的异步函数中 await 它,确保数据安全加载。

代码示例

假设我们有一个动态选择存储服务的逻辑(本地存储或云存储):

// storage.config.ts
import { ConfigModule } from "@nestjs/config";
import { CloudStorageModule } from "./cloud-storage.module";
import { LocalStorageModule } from "./local-storage.module";

export async function getStorageModule() {
    // 确保环境变量完全加载完毕
    await ConfigModule.envVariablesLoaded;

    // 此时可以确定 process.env 已经被正确赋值
    return process.env.STORAGE_PROVIDER === "CLOUD" ? CloudStorageModule : LocalStorageModule;
}

AppModule 中安全引入:

// app.module.ts
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { getStorageModule } from "./storage.config";

@Module({
    imports: [
        ConfigModule.forRoot(),
        getStorageModule(), // 动态导入解析后的存储模块
    ],
})
export class AppModule {}

3. 条件模块配置(Conditional module configuration)

场景与痛点

在实际开发中,有些模块只希望在特定环境下加载
例如邮件服务(Email Service)

  • 在本地开发或测试环境,我们不希望真的调用第三方邮件服务(如 SendGrid)去发送真实邮件,而是加载一个 MockEmailModule,把邮件内容直接打印到控制台,从而避免产生费用或向真实用户发送测试邮件。
  • 在生产环境,则必须加载 RealEmailModule 来发送真实的邮件。

解决方案

@nestjs/config 提供了 ConditionalModule 来进行模块的条件加载。它的 registerWhen 方法允许我们传入一个模块,并根据环境变量的状态决定是否将其注册到应用中。

代码示例

我们先在 .env 文件中定义一个变量控制是否使用模拟邮件:

USE_MOCK_EMAIL=true

然后在 AppModule 中使用 ConditionalModule

// app.module.ts
import { Module } from "@nestjs/common";
import { ConfigModule, ConditionalModule } from "@nestjs/config";
import { MockEmailModule } from "./email/mock-email.module";
import { RealEmailModule } from "./email/real-email.module";

@Module({
    imports: [
        ConfigModule.forRoot({
            isGlobal: true, // 设置为全局模块,方便其他地方使用配置
        }),

        // 1. 当 USE_MOCK_EMAIL 变量存在且不等于 'false' 时,加载模拟邮件模块
        ConditionalModule.registerWhen(MockEmailModule, "USE_MOCK_EMAIL"),

        // 2. 也可以传入自定义函数。当不使用模拟邮件时,加载真实邮件模块
        ConditionalModule.registerWhen(
            RealEmailModule,
            (env: NodeJS.ProcessEnv) => env.USE_MOCK_EMAIL !== "true",
        ),
    ],
})
export class AppModule {}

💡 注意事项与避坑指南

使用 ConditionalModule 时,需要注意以下几点:

  1. ConfigModule 必须先载入ConditionalModule 必须在 ConfigModule 被导入之后(或与其处于同级,且 ConfigModule 为全局模块时)才可使用。
  2. 5秒超时限制:在 NestJS 内部,ConditionalModule 会等待 ConfigModule.envVariablesLoaded 钩子就绪。如果你的配置由于各种原因在 5 秒内没有成功加载,NestJS 将会抛出超时错误并中止启动。
分类: NestJS中级 标签: Nestjsconfig模块

评论

暂无评论数据

暂无评论数据

目录