Vite环境变量完全指南:从.env加载原理到多环境配置实战
发布时间:2026/10/4 6:36:41 锦皓数字建站

很多用 Vite 的同学项目跑起来之后基本不太会去管.env那一堆文件到底是怎么被加载的反正写上VITE_开头的变量代码里用import.meta.env一读就能用。但真到了要同时管理本地开发、测试环境、生产环境多套配置的时候光会“写变量”是远远不够的。Vite 里的环境变量本质上是构建期的配置注入机制它解决的是同一份代码在不同环境跑出不同行为的问题本地连本地后端测试连测试后端上线连正式后端全程不用改业务代码。这篇文章我不打算对着官方文档复读一遍。我从实际项目的使用角度出发把环境变量的文件优先级、mode 模式、loadEnv的用法、TypeScript 类型提示、多环境切换的完整案例以及那些文档里找不到的踩坑经验一次性讲清楚。适合已经会跑 Vite 项目、但没仔细研究过.env机制的前端同学也适合正在搭建多环境发布流程的工程化负责人。1. 先搞清楚 Vite 环境变量到底解决什么问题1.1 从“改代码切接口”的痛点讲起假设你手头是一个后台管理系统前端要通过 axios 请求后端接口。本地开发时后端地址是http://localhost:8080测试环境是https://test-api.example.com生产环境是https://api.example.com。如果没有环境变量最原始的做法是定义在一个常量文件里export const API_BASE_URL http://localhost:8080然后每次要发布测试包改一次代码提交一次回滚一次发布生产包再改一次再提交一次。这种方案在只有一个人的玩具项目里还能忍但凡有测试、有运维、有多个开发分支早晚会出事故忘了改、改错了、漏提交说不定就把测试环境的接口带上了生产。环境变量的意义就是把这个“改代码”的过程变成“改配置”。你在.env文件里写不同的值构建的时候由 Vite 按当前模式把对应值注入进去。代码里永远只写import.meta.env.VITE_API_BASE_URL不需要关心当前到底是哪个环境。1.2 它和系统 PATH、Node 的 process.env 到底有什么区别很多同学被“环境变量”这个词带偏以为跟 Windows 上配置 Java、Python 那种系统环境变量是一回事。其实关系不大。系统环境变量由操作系统管理进程启动时读取比如PATH、HOME。Java 的JAVA_HOME就是典型例子它影响的是 JVM 能不能被找到。Node 的process.env运行在 Node 环境中的程序可以读取操作系统级的环境变量也可以临时注入比如在终端里执行FOObar vite。Vite 的import.meta.env这是 Vite 在编译时注入到客户端代码里的静态对象。浏览器里本来没有环境变量这个概念Vite 相当于在构建阶段把变量替换成了具体的值因此它只能在 Vite 处理过的模块里用。理解这一点非常关键import.meta.env不是浏览器原生能力也不是运行时读取而是“编译期的文本替换”。所以你在代码里console.log(import.meta.env.VITE_APP_TITLE)打包后在产物里看到的很可能直接就是那句console.log(本地开发环境)。这个机制决定了它天然不适合放会在运行时变化的配置只适合放构建时确定的静态值。2. .env 文件体系加载优先级与 mode 模式的正确理解2.1 五类 .env 文件谁覆盖谁Vite 支持从项目根目录的几个固定文件中加载环境变量。文件名就是一套“规则”常见的有这些文件加载时机用途.env所有模式都会加载放公共配置、默认值.env.local所有模式都会加载但 test 模式除外放本机私有配置通常不进 git.env.developmentdev 模式vite启动放开发环境配置.env.development.localdev 模式但优先级最高本机开发私有覆盖.env.productionbuild 模式默认放生产环境配置.env.test自定义 test 模式放测试环境配置.env.staging自定义 staging 模式放预发布环境配置文件之间的优先级简单概括就是越“具体”的文件优先级越高。如果同一个变量在多个文件里都出现了加载顺序大概是这样.env.[mode].local .env.[mode] .env.local .env举个例子项目根目录有.env和.env.development两个文件里都写了VITE_API_BASE_URL最后开发环境下生效的一定是.env.development里的值。如果还写了一个.env.development.local那它又能把.env.development的值覆盖掉。这个“local 后缀优先级最高”的设计初衷是让每个开发者在本地能盖掉团队的公共配置而不用污染提交记录。也正因为这个性质官方建议.env.local要加到.gitignore里避免不小心把本机私有配置提交出去。一个经常被忽略的细节是.env.local在test 模式下不会被加载。这一点是官方文档明确写过的原因是防止测试环境和本地开发环境互相污染影响测试结果的可靠性。真要是测试时需要某台机器的特殊配置用.env.test.local会更符合预期。2.2 mode、NODE_ENV、--mode 参数之间到底怎么联动这三个概念放在一起特别容易乱。mode是 Vite 当前运行的模式。不传参数的时候vite命令默认是development模式vite build默认是production模式。你也可以自己指定比如执行vite build --mode test那当前模式就变成了testVite 会去加载.env.test这个文件。NODE_ENV是 Node 里约定俗成的一个环境变量很多库都依赖它来区分开发和生产。在vite build时Vite 会把NODE_ENV设为production如果之前没设置的话。这里要特别留意--mode test改的是mode不会把NODE_ENV从production改成test。换句话说构建时你用了自定义模式构建产物依然是以生产模式的标准去做压缩和 tree-shaking这恰恰是大多数时候我们想要的行为。你还可以在.env.staging里手动写一行NODE_ENVproduction这样即便 mode 是staging各种依赖库也会把它当作生产环境来对待。这个手法很多团队在用逻辑是mode 管“加载哪个 env 文件”NODE_ENV管“第三方库按什么心智模式运行”两者可以分开控制。开发模式同理vite启动时modedevelopmentNODE_ENVdevelopment。想跑一个 production 模式的本地预览不要用vite应该先vite build再用vite preview预览构建产物。2.3 vite.config.ts 里怎么拿到环境变量别被 process.env 坑了这是很多人踩过的大坑明明.env文件里写了变量但打开vite.config.ts敲一段console.log(process.env.VITE_XXX)输出却是undefined。原因在于Vite 加载.env文件并把变量暴露到import.meta.env是在配置加载完成之后才做的。你在vite.config.ts里直接操作process.env执行阶段根本还没解析这些文件自然读不到。正确的做法是使用 Vite 提供的loadEnv方法import { defineConfig, loadEnv } from vite import vue from vitejs/plugin-vue export default defineConfig(({ mode }) { const env loadEnv(mode, process.cwd(), ) return { plugins: [vue()], server: { proxy: { /api: { target: env.VITE_API_BASE_URL || http://localhost:8080, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } } } })loadEnv的三个参数分别是当前模式、项目根目录、要加载变量的前缀。第三个参数传空字符串表示不过滤前缀把.env系列文件里的所有变量都加载进来。如果不传默认只加载VITE_前缀的变量。这个能力在配置 dev server 代理时特别有用。你想让本地开发统一走/api前缀然后由 Vite 代理转发到真实后端地址那么这个后端地址最好从.env.development里读而不是硬编码在vite.config.ts里。这样团队里有人换了联调后端只需改环境变量文件即可不需要经过你改配置文件。3. 在代码里读取环境变量从 import.meta.env 到类型提示3.1 import.meta.env 的暴露原则VITE_ 前缀是安全边界Vite 里有一个默认规则只有以VITE_开头的环境变量才会被暴露到客户端代码中。你在.env里写了API_SECRETxxx在vite.config.ts里用loadEnv能读到但浏览器端.env的import.meta.env.API_SECRET永远是undefined。这个设计初看有点绕其实是一个安全边界。前端代码最终会整个打到 bundle 里任何用户打开浏览器 DevTools 都能翻到import.meta.env里出现的所有变量。如果所有变量都不经筛选地暴露很容易把后端密钥、内部服务地址这种敏感信息全都送到浏览器端。所以你在vite.config.ts或服务端可能需要但浏览器不需要的配置放在不带VITE_前缀的变量里在 React/Vue 页面组件、业务模块中要读取的配置统一用VITE_前缀。如果项目里的确需要多个前缀比如既有VITE_也有APP_可以通过envPrefix配置扩展export default defineConfig({ envPrefix: [VITE_, APP_] })但不建议做得太花哨团队项目里统一唯一前缀更利于查找。3.2 给环境变量加上 TypeScript 类型默认情况下import.meta.env.VITE_APP_TITLE在你项目里是string | undefined或者直接any编辑器的补全和拼写检查都约等于零。把变量名写错一个字母等跑起来才发现是undefined很浪费时间。我习惯在项目里维护一个src/vite-env.d.ts或者直接在已有的env.d.ts中扩展类型/// reference typesvite/client / interface ImportMetaEnv { readonly VITE_APP_TITLE: string readonly VITE_API_BASE_URL: string readonly VITE_API_TIMEOUT?: string } interface ImportMeta { readonly env: ImportMetaEnv }这样项目里只要用了import.meta.env.VITE_APP_TITLEIDE 就能给出string类型提示写错键名也会有红色波浪线。需要特别注意的是不要重复声明interface ImportMeta也不要把它塞在declare global外层的模块作用域里否则类型可能覆盖不生效。写完这个文件后如果编辑器没反应把 TS Server 重启一下。3.3 几个能直接抄走的读取封装技巧我踩过很多次“在十来个组件里直接写import.meta.env.VITE_XXX”的坑。后来复盘这种做法有三个问题变量名分散在代码各处统一重命名非常痛苦每个使用点都得处理undefined拿到的原始字符串没有领域语义比如true和true的区别全靠人脑记。所以更推荐的做法是所有环境变量的读取都收敛到一个独立的配置文件里。比如src/config/env.tsinterface AppEnv { title: string apiBaseUrl: string isProd: boolean enableMock: boolean timeout: number } const parseBoolean (value: string | undefined, fallback: boolean): boolean { if (value undefined) return fallback return value true } export const appEnv: AppEnv { title: import.meta.env.VITE_APP_TITLE || 未命名应用, apiBaseUrl: import.meta.env.VITE_API_BASE_URL || /api, isProd: import.meta.env.PROD, enableMock: parseBoolean(import.meta.env.VITE_ENABLE_MOCK, false), timeout: Number(import.meta.env.VITE_API_TIMEOUT || 15000) }业务代码里要配置时永远只从appEnv取。以后如果要调整变量命名、增加默认值、做 type 收窄只需要改这一个文件。团队协作时这也相当于一个配置出口新人接手看一眼这个文件就知道当前应用有哪些运行期开关。有个细节.env里的值全部是字符串不要企图直接写成VITE_ENABLE_MOCKfalse然后当布尔值用。false字符串在 JS 里是 truthy。所以上面parseBoolean的写法是必要的或者统一用true/false并解析字符串。4. 全套实战实现开发、测试、生产三套 API 地址切换前面讲了原理和工具这一节我用一个完整的例子把多环境配置从零到落地的路径走一遍。场景还是那个后台管理系统本地联调、测试验收、生产发布三套接口地址各不同。4.1 准备三个 .env 文件和构建脚本在项目根目录创建三个文件.env.development# 开发环境 VITE_APP_TITLE后台管理系统(开发) VITE_API_BASE_URL/api VITE_ENABLE_MOCKtrue.env.test# 测试环境 VITE_APP_TITLE后台管理系统(测试) VITE_API_BASE_URLhttps://test-api.example.com VITE_ENABLE_MOCKfalse.env.production# 生产环境 VITE_APP_TITLE后台管理系统 VITE_API_BASE_URLhttps://api.example.com VITE_ENABLE_MOCKfalse这里留一个小细节开发环境的VITE_API_BASE_URL没有写完整后端地址而是写成/api。原因是配合 Vite dev server 的 proxy让浏览器请求同源地址避免开发时被 CORS 问题折磨。代理转发那一层我放到 4.2 节。然后修改package.json的 scripts{ scripts: { dev: vite, build: vite build, build:test: vite build --mode test, preview: vite preview } }npm run build默认走生产模式加载.env.productionnpm run build:test通过--mode test加载.env.test。因为vite build会主动把NODE_ENV设成production所以 test 包虽然接口地址是测试环境但构建优化的程度和生产包一致不会出现测试包不压缩的情况。4.2 用 loadEnv 在 vite.config.ts 里做代理本地开发地址写/api之后需要让 Vite 把/api开头的请求转发到真正的后端。这里我不能在vite.config.ts里写死http://localhost:8080因为团队里可能有人要连远程测试后端调联。我把这个转发目标也做成环境变量。在.env.development里再加一行VITE_DEV_PROXY_TARGEThttp://localhost:8080注意这个变量只在开发时需要生产构建用不到。然后在vite.config.ts里用loadEnv读取import { defineConfig, loadEnv } from vite import vue from vitejs/plugin-vue export default defineConfig(({ mode }) { const env loadEnv(mode, process.cwd(), ) return { plugins: [vue()], server: { proxy: { /api: { target: env.VITE_DEV_PROXY_TARGET || http://localhost:8080, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } } } })rewrite那段的作用是把请求路径里的/api前缀去掉。比如浏览器请求/api/login代理转发给后端的是/login。这个按后端实际规范来定不一定都要 rewrite。我用这个方案半年多最大的体会是本地开发时你不用关心后端地址写死在哪后端同事换了端口改一下.env.development就能继续干活不会出现“换个人拉代码就跑不起来”的情况。4.3 请求层封装与前端代码改造接 axios 的时候不要在每个接口文件里到处拼import.meta.env.VITE_API_BASE_URL。我用的是前面创建的src/config/env.ts作为统一出口然后封装一个request.tsimport axios from axios import { appEnv } from /config/env const request axios.create({ baseURL: appEnv.apiBaseUrl, timeout: appEnv.timeout }) request.interceptors.request.use((config) { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) export default request组件里调用接口时export function fetchUserInfo() { return request.get(/user/info) }这样整个业务层完全不感知环境差异。本地开发时baseURL是/api走了 Vite 代理测试和生产构建时baseURL会被替换成真实地址。代码在三种环境下跑的是同一份。4.4 构建、预览与产物验证执行npm run build:test构建结束后在dist目录里搜一下https://test-api.example.com应该能找到替换后的产物。这一步很重要我见过不少“构建完还是旧地址”的案例最后发现要么是忘了先删dist要么是浏览器缓存要么是.env.test压根没提交。如果要本地验证测试包效果用vite previewnpm run preview默认会起一个本地静态服务器预览dist目录的内容。有人会拿npm run dev来验证那不对dev是开发模式不会读.env.test。另外vite preview默认端口是 4173要指定端口就vite preview --port 5000。还有一个容易忽略的地方.env文件本身不会出现在dist里。Vite 只是在构建时读取值并替换到代码中文件不会被打包。所以检查产物时应该查dist/assets/*.js里的字符串值而不是去找.env文件。5. 常见问题与排查技巧实录5.1 高频问题速查表把各个项目里遇到过的问题整理一下症状可能原因排查方向import.meta.env.VITE_XXX是undefined变量没有VITE_前缀写错文件名没重启检查 .env 文件名和前缀重启 dev server修改 .env 后 dev server 没反应Vite 不一定对所有 .env 变更做热更新手动重启 dev server构建后变量是旧值构建前没清缓存浏览器缓存没有指定 mode删 dist、清缓存、确认脚本带 --modevite.config.ts 里读不到变量直接用了 process.env改用loadEnv(mode, process.cwd(), )测试包用了生产地址构建脚本少了--mode test或 .env.test 没配置检查 scripts 和文件是否存在字符串false被当成 trueenv 值全是字符串用 true解析构建产物里出现敏感密钥把密钥写成了 VITE_ 变量改名去掉前缀或移到后端5.2 “修改 .env 不生效”怎么从根上排查这个问题出现频率极高我单独拎出来说。首先是重启。Vite 对.env文件的更新并不可靠开发服务器跑着的时候改了.env很多时候不会自动刷新环境变量。这是设计上的行为别跟它较劲改完直接重启 dev server。其次是文件名和模式对不上。上一个项目我写过.env.testing然后在build --mode test里期待它生效结果当然没生效。Vite 文件名里的后缀必须和--mode完全一致test就对应.env.teststaging就对应.env.staging。一个下划线都不能差。再就是文件位置。.env系列文件必须放在项目根目录也就是执行vite命令时的cwd放到src下、放错层级都会读不到。最后检查高优先级覆盖如果.env和.env.development同时定义了同一个变量改.env自然看不到效果因为.env.development把值盖掉了。排查想加一句console.log(import.meta.env)看当前环境实际拿到什么比瞎猜快得多。5.3 环境变量的安全边界哪些东西千万别放进去很多人第一次建.env.production时顺手把数据库连接串、Redis 密码、对象存储 SecretKey 都写了进去还用的是VITE_前缀。这是非常危险的事。一旦变量带VITE_前缀就一定会被编译进前端代码里。意味着所有访问网站的人打开 DevTools看Sources面板搜索关键词就能找到你的明文密钥。我见过不止一次线上事故就是因为拿前端 bundle 里的 key 直接调了云厂商的管理接口。正确的姿势分两种情况只在前端业务代码中使用的公开配置如 API 地址、应用标题、埋点 ID用VITE_因为它们本来就要暴露。构建期才用、不能被用户看到的密钥例如构建脚本访问内部服务的 Token写在.env里但不带VITE_前缀并用loadEnv在构建配置里读取。后端需要的核心密钥比如数据库密码、JWT 签名私钥不应该进前端项目。放到 CI/CD 平台的 secrets 或部署服务器的环境变量里由后端服务直接读取。另外.env.local、.env.production.local这类 local 文件务必加入.gitignore。它们是留给本机或特定机器用的如果被提交到仓库等于把团队每个人的私有配置和历史遗留密钥都暴露了一遍。我在新项目初始化时一定会顺手把这一行加进去。5.4 在 SPA 架构里动态改接口地址的一种替代思路用.env文件配置环境变量本质是“构建时锁定配置”。这带来一个痛点如果你的产品是给多个客户独立部署的客户 A 和客户 B 需要不同的 API 地址、不同的应用名称那每个客户单独 build 一次维护成本很高。遇到这种场景可以用运行时配置来补充。思路是在public目录下放一个runtime-config.jswindow.__APP_CONFIG__ { apiBaseUrl: https://api.example.com, appTitle: 后台管理系统 }然后代码里配置优先级这样处理const runtime window.__APP_CONFIG__ || {} export const appEnv { title: runtime.appTitle || import.meta.env.VITE_APP_TITLE || 未命名应用, apiBaseUrl: runtime.apiBaseUrl || import.meta.env.VITE_API_BASE_URL || /api }由于public目录里的文件会原样复制到dist根部部署方只要修改这个 JS 对象就能在不重新构建前端的情况下调整关键配置。用 Docker 部署的话这个文件还可以通过环境变量模板动态生成运维同学会很喜欢。这个方法不是要替代.env而是说import.meta.env只解决“构建时确定配置”一类问题。遇到“部署后再决定配置”的需求要用 window 全局注入这类运行时方案去补位。上面这些内容基本覆盖了 Vite 环境变量从原理到实战的完整链路。最后再分享一个习惯我每次在项目里新建.env文件时都会在文件顶部写上两三行注释说明这个文件对应哪个 stage、哪些变量是敏感项、谁负责维护。环境变量文件容易被当成“写了就行”的一次性配置但真正接手项目的人往往就是靠着这几行注释在关键时刻救回一命。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。