难的不是读取
.env,而是值从哪里来、谁覆盖谁、何时装配完成,以及一次请求最终应该看到哪份配置。
小项目里,端口、数据库和 Redis 放进一个 .env 文件通常已经足够。但当部署形态、测试隔离、模块复用、客户差异或多租户需求出现时,配置就不再是一串变量,而是运行时架构的一部分。
本文以 NestJS @nestjs/config 4.0.4 和当前 Cabloy Basic 的 Vona 后端为基线。结论是:NestJS 提供可组合的应用配置工具;CabloyJS/Vona 则将 mode、flavor、模块配置和实例有效配置组织成一条框架级链路。
先看结论
| 维度 | NestJS @nestjs/config | CabloyJS / Vona |
|---|---|---|
| 核心定位 | 应用可组合的配置工具箱 | 运行时与配置层叠模型的一部分 |
| 环境选择 | 应用声明 .env 路径和加载策略 | CLI 先确定 mode/flavor,再选择级联 env/config |
| 运行时验证 | Joi / 自定义 validate 入口 | 强类型配置形状;中心启动路径未提供同类统一验证入口 |
| 模块归属 | registerAs()、load、forFeature() | 模块 config 资源 + 项目 config.modules[...] 覆盖 |
| 请求级差异 | 应用自行组合租户/请求配置层 | ctx.config 提供实例合并后的有效配置 |
两者都能使用 env,但回答的不是同一个问题:NestJS 更关注“应用如何组合配置能力”,Vona 更关注“运行时的配置应该由应用、模块还是实例拥有”。
NestJS:以 process.env 为中心的组合式方案
NestJS 的典型入口是 ConfigModule.forRoot():读取 env 文件、结合已有的 process.env,并提供 ConfigService。应用可以自主选择 Joi schema、自定义同步 validate()、配置 factory、命名空间、缓存与变量展开。
ConfigModule.forRoot({
isGlobal: true,
envFilePath: ['.env.production.local', '.env.production'],
validationSchema: Joi.object({
PORT: Joi.number().port().required(),
}),
});
这里必须区分两套优先级:多个 envFilePath 时,数组靠前的文件优先;启动前已经注入的 process.env 默认覆盖文件中的同名值。这很适合 Docker、Kubernetes 与 CI/CD:镜像有默认值,平台 Secret 或命令行注入保留最高部署优先级。若生产环境只信任外部变量,可使用 ignoreEnvFile: true。
但 env 装载顺序不等于 ConfigService.get() 的查找顺序。4.x 中,get() 先找内部自定义配置,再找验证后的环境配置,然后才回退到 process.env 和调用方默认值。因此,“进程变量覆盖 .env”不等于它总能覆盖 registerAs() 创建的内部配置。
NestJS 的突出优势是启动期验证:validationSchema 或 validate() 能拒绝错误的端口、缺失的数据库地址或不合法的生产开关,并在边界上完成字符串到 number/boolean 的转换。ConfigService.get<number>('PORT') 的泛型本身并不会执行运行时转换,这一点常被忽略。
registerAs() 还能为数据库、认证或消息队列建立命名空间,并以 ConfigType<typeof config> 注入:
export const databaseConfig = registerAs('database', () => ({
host: process.env.DATABASE_HOST,
port: Number(process.env.DATABASE_PORT ?? 5432),
}));
constructor(
@Inject(databaseConfig.KEY)
private readonly database: ConfigType<typeof databaseConfig>,
) {}
配合 ConfigModule.forFeature(databaseConfig),配置可跟随功能模块注册。不过 partial registration 有生命周期边界:跨模块在构造函数中过早读取配置,可能早于目标模块初始化;这种场景应将读取移到 onModuleInit() 等更安全的阶段。cache: true 主要缓存 ConfigService 对 process.env 的读取,也不应被误解为通用配置缓存。
Vona:把配置放进运行时链路
Vona 的起点不是单个配置包,而是 CLI 确定的运行时维度:
META_MODE:如dev、test、prod;META_FLAVOR:如normal、docker、ci,也可以是项目自定义 flavor。
CLI 会由 mode 衍生 NODE_ENV,并规范化 SERVER_WORKERS:生产模式默认使用 CPU 数量,非生产模式默认是 1。这使 mode/flavor 成为启动前已确定的框架运行时入口,而不是散落在业务代码中的判断字符串。
env 与项目 config 都是级联的
以 prod + docker 为例,env 可以按以下链路选择:
.env → .env.prod → .env.prod.docker
→ .env.local / .env.prod.local / .env.prod.docker.local
.local 是最高优先级的本地覆盖层;生成最终 env 时,已经存在的 process.env 仍可覆盖对应键。项目 config 也采用同样的思路:config.ts → mode → flavor → local。选中的配置函数支持异步执行,随后按确定顺序深度合并,将 env 翻译为 server、logger、Redis、database 等运行时结构。
这让本地开发、Docker 构建、CI 和外部部署注入可以使用同一套优先级心智模型,而不是依赖多份互不相干的启动脚本。
模块默认值和项目覆盖有明确所有权
Vona 模块可在 src/config/config.ts 定义可复用默认值;项目则通过 config.modules['module-name'] 覆盖:
模块默认 config → 当前项目的 config.modules[module-name]
当前模块通过 this.scope.config 读取配置,跨模块通过 this.$scope.<module>.config 读取。配置因此和 service、model、entity、locale 一样成为模块资源:模块作者负责默认能力,项目负责部署或产品差异。对于 suite/module 可复用的复杂系统,这比“所有键都从一个全局 service 取”的约定更容易审计。
ctx.config 是更有区分度的能力
app.config 是应用全局基线;请求进入一个实例上下文后,ctx.config 是实例有效配置:
app.config
→ 静态实例配置
→ 实例记录中持久化的配置
→ ctx.config
这不代表 NestJS 不能实现多租户。NestJS 可以通过 request-scoped provider、中间件和自建 tenant config service 实现同类业务能力。区别在于,典型 NestJS 工程需要自行设计租户解析、配置合并与 datasource 路由;Vona 则让实例解析、有效配置、启动和 datasource 行为共享同一套运行时语义。
当多实例部署、实例隔离或客户级差异是持续需求时,这能减少每个模块各自判断 tenant 的漂移风险;若服务始终只有一份应用配置,额外的 flavor 和实例模型也可能显得过重。
关键差异:类型安全不等于运行时验证
Vona 的配置函数和模块 metadata 能推导配置形状,scope.config 的类型体验很强,能减少字段拼写与跨模块访问错误。但当前中心 env/config 启动路径中,没有看到与 NestJS validationSchema 或 validate() 对应的统一运行时验证阶段。
因此,Vona 项目仍应把端口、数据库、外部服务凭证和生产安全开关视为启动契约:在项目 config 或专门的启动校验边界显式检查。反过来,NestJS 项目即使使用 Joi,也不应让领域配置演变为无归属的 ConfigService.get('...') 字符串调用。
一个更可靠的分层是:
- env 是外部、字符串化的部署输入;
- 验证边界负责拒绝不合法输入并完成转换;
- 项目 config 负责组织应用级运行时结构;
- 模块 config 负责默认值和可复用能力;
- 需要按实例变化的行为,应有明确的请求级有效配置。
NestJS 在第 2 点提供成熟工具;Vona 在第 3、4、5 点提供更完整的框架约定。CabloyJS 的价值不在于替换一个 dotenv 包,而在于把配置提升为可统一理解和审计的运行时架构。
如何选择
若服务主要是单应用、部署形态较少,并且团队重视显式 env 验证与自由组合,NestJS @nestjs/config 是直接而成熟的选择。
若项目长期需要 mode/flavor 驱动的构建与部署、suite/module 默认值与项目覆盖、以及实例级有效配置,CabloyJS/Vona 的体系化约定更有优势。它要求团队接受更多运行时词汇,却也避免团队为每个模块、每个租户和每种部署形态重新发明一套配置规则。
参考资料与源码出处
NestJS
- NestJS Configuration 官方文档
- NestJS:自定义 env 文件路径
- NestJS:配置命名空间
- NestJS:Partial registration
@nestjs/config4.0.4 Release@nestjs/config4.0.4ConfigModule源码@nestjs/config4.0.4ConfigService源码