前言
在构建任何健壮的 Node.js 应用时,配置管理都是一个核心问题。你的应用在开发环境(development)、测试环境(testing)和生产环境(production)需要连接不同的数据库、使用不同的端口号,甚至有不同的日志级别。
本文是咨询了一个在字节 infra 团队的资深开发,然后应用到我们公司生产的一套多环境配置方案,案例包含了 nest.js 和 hono.js 的代码。核心内容包括:
- 使用 YAML 文件 实现清晰的 多环境配置。
- 通过 配置合并 实现"默认值 + 环境覆盖"的灵活机制。(会有一个 default.yaml 是默认变量, 支持别的环境可以覆盖默认值)
- 引入 Zod 库 ,对配置文件进行 运行时校验,彻底杜绝配置错误导致的线上事故!
如果你喜欢讨论技术,欢迎加入到我们的交流群,主要涉及到全栈前端技术 + ai agent 开发,以下是我的 headless 组件库网站,同时感谢你的 star:
nest.js / hono.js 一起学系列,最终会封装一个通用的架子,例如有鉴权,日志收集,多环境配置等等功能,用两种框架去实现。 之前写了两篇关于这两个框架编程思想相关的
我们先来搞定 nest.js 的环境配置:
nest.js 环境配置
第一步:定义 YAML 配置文件------让配置说话
我们使用 yaml 格式来组织配置文件,因为它简洁、易读。所有的配置文件都放在一个集中的 config 文件夹下。
1. 默认配置文件:config/default.yaml
这是所有环境的基准配置。一些通用或大部分环境相同的配置项都写在这里。
YAML
yaml
# config/default.yaml
# 应用运行的端口号
port: 3000
# 数据库连接信息 - 注意:这里只写了部分通用信息
database:
host: "localhost"
port: 5432
# 数据库连接的额外选项
options:
logging: true
2. 环境覆盖文件:config/development.yaml 或 config/production.yaml
这些文件只包含需要覆盖或新增的配置项。
假设在生产环境,我们需要使用不同的数据库主机和禁用日志:
yaml
# config/production.yaml (生产环境)
database:
host: "prod-db-server.com" # 覆盖 default.yaml 中的 localhost
options:
logging: false # 覆盖 default.yaml 中的 true
💡 核心机制: 当应用以
production环境启动时,它会先加载default.yaml,然后用production.yaml的内容进行深度合并,后者的值会覆盖前者。
第二步:使用 Zod 定义配置结构与校验
配置文件是人手写的,难免出错。如果端口号写成了字符串 "three thousand",或者忘记了数据库密码,应用启动就会失败。
Zod 是一个强大的 TypeScript 声明和校验库。它能确保加载进来的配置:
- 结构正确:必须包含哪些字段,哪些字段是可选的。
- 类型正确:端口号必须是数字、数据库主机必须是字符串等。
src/config/schema.ts
以下代码完全是一个示例,大家明白意思即可。
TypeScript
import { z } from 'zod';
export const DEFAULT_ENV = 'development';
export const PROD_ENV = 'production';
// 定义数据库配置结构 (DatabaseSchema)
const DatabaseSchema = z.object({
host: z.string(), // 必须是字符串
port: z.number().int().positive(), // 必须是正整数
username: z.string(), // 必须提供用户名
password: z.string(), // 必须提供密码
options: z
.object({
timeout: z.number().int().positive().optional(), // 可选,正整数
logging: z.boolean().optional(), // 可选,布尔值
})
.optional(), // options 字段本身是可选的
});
// 定义根配置结构 (ConfigSchema)
export const ConfigSchema = z.object({
env: z.enum([DEFAULT_ENV, PROD_ENV]).default(DEFAULT_ENV), // 只能是 'development' 或 'production'
port: z.number().int(), // 应用端口号
database: DatabaseSchema, // 数据库配置必须符合 DatabaseSchema
redis: z
.object({
host: z.string().optional(),
port: z.number().optional(),
})
.optional(), // Redis 配置是可选的
});
// 导出配置类型,供 NestJS 的 ConfigService 使用,实现完整的类型提示!
export type Config = z.infer<typeof ConfigSchema>;
第三步:实现配置的加载、合并与校验
这是最核心的部分,我们将其封装为一个加载函数,供 NestJS 的 ConfigModule 使用。
src/config/configuration.ts
TypeScript
import { readFileSync, existsSync } from 'fs';
import * as yaml from 'js-yaml'; // 用于解析 YAML 文件
import { join } from 'path';
import { merge } from 'es-toolkit/object'; // 强大的深度合并工具
import { ConfigSchema } from './schema';
const YAML_CONFIG_FILENAME = 'default.yaml';
// 导出一个默认函数,它会返回最终的配置对象
export default () => {
// 决定当前环境,优先使用环境变量 NODE_ENV,否则默认为 'development'
const env = process.env.NODE_ENV || 'development';
// --- 1. 加载默认配置 (default.yaml) ---
const defaultConfigPath = join(process.cwd(), 'config', YAML_CONFIG_FILENAME);
let defaultConfig = {};
if (existsSync(defaultConfigPath)) {
// 读取并解析 YAML 文件
defaultConfig = yaml.load(
readFileSync(defaultConfigPath, 'utf8'),
) as Record<string, any>;
}
// --- 2. 加载环境特定配置 (例如 config/production.yaml) ---
const envConfigPath = join(process.cwd(), 'config', `${env}.yaml`);
let envConfig = {};
if (existsSync(envConfigPath)) {
envConfig = yaml.load(readFileSync(envConfigPath, 'utf8')) as Record<
string,
any
>;
}
// --- 3. 深度合并:环境配置覆盖默认配置 ---
const mergedConfig = merge(defaultConfig, envConfig);
// --- 4. 使用 Zod 进行校验和转换 ---
// .strict() 表示如果配置里有 schema 中未定义的字段,校验也会失败。
const result = ConfigSchema.strict().safeParse(mergedConfig);
if (!result.success) {
// 校验失败时,打印详细错误并抛出异常,阻止应用启动!
console.error('❌ 配置文件校验失败:', result.error.issues);
throw new Error('Config validation failed');
}
// 校验通过,返回最终的、类型安全的配置数据
return result.data;
};
第四步:在 NestJS 应用中加载配置
最后一步,将我们精心准备的配置加载器集成到 NestJS 的 ConfigModule 中。
src/app.module.ts
TypeScript
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import configuration from './config/configuration'; // 导入我们自定义的配置加载器
// ... 其他依赖
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true, // 👈 设为全局,所有模块无需重复导入即可使用
load: [configuration], // 👈 使用自定义加载器加载配置
}),
],
// ...
})
export class AppModule {}
总结
通过这套方案,您实现了:
- 分层清晰的配置 :
default.yaml(默认值) +[env].yaml(环境覆盖)。 - 启动时校验:使用 Zod 确保配置结构和类型完全正确,将配置错误扼杀在摇篮里。
- 强大的类型提示 :
Config类型确保您在代码中获取配置时(例如configService.get('database.host'))拥有完整的 TypeScript 提示和类型安全。
有了这套专业、健壮的配置管理系统,您的 NestJS 应用将更加稳定和易于维护!
hono.js 环境配置
hono.js 的代码跟上面几乎一致,以下内容要看上面 nest.js 的配置,包含
-
第一步:定义 YAML 配置文件------让配置说话
- 默认配置文件:
config/default.yaml - 环境覆盖文件:
config/development.yaml或config/production.yaml
- 默认配置文件:
-
第二步:使用 Zod 定义配置结构与校验
-
第三步:实现配置的加载、合并与校验
第四步有有所不同,我们使用如下方法来加载这些变量:
以下是 hono.js 中的 index.js 也就是程序启动入口
javascript
import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { init } from "./init";
const app = new Hono();
const { config } = await init();
app.get("/", (c) => {
return c.text("Hello Hono!");
});
serve(
{
fetch: app.fetch,
port: config.port,
},
(info) => {
console.log(`Server is running on http://localhost:${info.port}`);
}
);
最关键的代码在于:
javascript
const { config } = await init();
然后,这个 init 会初始化项目需要的内容,所以这里包含了初始化读取环境变量的内容:
javascript
export async function init() {
const config = await loadConfig();
return {
config,
};
}
这个 loadConfig 就是我们上面提到的第三步的代码:第三步:实现配置的加载、合并与校验, 代码上面有,我们这里再次贴出来,大家就能明白全部流程了:
javascript
import { existsSync } from "fs";
import * as yaml from "js-yaml";
import { join } from "path";
import { merge } from "es-toolkit/object";
import { ConfigSchema } from "./schema";
import { readFile } from "fs/promises";
const YAML_CONFIG_FILENAME = "default.yaml";
export async function loadConfig() {
const env = process.env.NODE_ENV || "development";
// 1. 加载默认配置(若文件不存在则使用空对象)
const defaultConfigPath = join(process.cwd(), "config", YAML_CONFIG_FILENAME);
let defaultConfig = {};
if (existsSync(defaultConfigPath)) {
defaultConfig = yaml.load(
await readFile(defaultConfigPath, "utf8")
) as Record<string, any>;
}
// 2. 加载环境特定配置 (例如 config/production.yaml)
const envConfigPath = join(process.cwd(), "config", `${env}.yaml`);
let envConfig = {};
if (existsSync(envConfigPath)) {
const envConfigContent = await readFile(envConfigPath, "utf8");
envConfig = yaml.load(envConfigContent) as Record<string, any>;
}
// 3. 深度合并 (环境配置覆盖默认配置)
const mergedConfig = merge(defaultConfig, envConfig);
// 4. 使用 Zod 进行校验和转换
// 使用 strict() 让 schema 拒绝未定义字段,从而保证 safeParse 在存在多余字段时返回 success: false
const result = ConfigSchema.strict().safeParse(mergedConfig);
if (!result.success) {
console.error("❌ 配置文件校验失败:", result.error.issues);
throw new Error("Config validation failed");
}
console.log("✅ 配置文件校验成功");
return result.data;
}
总结
其实配置多环境更多的是配置思路,跟框架没什么太大的关系,例如后续如果出 bun 相关框架的教程,本质上也是这套思路,换一个集成的环境而已!