Vite 环境变量和构建配置入门

本地构建时 BASE_URL 写的是线上域名,CSS 里硬编码打进 bundle,后台接口地址全错,排查花了俩小时。这篇记 Vite 的环境变量命名规矩、按环境分文件、自定义 mode,以及一次把真密钥写进 env 文件的翻车。
第一次部署踩到的环境变量错
我这站后端 BASE_URL 写成 https://www.tiana.com.cn,本地 dev 时也是这个值。结果本地跑 npm run build 的时候,CSS 里硬编码把这个域名打进 bundle,nav 跳转看着正常,但后台接口地址错了。
排查路径现在说起来三句话就完事,当时花了我俩小时。原因是项目里没有区分环境,所有 BASE 全部走「相对路径」拼「后端 IP 端口」写死,本地开发走 8123 端口,后端 dev 还启在 8000,前端直接写 http://127.0.0.1:8000/api。
我说这些不是为了抒怀,而是环境变量本来就是用来解决这种鸡毛蒜皮的错的。但仍然有一堆人栽在这一步。
Vite 的环境变量命名规矩
Vite 跟传统 webpack 的 process.env 不一样,它用 import.meta.env。只有 VITE_ 开头的变量会被打包进客户端代码。
# .env.development
VITE_API_BASE_URL=http://127.0.0.1:8000
VITE_SITE_NAME=本地博客
# .env.production
VITE_API_BASE_URL=https://www.tiana.com.cn
VITE_SITE_NAME=Tiana 随笔代码里这样读:
const baseURL = import.meta.env.VITE_API_BASE_URL
const siteName = import.meta.env.VITE_SITE_NAME拼接到 fetch 上:
async function listPosts(page = 1) {
const r = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/blog/posts/list`, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ page, page_size: 20 }),
})
return (await r.json()).data
}只读 VITE_ 前缀这点是安全边界,不是我给你友情提示。SPA 所有代码最终都是公开的,没设前缀的变量连 bundle 都不会进,假设你在 .env.production 里写了 DATABASE_PASSWORD=xxx,构建出来文件里也没有。捡了芝麻丢了西瓜的事 Vite 已经帮你做了。
一个 .env 文件盯一类环境
我倾向于每个环境一个文件:
.env # 所有环境共享,最低优先级
.env.development # npm run dev 默认加载
.env.production # npm run build 默认加载
.env.staging # 自定义环境,--mode staging 时加载加载优先级是 mode > .env.<mode> > .env。相同 key 后加载的覆盖前加载的。
自定义 mode 给 SEO 渲染用
我这站首页要服务爬虫,我额外加了一个 mode:
# .env.crawler
VITE_RENDER_MODE=ssr-fakevite build --mode crawler这样打出来的 bundle 跟生产几乎一致,只是 embedding 了一段标记让后端知道这份产物是爬虫版。比起维护两套几乎一样的代码,mode 切换更省心。
一个真实翻车:环境变量里藏了真密钥
我刚开始做后台对接时,图省事把管理员 token 放在了 .env.production。结果构建出来之后,所有人打开浏览器的 devtools,都能 grep 到 VITE_ADMIN_TOKEN。
原因很简单:所谓「前端环境变量」其实是构建时写死在 bundle 里的常量,不是运行时由服务器发的。前端能拿到的值就是公开的。我把能进 bundle 的东西分了三类:
- 安全边界明确的:公开配置。
API_BASE_URL、SITE_NAME、CDN 域名、是否启用某种 feature - 半公开:构建时敏感配置。比如第三方分析 ID、错误监控的 dsn。理论上公开但有滥用的可能
- 严格敏感:前端永远不能有。数据库密码、签名密钥、admin token、OAuth client secret。第三类必须留在后端
第一类的 subset 全部走 VITE_ 前缀。后两类的 subset 你不要打给前端,哪怕你用了某个不那么暴露的命名。如果非要给前端管理 token,正确做法是后端代前端鉴权,前端发送不带 token 的 cookie。
顺手的另一件事:__VITE_IS_MODERN__
Vite 在打包时识别旧浏览器的能力让我栽过一次。我用了某个新语法,但版本没设对,老用户拿到的 bundle 里语法没转译就崩了。
import { isModern } from './utils'这里 isModern 源码是 const isModern = true,Vite 把它替换成字面量。问题是我以为打包时所有用户都拿同一份,结果老浏览器拿到的是替换后的常量,没办法分版本。
解决办法是用 Vite 自带的 build.target:
// vite.config.js
export default defineConfig({
build: {
target: 'es2020',
},
})如果非要兼容更老的浏览器,分两份 build:一份 modern(es2020+),一份 legacy(es5)。Vite 默认就支持,分文件输出就行。维护成本是个 trade-off:你的用户统计里 50% 是 IE11 吗?不是的话只发 modern 版本。
最后一个习惯:env.example
新同事入职都得 clone 项目才能跑起来。我留了一份 .env.example 在仓库里,里面是允许 commit 的所有 key(值留空)。这样新同事 clone 之后 cp .env.example .env.local,再填真值就能跑。
我本地开发值永远写在 .env.local 而不是 .env.development,因为 .env.local 默认被 git ignore,每个人填自己的值,不会互相污染。.env.development 写团队共同的部分,比如 API 默认走 8000 端口。
现在我对环境变量的态度
它就是项目的「配置文件」。能帮构建时区分环境、能帮团队共享 key、能让新人快速上手。但永远别把任何重要密钥写进会在浏览器里跑出来的代码,这事想清楚了就不再犯。