Nests config模块扩展用法
前言
在 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 时,需要注意以下几点:
- ConfigModule 必须先载入:
ConditionalModule必须在ConfigModule被导入之后(或与其处于同级,且ConfigModule为全局模块时)才可使用。 - 5秒超时限制:在 NestJS 内部,
ConditionalModule会等待ConfigModule.envVariablesLoaded钩子就绪。如果你的配置由于各种原因在 5 秒内没有成功加载,NestJS 将会抛出超时错误并中止启动。
本文系作者 @木灵鱼儿 原创发布在木灵鱼儿站点。未经许可,禁止转载。
暂无评论数据