环境变量与多环境部署

1. Spring Profile 和前端环境变量的对照

Java 后端你早已习惯:

# application-dev.yml
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/dev

# application-prod.yml
spring:
  datasource:
    url: jdbc:mysql://prod-db:3306/app

启动时 --spring.profiles.active=prod 切换。

前端也一样,只不过文件名叫 .env.*

Spring Vite
application.yml .env
application-dev.yml .env.development
application-prod.yml .env.production
@Value("${xxx}") import.meta.env.VITE_XXX
--spring.profiles.active=test vite --mode test

核心区别:前端环境变量会被打包进最终代码,浏览器里用户能看到。不能放密钥


2. Vite 的 .env 文件加载规则

开发时 pnpm dev--mode development)或构建时 pnpm build--mode production),Vite 按顺序加载:

  1. .env —— 所有环境都加载
  2. .env.local —— 所有环境都加载,但被 git 忽略(本地个人配置)
  3. .env.[mode] —— 指定模式加载(如 .env.production
  4. .env.[mode].local —— 指定模式 + 本地

后加载的覆盖先加载的,优先级从低到高:

.env < .env.local < .env.development < .env.development.local

in4vue 的推荐组织

in4vue/
├── .env                    ← 共享默认值(提交到 git)
├── .env.development        ← 开发环境(提交到 git)
├── .env.production         ← 生产环境(提交到 git)
├── .env.local              ← 个人本地(git 忽略)

.gitignore 里:

.env.local
.env.*.local

3. 变量命名:VITE_ 前缀是硬规则

# .env
VITE_API_BASE_URL=https://api.example.com
DATABASE_PASSWORD=secret123  # ← 不会暴露给客户端

为什么这样设计:防止意外把数据库密码、API 私钥等打进前端包。Vite 默认安全。

Java 对照:类似 Spring 的 @ConfigurationProperties(prefix = "app") 强制前缀——但 Vite 更严格,错前缀直接读不到,不会静默失败。


4. 读取环境变量

// TypeScript 里
const apiUrl = import.meta.env.VITE_API_BASE_URL
const isProd = import.meta.env.PROD       // 内置: 生产环境为 true
const isDev = import.meta.env.DEV         // 内置: 开发环境为 true
const mode = import.meta.env.MODE          // 'development' / 'production' / 自定义

console.log('API:', apiUrl, 'mode:', mode)

内置变量(Vite 自动注入)

变量
MODE 当前模式字符串
BASE_URL base 配置值(默认 /
PROD 生产环境 true
DEV 开发环境 true(和 PROD 互斥)
SSR 服务端渲染时 true

5. 类型提示:让 import.meta.env.XXX 有补全

写多了会发现 import.meta.env.VITE_XXXX 没补全、打错名不报错。写一个类型声明:

// src/types/env.d.ts
/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_API_BASE_URL: string
  readonly VITE_APP_TITLE: string
  readonly VITE_AI_PROXY_URL: string
  readonly VITE_SENTRY_DSN?: string // 可选的加 ?
  readonly VITE_ENABLE_MOCK: 'true' | 'false' // 字符串枚举
}

interface ImportMeta {
  readonly env: ImportMetaEnv
}

效果

Java 对照:类似 Spring Boot 的 @ConfigurationProperties + @NestedConfigurationProperty 给 YAML 配置提供类型——IDE 一路提示,写错立刻知道。


6. 类型陷阱:所有环境变量都是字符串

# .env
VITE_ENABLE_MOCK=true
VITE_PORT=3000
// ❌ 不是布尔值!
if (import.meta.env.VITE_ENABLE_MOCK) { ... }
// 无论 =true 还是 =false,字符串都是 truthy

// ✅ 显式比较
if (import.meta.env.VITE_ENABLE_MOCK === 'true') { ... }

// 数字类似
const port = Number(import.meta.env.VITE_PORT)

封装一个工具函数避免重复:

// src/utils/env.ts
export function envBool(key: keyof ImportMetaEnv): boolean {
  return import.meta.env[key] === 'true'
}

export function envNum(key: keyof ImportMetaEnv, fallback = 0): number {
  const n = Number(import.meta.env[key])
  return isNaN(n) ? fallback : n
}

7. in4vue 的分环境配置

.env(通用默认):

VITE_APP_TITLE=in4vue
VITE_ENABLE_MOCK=false

.env.development(开发):

VITE_API_BASE_URL=/api
VITE_AI_PROXY_URL=http://localhost:8787/ai
VITE_ENABLE_MOCK=true

.env.production(生产):

VITE_API_BASE_URL=https://in4vue.pages.dev/api
VITE_AI_PROXY_URL=https://in4vue.pages.dev/ai
VITE_ENABLE_MOCK=false
VITE_SENTRY_DSN=https://xxx@sentry.io/yyy

.env.local(个人,不提交):

# 本地跑真实 AI 接口,覆盖默认的 mock
VITE_ENABLE_MOCK=false
VITE_AI_PROXY_URL=http://localhost:8787/ai

8. 自定义模式:多于两套环境

除了 dev/prod,实战里经常需要 staging(预发)、test(测试服):

# .env.staging
VITE_API_BASE_URL=https://staging.in4vue.com/api
// package.json
{
  "scripts": {
    "build": "vite build",
    "build:staging": "vite build --mode staging",
    "build:test": "vite build --mode test"
  }
}

pnpm build:staging 会加载 .env.staging

Java 对照:类似 -Dspring.profiles.active=staging —— 同一套代码出多套构建产物。


9. 运行时可变配置:构建后还能改

环境变量是构建时注入的。但有些配置希望部署后也能改,不用重新构建:

做法:部署时生成 config.js,前端运行时加载

<!-- public/config.js (部署脚本动态生成) -->
<script>
window.__APP_CONFIG__ = {
  API_BASE: 'https://api.customer-a.com',
  ENABLE_NEW_FEATURE: true,
}
</script>
<!-- index.html -->
<head>
  <script src="/config.js"></script>
  <script type="module" src="/src/main.ts"></script>
</head>
// src/utils/config.ts
interface AppConfig {
  API_BASE: string
  ENABLE_NEW_FEATURE: boolean
}

export const appConfig: AppConfig = {
  ...{
    API_BASE: import.meta.env.VITE_API_BASE_URL,
    ENABLE_NEW_FEATURE: false,
  },
  ...(window as any).__APP_CONFIG__,
}

Java 对照:类似 Spring 的 @RefreshScope + 配置中心(Nacos/Apollo)——部署不变,配置变。


10. 部署:Cloudflare Pages

in4vue 用 Cloudflare Pages,wrangler 已经装好:

// package.json
"scripts": {
  "cf:deploy": "pnpm build && wrangler pages deploy dist --project-name=in4vue --branch=master"
}

部署流程

  1. pnpm build.env.production 构建
  2. wrangler pages deploy distdist/ 上传到 Cloudflare Pages
  3. Cloudflare 自动分发到全球 CDN

10.1 Cloudflare 上配生产环境变量

Cloudflare Pages 的环境变量面板里配:

注意VITE_ 变量在构建时就打进包,Cloudflare 上配没意义——除非你用 Cloudflare 的 CI 构建(此时那里配的才会进到构建过程的 .env)。

推荐VITE_ 变量仍然写在 .env.production 提交到仓库(反正都要打进客户端代码,不算敏感)。敏感的(Edge Function 的 AI Key)放 Cloudflare 环境变量,Edge Function 运行时用 env.OPENAI_KEY 读。

10.2 分支预览

Cloudflare Pages 默认给每个 PR 自动部一个预览站点:https://abc123.in4vue.pages.dev

用法:提 PR → 等待构建 → Cloudflare 在 PR 里评论预览地址 → 别人点链接直接看效果。


11. 部署:Vercel

Vercel 是另一个流行的平台,操作更简单:

pnpm add -g vercel
vercel          # 首次登录 + 配置
vercel --prod   # 部署到生产

Vercel 自动识别 Vite 项目,零配置。环境变量在控制台的 Settings > Environment Variables 里配。

Cloudflare vs Vercel 选哪个

in4vue 选的 Cloudflare,学习阶段两个都试试。


12. 部署:Nginx 自建

传统部署:Nginx 托管 dist/,反向代理 API:

server {
  listen 80;
  server_name in4vue.example.com;

  # 前端静态文件
  root /var/www/in4vue/dist;
  index index.html;

  # SPA 路由回退:任何找不到的路径都返回 index.html
  location / {
    try_files $uri $uri/ /index.html;
  }

  # API 反向代理
  location /api/ {
    proxy_pass http://localhost:8080/;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
  }

  # 静态资源缓存(Vite 带 hash,可以长缓存)
  location /assets/ {
    expires 1y;
    add_header Cache-Control "public, immutable";
  }
}

关键点

Java 对照:类似 Spring 的 ResourceHandlerRegistry —— 静态资源加缓存头。


13. 部署检查清单

每次上线前走一遍:


14. 常见坑点

现象 原因 解法
import.meta.env.VITE_XXXundefined 没加 VITE_ 前缀 变量名改 VITE_XXX
改了 .env 没生效 dev server 不自动重启 env 手动重启 pnpm dev
生产里变量值不对 .env.production 没提交或 CI 不读 确认文件已提交 / CI 有此 env
构建后刷新 404 Nginx 没 try_files 配 SPA 回退
线上图片资源 404 base 路径错 部署到子路径要配 base: '/app/'
生产还是能看到 console.log Vite 没配 drop esbuild.drop: ['console', 'debugger']
.env.local 被 git 推上去了 .gitignore 漏配 .env.local 到 gitignore

15. 子路径部署(进阶)

如果 in4vue 部署到 https://yingjf.me/notes/ 这样的子路径:

// vite.config.ts
export default defineConfig({
  base: '/notes/',
})

Vue Router 也要同步:

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL), // 读 vite 的 base
  routes: [...],
})

资源路径和路由都会自动加前缀。


16. 小决策表

需求 方案
开发和生产用不同 API .env.development / .env.production
本地个人配置 .env.local
添加测试/预发环境 .env.staging + --mode staging
变量让客户端用 VITE_ 前缀
变量打不进客户端 不加 VITE_ 前缀(仅构建时)
部署后还能改 window.__APP_CONFIG__ + config.js
部署到 Cloudflare wrangler pages deploy
部署到 Vercel vercel --prod
自建 Nginx try_files + 反向代理
子路径部署 base: '/xxx/'

小练习

  1. 给 in4vue 建 .env / .env.development / .env.production,各加 2 个变量
  2. src/types/env.d.ts 给变量加类型
  3. pnpm devpnpm preview 看两种模式下 import.meta.env.MODE 的区别
  4. 添加一个 .env.stagingpnpm build:staging 看输出的 dist 有无变化
  5. 试着用 wrangler pages deploy 部署到 Cloudflare(有免费额度)

延伸阅读