深入理解前端应用的环境变量管理

October, 4th 2025 9 min read

目录

  1. 核心概念
  2. 构建时环境变量
  3. 运行时环境变量
  4. 实际案例分析
  5. 我们的解决方案
  6. 最佳实践建议

核心概念

什么是构建时(Build Time)?

  • 定义:执行 npm run buildbun run build 的时候
  • 时机:在开发者的机器上或 CI/CD 环境中
  • 产物:生成静态的 HTML、CSS、JS 文件
  • 特点:一旦构建完成,这些值就被”烘焙”(baked)进代码中,无法更改

什么是运行时(Runtime)?

  • 定义:应用程序实际运行的时候
  • 时机:用户访问网站时,服务器处理请求时
  • 环境:生产服务器、Docker 容器、Vercel、Zeabur 等
  • 特点:可以读取当前环境的变量,动态响应

构建时环境变量

NEXT_PUBLIC_ 前缀的变量

javascript
12345
      // 构建时,Next.js 会将这个替换为实际的值
const apiUrl = process.env.NEXT_PUBLIC_API_URL;

// 构建后的代码实际变成了:
const apiUrl = "https://api.production.com";
    

工作原理

  1. 构建阶段:Next.js 扫描所有代码
  2. 文本替换:将 process.env.NEXT_PUBLIC_* 替换为实际值
  3. 打包输出:生成的 JS 文件中包含硬编码的值

示例流程

bash
123456789
      # 构建时的环境
NEXT_PUBLIC_IMAGE_BASE_URL=https://cdn.production.com

# 执行构建
npm run build

# 生成的代码(简化示例)
// 原代码:const url = process.env.NEXT_PUBLIC_IMAGE_BASE_URL
// 变成了:const url = "https://cdn.production.com"
    

问题所在

1. 不同环境需要不同镜像

yaml
1234567891011
      # 生产环境构建
NEXT_PUBLIC_IMAGE_BASE_URL=https://cdn.production.com npm run build
# 生成 Docker 镜像 A

# 测试环境构建
NEXT_PUBLIC_IMAGE_BASE_URL=https://cdn.staging.com npm run build
# 生成 Docker 镜像 B

# 开发环境构建
NEXT_PUBLIC_IMAGE_BASE_URL=https://cdn.dev.com npm run build
# 生成 Docker 镜像 C
    

2. 每次更改都需要重新构建

  • 修改 CDN 地址?重新构建!
  • 切换环境?重新构建!
  • 临时测试?还是重新构建!

3. 构建时间成本

  • Next.js 构建:3-5 分钟
  • Docker 镜像构建:5-10 分钟
  • 推送到仓库:1-2 分钟
  • 总计:10-20 分钟等待

运行时环境变量

服务端环境变量

javascript
12
      // 服务端代码(API Routes、Server Components)
const apiKey = process.env.API_KEY; // 运行时读取
    

工作原理

  1. 代码保持原样process.env.API_KEY 不会被替换
  2. 运行时读取:每次执行时从当前环境读取
  3. 动态响应:可以根据不同环境返回不同值

客户端的挑战

客户端(浏览器)没有 process.env,所以需要特殊处理:

javascript
123456789
      // ❌ 客户端无法直接使用
const apiKey = process.env.API_KEY; // undefined

// ✅ 需要通过 API 获取
fetch('/api/config')
  .then(res => res.json())
  .then(config => {
    console.log(config.imageBaseUrl); // 运行时配置
  });
    

实际案例分析

之前的问题(构建时配置)

typescript
123456
      // lib/image-url.ts(旧版本)
export function getImageUrl(path: string): string {
  // NEXT_PUBLIC_IMAGE_BASE_URL 在构建时被替换
  const baseUrl = process.env.NEXT_PUBLIC_IMAGE_BASE_URL || '';
  return `${baseUrl}${path}`;
}
    

问题场景

  1. 在本地构建:NEXT_PUBLIC_IMAGE_BASE_URL=https://dev.oss.com
  2. 生成的代码:const baseUrl = "https://dev.oss.com" || '';
  3. 部署到生产环境:仍然使用 https://dev.oss.com

我们的解决方案(运行时配置)

1. 服务端 API 提供配置

typescript
123456789
      // app/api/config/route.ts
export async function GET() {
  // 运行时读取环境变量
  const config = {
    imageBaseUrl: process.env.IMAGE_BASE_URL || '',
  };
  
  return NextResponse.json(config);
}
    

2. 客户端获取配置

typescript
1234567891011121314151617
      // contexts/config-context.tsx
export function ConfigProvider({ children }) {
  const [config, setConfig] = useState(null);
  
  useEffect(() => {
    // 运行时获取配置
    fetch('/api/config')
      .then(res => res.json())
      .then(setConfig);
  }, []);
  
  return (
    <ConfigContext.Provider value={config}>
      {children}
    </ConfigContext.Provider>
  );
}
    

3. 使用运行时配置

typescript
123456789101112
      // lib/image-url.ts(新版本)
export function getImageUrl(path: string): string {
  if (typeof window !== 'undefined') {
    // 客户端:从 window 对象获取运行时配置
    const config = window.__APP_CONFIG__;
    const baseUrl = config?.imageBaseUrl || '';
    return `${baseUrl}${path}`;
  }
  
  // 服务端:可以直接读取环境变量
  return path;
}
    

我们的解决方案

架构图

plaintext
12345678910111213141516171819202122232425262728
      构建阶段:
┌─────────────────┐
│   源代码        │
│                 │
│ 无环境变量硬编码 │
└────────┬────────┘
         ↓
    npm run build
         ↓
┌─────────────────┐
│  Docker 镜像     │
│                 │
│  通用镜像        │
└─────────────────┘

运行阶段:
┌─────────────────────────────────────────┐
│          Docker 容器运行                  │
├─────────────┬──────────────┬─────────────┤
│   开发环境   │    测试环境   │   生产环境   │
├─────────────┼──────────────┼─────────────┤
│ IMAGE_BASE_ │ IMAGE_BASE_  │ IMAGE_BASE_ │
│ URL=dev.com │ URL=test.com │ URL=prod.com│
└─────────────┴──────────────┴─────────────┘
         ↓            ↓             ↓
    读取环境变量  读取环境变量   读取环境变量
         ↓            ↓             ↓
    返回对应配置  返回对应配置   返回对应配置
    

优势对比

方面构建时配置运行时配置
构建次数每个环境一次只需一次
镜像数量N个环境 = N个镜像1个镜像
部署速度需要重新构建(10-20分钟)直接部署(1-2分钟)
灵活性低(需要重新构建)高(修改环境变量即可)
回滚能力需要保存多个版本同一镜像,改变配置
存储成本高(多个镜像)低(单个镜像)
配置错误构建时发现运行时发现

实际收益

  1. 部署提速:从 20 分钟降至 2 分钟
  2. 运维简化:一个镜像管理所有环境
  3. 快速切换:修改环境变量即可切换配置
  4. 应急响应:紧急修改配置无需重新构建

最佳实践建议

1. 选择合适的配置类型

使用构建时配置的场景:

  • 不会改变的常量(如应用名称)
  • 需要在编译时优化的代码
  • 需要 Tree Shaking 的功能开关
javascript
123
      // 适合构建时
const APP_NAME = process.env.NEXT_PUBLIC_APP_NAME;
const ENABLE_ANALYTICS = process.env.NEXT_PUBLIC_ENABLE_ANALYTICS;
    

使用运行时配置的场景:

  • API 端点、CDN 地址
  • 功能开关、A/B 测试配置
  • 第三方服务的配置
javascript
1234
      // 适合运行时
const API_ENDPOINT = getConfig('apiEndpoint');
const CDN_URL = getConfig('cdnUrl');
const FEATURE_FLAGS = getConfig('featureFlags');
    

2. 混合策略

typescript
1234567
      // 构建时:基础配置
const IS_PRODUCTION = process.env.NODE_ENV === 'production';

// 运行时:环境特定配置
const API_URL = IS_PRODUCTION 
  ? getRuntimeConfig('productionApi')
  : getRuntimeConfig('developmentApi');
    

3. 配置验证

typescript
12345678910
      // 启动时验证必需的环境变量
function validateEnv() {
  const required = ['DATABASE_URL', 'API_KEY', 'IMAGE_BASE_URL'];
  
  for (const key of required) {
    if (!process.env[key]) {
      throw new Error(`Missing required env var: ${key}`);
    }
  }
}
    

4. 配置缓存

typescript
12345678910
      // 避免每次都获取配置
let cachedConfig: Config | null = null;

async function getConfig(): Promise<Config> {
  if (!cachedConfig) {
    const res = await fetch('/api/config');
    cachedConfig = await res.json();
  }
  return cachedConfig;
}
    

总结

关键要点

  1. 构建时变量:像烤面包,一旦烤好就定型了
  2. 运行时变量:像自动售货机,根据投币(环境)提供不同商品(配置)
  3. NEXT_PUBLIC_ 前缀:专门用于客户端的构建时变量
  4. 无前缀变量:服务端运行时变量

我们的改造成果

  • ✅ 移除了 NEXT_PUBLIC_IMAGE_BASE_URL 构建时依赖
  • ✅ 实现了运行时配置系统
  • ✅ 一个 Docker 镜像适配所有环境
  • ✅ 部署时间从 20 分钟缩短到 2 分钟
  • ✅ 保持了向后兼容性

记住这个原则

“Build once, deploy anywhere”(构建一次,到处部署)

这就是为什么大型企业都倾向于使用运行时配置而不是构建时配置。配置应该是灵活的,而不是被”烘焙”进代码里的。


最后更新:2025-01-13 作者:Claude AI Assistant 项目:Zetar Mold Production System