环境变量与多环境部署
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 按顺序加载:
.env—— 所有环境都加载.env.local—— 所有环境都加载,但被 git 忽略(本地个人配置).env.[mode]—— 指定模式加载(如.env.production).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 # ← 不会暴露给客户端
VITE_开头的变量才会被打包进客户端代码- 其他变量只在构建时可用(Node.js 环境),进不了浏览器
为什么这样设计:防止意外把数据库密码、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
}
效果:
import.meta.env.VITE_API_BASE_URL有补全和类型- 拼错名 IDE 直接红波浪
- 忘配的变量编译时能发现
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. 运行时可变配置:构建后还能改
环境变量是构建时注入的。但有些配置希望部署后也能改,不用重新构建:
- 后台管理系统部署给 N 个客户,每个客户的 API 地址不同
- 临时切换 CDN / feature flag
做法:部署时生成 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"
}
部署流程:
pnpm build用.env.production构建wrangler pages deploy dist把dist/上传到 Cloudflare Pages- Cloudflare 自动分发到全球 CDN
10.1 Cloudflare 上配生产环境变量
Cloudflare Pages 的环境变量面板里配:
VITE_SENTRY_DSN(如果是敏感的)- 非
VITE_前缀的变量(服务端用的)
注意: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 选哪个:
- Cloudflare Pages —— 全球 CDN 节点最多,免费额度大,适合高流量
- Vercel —— UI 更友好,生态丰富(适合 Next.js,Vue 也能用)
- 两者都有免费档够个人项目用
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";
}
}
关键点:
try_files ... /index.html—— SPA 单页必备,不加会导致刷新 404/assets/加长缓存 —— Vite 构建出的 JS/CSS 带 hash 命名,改动自动换 URL
Java 对照:类似 Spring 的 ResourceHandlerRegistry —— 静态资源加缓存头。
13. 部署检查清单
每次上线前走一遍:
- [ ]
.env.production的变量都对 - [ ]
pnpm build本地跑通 - [ ]
pnpm preview本地预览生产构建 - [ ] 生产 URL 不会有
http://localhost残留(grep -r "localhost" dist/) - [ ]
console.log已被 Vite 构建时移除 - [ ] 静态资源 Cache-Control 配对
- [ ] SPA 路由回退配了(
try_files) - [ ] HTTPS 证书有效
- [ ] Sentry / 监控接入能收到
- [ ] 测一个完整业务流程(登录 → 操作 → 退出)
14. 常见坑点
| 现象 | 原因 | 解法 |
|---|---|---|
import.meta.env.VITE_XXX 是 undefined |
没加 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/' |
小练习
- 给 in4vue 建
.env/.env.development/.env.production,各加 2 个变量 - 写
src/types/env.d.ts给变量加类型 pnpm dev和pnpm preview看两种模式下import.meta.env.MODE的区别- 添加一个
.env.staging,pnpm build:staging看输出的 dist 有无变化 - 试着用
wrangler pages deploy部署到 Cloudflare(有免费额度)