Chapter 05

环境变量与多环境

用 .env 文件链管理 API 地址等配置,搭配 import.meta.env 在代码里读取,并在 TS 里得到完整类型补全。

5.1 import.meta.env 是什么

Vite 在 ESM 标准的 import.meta 上挂了一个 .env 对象,所有环境变量都从这里读:

console.log(import.meta.env.MODE);          // 'development' / 'production'
console.log(import.meta.env.DEV);           // boolean
console.log(import.meta.env.PROD);
console.log(import.meta.env.SSR);
console.log(import.meta.env.BASE_URL);      // 相当于 config.base
console.log(import.meta.env.VITE_API_URL);   // 自定义变量
只有 VITE_ 前缀才会注入到客户端

这是安全设计:避免你把 DB_PASSWORDJWT_SECRET 之类的服务端密钥误打包进浏览器 bundle。任何不以 VITE_ 开头的环境变量在客户端代码里都不存在

5.2 .env 文件链

Vite 在项目根按以下顺序加载(后加载的覆盖先加载的):

文件加载时机是否应提交
.env所有模式✅ 可以
.env.local所有模式(仅本地)❌ 放进 .gitignore
.env.[mode]特定模式(development/production/staging...)✅ 可以
.env.[mode].local特定模式(仅本地)❌ 放进 .gitignore
# .env (提交,共享默认值)
VITE_APP_NAME=My App
VITE_API_URL=https://api.example.com

# .env.development (提交,开发环境覆盖)
VITE_API_URL=http://localhost:3000

# .env.local (不提交,个人临时值)
VITE_API_URL=http://192.168.1.100:3000
VITE_MY_TOKEN=sk-xxxxx

5.3 模式(mode)

$ vite               # 默认 mode=development
$ vite build         # 默认 mode=production
$ vite build --mode staging   # 自定义 mode → 加载 .env.staging

典型做法是区分 development / staging / production 三个模式:

package.json
  "scripts": {
    "dev":         "vite",
    "build:stg":   "vite build --mode staging",
    "build:prod":  "vite build"
  }

5.4 TypeScript 类型补全

Vite 默认 import.meta.envImportMetaEnv,只含内置字段。自定义变量要自己补声明:

// src/vite-env.d.ts(脚手架已有,往里加)
/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_APP_NAME: string;
  readonly VITE_API_URL: string;
  readonly VITE_FEATURE_FLAG_NEW_UI: 'on' | 'off';
}

interface ImportMeta {
  readonly env: ImportMetaEnv;
}

现在 import.meta.env.VITE_API_URL 有自动补全,拼错会报错。

5.5 在 vite.config.ts 里读环境变量

配置文件本身是 Node 环境,不能直接用 import.meta.env。用官方 loadEnv

import { defineConfig, loadEnv } from 'vite';

export default defineConfig(({ mode }) => {
  // 第 3 参 '' 表示加载所有前缀,不限 VITE_
  const env = loadEnv(mode, process.cwd(), '');

  return {
    server: {
      port: Number(env.VITE_DEV_PORT) || 5173,
      proxy: {
        '/api': { target: env.VITE_API_URL, changeOrigin: true },
      },
    },
  };
});

5.6 特殊内置变量

import.meta.env.MODE
当前模式字符串。
import.meta.env.DEV / PROD
布尔值,会被 dead-code elimination 消除。例如 if (import.meta.env.DEV) logDebug() 在生产构建里整段被删。
import.meta.env.SSR
当前代码是否运行在服务端(SSR 构建)。
import.meta.env.BASE_URL
配置里的 base 值,默认 /

5.7 运行时 vs 构建时

Vite 的环境变量在 构建时替换——生产包里 import.meta.env.VITE_API_URL 已经是字面字符串。这带来两个影响:

好处:tree-shaking 友好

if (import.meta.env.PROD) {
  initSentry();    // 开发时整段被删,Sentry SDK 不会打进 dev bundle
}

代价:一次构建只对应一套环境

你不能用同一份 dist/ 切换 staging/production 的 API 地址。解决方案:

方案 A:各环境独立构建

$ VITE_API_URL=https://stg.example.com vite build
$ VITE_API_URL=https://api.example.com vite build

方案 B:运行时注入 window.__CONFIG__

index.html 里放一个可被服务端模板替换的全局变量:

<script>
  window.__CONFIG__ = {
    API_URL: "{{ API_URL }}",   // Nginx/Express 部署时替换
  };
</script>

5.8 .env 安全原则

永远不要把密钥放进 VITE_ 变量

前端代码会下发到浏览器——VITE_ 前缀的任何变量都能被用户在 devtools 里看到。API key、JWT secret、数据库密码一律放后端 .env 并通过 API 代理。

.gitignore 应该包含

.env.local
.env.*.local
.env.production.local

团队共享 .env 模板

提交一个 .env.example 作为变量清单,但不写真实值:

# .env.example(提交到 git)
VITE_APP_NAME=
VITE_API_URL=
VITE_SENTRY_DSN=

新人 clone 后 cp .env.example .env.local 再填自己的值。

5.9 全部示例:多环境项目结构

my-app/
├── .env                  # 所有环境共用默认
├── .env.development      # dev
├── .env.staging          # staging
├── .env.production       # prod
├── .env.local            # 个人(不提交)
├── .env.example          # 变量清单模板(提交)
├── .gitignore            # 含 .env.local / .env.*.local
├── src/
└── vite.config.ts

5.10 小结