在React项目中,我们习惯把接口地址、第三方服务的AppKey这类会随环境变化的配置放进.env文件,避免把测试环境和生产环境的地址硬编码在代码里。但很多开发者在从Create React App迁移到Vite时发现:原来跑得好好的.env文件,换到Vite后变量死活读不到,或者值变成了undefined。问题的根源在于,CRA和Vite对环境变量的加载规则、前缀要求和读取方式完全是两套体系,本文就来详细拆解它们的区别。

一、基本读取方式的差异:process.env与import.meta.env
先看最核心的区别:变量在代码里怎么读出来。CRA基于Webpack,会把process.env.XXX在构建时做静态替换,所以你在CRA项目里是这样写的:
// CRA 中的写法 const apiUrl = process.env.REACT_APP_API_URL; console.log(process.env.REACT_APP_MODE);
而Vite基于原生ESM,浏览器环境中根本没有process这个全局对象,所以Vite选择在编译阶段把import.meta.env替换为具体的对象字面量:
// Vite 中的写法 const apiUrl = import.meta.env.VITE_API_URL; console.log(import.meta.env.VITE_MODE);
需要注意一个隐晦的坑:CRA支持动态访问,比如process.env['REACT_APP_' + name]在某些场景下能工作,而Vite的静态替换要求你必须写完整的属性名,import.meta.env[key]这种动态取值在默认配置下可能拿到的是替换后的快照对象,虽然通常能工作,但官方明确建议使用静态写法以保证可靠性。迁移项目时,全局搜索process.env是必须做的第一步。
二、前缀规则:REACT_APP_与VITE_
两个工具出于安全考虑,都不会把.env文件里的所有变量都暴露给客户端代码,而是要求变量必须带有特定前缀。CRA要求前缀是REACT_APP_,Vite默认要求前缀是VITE_。
如果没有前缀会发生什么?在CRA中,没有REACT_APP_前缀的变量(除了NODE_ENV等少数内置变量)会被直接忽略,客户端代码里读到undefined。在Vite中同理,不带VITE_前缀的变量不会出现在import.meta.env中。所以如果你在.env里写了API_URL=https://ipipp.com/api,两个框架都读不到,必须改成REACT_APP_API_URL或VITE_API_URL。
有趣的是,Vite的前缀是可以自定义的。在vite.config.js中通过envPrefix配置项可以修改默认前缀:
import { defineConfig } from 'vite';
export default defineConfig({
// 自定义环境变量前缀,例如统一使用 APP_ 开头
envPrefix: 'APP_',
});
这在团队协作中有实际价值:如果项目里有CRA和Vite共存的历史包袱,或者想统一一套前缀规范,就可以借助这个配置减少心智负担。CRA没有提供类似的自定义能力,前缀是写死的。
三、多环境文件与加载优先级
两个工具都支持按环境拆分配置文件。CRA支持的文件形式包括.env、.env.local、.env.development、.env.production、.env.test,运行npm run build时默认加载production环境。Vite在此基础上更进一步,支持.env.[mode]的任意自定义模式,并通过--mode参数指定:
# Vite 构建 staging 模式,会加载 .env.staging 文件 vite build --mode staging # CRA 只能通过自定义脚本修改 BUILD_MODE 等变量间接实现
优先级方面,两者规则一致:指定环境的local文件优先级最高,其次是指定环境文件,再次是通用.env.local,最后是.env。也就是.env.development.local会覆盖.env.development,后者再覆盖.env。通常的做法是把各环境通用的默认值放进.env,敏感的本地调试配置放进对应的.local文件,并把所有.local文件加入.gitignore。
还有一个容易被忽略的细节:变量值如果是字符串,不需要加引号;如果包含空格或特殊字符,建议用双引号包裹。另外在.env文件里可以使用变量展开,Vite和CRA的新版本都支持VITE_BASE_URL=$VITE_HOST/api这种引用已定义变量的写法,但CRA的老版本可能不支持,升级时要注意兼容性。
四、类型声明与开发体验
在TypeScript项目中,两边的类型提示写法也不同。Vite提供了官方的类型增强方式,在src目录下创建一个env.d.ts(或vite-env.d.ts)文件:
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL: string;
readonly VITE_APP_TITLE: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
声明之后,在代码里输入import.meta.env.时,编辑器就能自动补全出你定义的所有变量,拼错变量名也会有红线提示。CRA项目则通常借助@types/node中对process.env的类型定义,或者在react-app-env.d.ts的基础上自行扩展ProcessEnv接口,体验相对原始一些。
此外,Vite还内置了几个常用元变量:import.meta.env.MODE表示当前模式(development、production或自定义模式),import.meta.env.DEV和import.meta.env.PROD是布尔值,import.meta.env.BASE_URL对应部署基础路径。CRA对应的是process.env.NODE_ENV和process.env.PUBLIC_URL,功能类似但命名体系完全不同。
五、安全注意事项与迁移检查清单
必须强调一点:不管CRA还是Vite,凡是暴露到客户端的环境变量,本质上都会被打包进最终的JS文件,任何人打开浏览器开发者工具都能看到。所以API密钥、数据库密码这类敏感信息绝对不能放在带前缀的环境变量里。正确的做法是:客户端只存放公开的接口地址和展示类配置,真正的密钥放在后端服务或CI/CD的私有环境中,由服务端代理使用。
如果你的Vite配置文件本身需要读取环境变量(比如根据环境决定代理目标),可以在vite.config.js中使用loadEnv:
import { defineConfig, loadEnv } from 'vite';
export default defineConfig(({ mode }) => {
// 第三个参数传入空字符串可以读取不带前缀的变量
const env = loadEnv(mode, process.cwd(), '');
return {
server: {
proxy: {
'/api': {
target: env.LOCAL_PROXY_TARGET || 'http://127.0.0.1:3001',
changeOrigin: true,
},
},
},
};
});
最后给出一份从CRA迁移到Vite时的环境变量检查清单:第一,全局替换process.env.REACT_APP_为import.meta.env.VITE_;第二,检查.env文件中所有变量名的前缀并批量修改;第三,重命名环境文件,确认.env.development和.env.production的语义在新项目中保持一致;第四,为import.meta.env补充TypeScript类型声明;第五,检查代码中是否有动态拼接变量名的访问方式并改为静态写法。完成这几步,环境变量迁移基本就不会再出问题了。