nest.js / hono.js 一起学!字节团队如何配置多环境攻略!

前言

在构建任何健壮的 Node.js 应用时,配置管理都是一个核心问题。你的应用在开发环境(development)、测试环境(testing)和生产环境(production)需要连接不同的数据库、使用不同的端口号,甚至有不同的日志级别。

本文是咨询了一个在字节 infra 团队的资深开发,然后应用到我们公司生产的一套多环境配置方案,案例包含了 nest.js 和 hono.js 的代码。核心内容包括:

  1. 使用 YAML 文件 实现清晰的 多环境配置
  2. 通过 配置合并 实现"默认值 + 环境覆盖"的灵活机制。(会有一个 default.yaml 是默认变量, 支持别的环境可以覆盖默认值)
  3. 引入 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.yamlconfig/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 声明和校验库。它能确保加载进来的配置:

  1. 结构正确:必须包含哪些字段,哪些字段是可选的。
  2. 类型正确:端口号必须是数字、数据库主机必须是字符串等。

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 {}

总结

通过这套方案,您实现了:

  1. 分层清晰的配置default.yaml (默认值) + [env].yaml (环境覆盖)。
  2. 启动时校验:使用 Zod 确保配置结构和类型完全正确,将配置错误扼杀在摇篮里。
  3. 强大的类型提示Config 类型确保您在代码中获取配置时(例如 configService.get('database.host'))拥有完整的 TypeScript 提示和类型安全。

有了这套专业、健壮的配置管理系统,您的 NestJS 应用将更加稳定和易于维护!

hono.js 环境配置

hono.js 的代码跟上面几乎一致,以下内容要看上面 nest.js 的配置,包含

  • 第一步:定义 YAML 配置文件------让配置说话

    • 默认配置文件:config/default.yaml
    • 环境覆盖文件:config/development.yamlconfig/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 相关框架的教程,本质上也是这套思路,换一个集成的环境而已!

相关推荐
用户4099322502122 小时前
Vue3数组语法如何高效处理动态类名的复杂组合与条件判断?
前端·ai编程·trae
山里看瓜2 小时前
解决 iOS 上 Swiper 滑动图片闪烁问题:原因分析与最有效的修复方式
前端·css·ios
Java水解2 小时前
前端与 Spring Boot 后端无感 Token 刷新 - 从原理到全栈实践
前端·后端
软件技术NINI2 小时前
前端怎么学
前端
O***p6042 小时前
前端体验的下一次革命:从页面导航到“流式体验”的系统化重构
前端·重构
一岁天才饺子2 小时前
XSS挑战赛实战演练
前端·网络安全·xss
Hilaku2 小时前
Canvas 粒子特效:带你写一个黑客帝国同款的代码雨(附源码)😆
前端·javascript·前端框架
文心快码BaiduComate3 小时前
我用文心快码Spec 模式搓了个“pre作弊器”,妈妈再也不用担心我开会忘词了(附源码)
前端·后端·程序员
JH灰色3 小时前
【大模型】-LangChain--stream流式同步异步
服务器·前端·langchain