三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

UniApp+Vue3+Vite环境变量配置实战:多端构建与安全部署指南

UniApp+Vue3+Vite环境变量配置实战:多端构建与安全部署指南

1. 项目概述:为什么环境变量是跨端开发的“命门”?

最近在带几个新人做UniApp项目,发现一个挺普遍的问题:大家本地开发跑得好好的,一到测试或生产环境,接口地址、AppID、密钥这些就全乱套了,要么就是打包后配置没生效,要么就是不同环境配置混在一起。追根溯源,问题往往出在环境变量配置这个基础环节上。很多人觉得这不过是几个配置文件,照着教程配一下就行,但真到了多环境、跨平台(H5、小程序、App)的UniApp + Vue3 + Vite项目里,这里面的门道可不少。

环境变量本质上是一套“运行时注入”的配置管理方案。它允许我们将与环境相关的配置(如API基地址、调试开关、第三方密钥)从代码中剥离出来,实现“一份代码,多处部署”。对于UniApp这种一次开发、多端发布的框架来说,这一点尤为重要。想象一下,你的应用需要对接的后端API,在开发时可能是http://localhost:3000,测试环境是https://test-api.example.com,而上线后则是https://api.example.com。如果这些地址硬编码在代码里,每次切换环境都得改代码、重新打包,不仅效率低下,而且极易出错。

Vue3 + Vite的组合为现代前端开发带来了极致的开发体验,其基于ES模块的原生支持使得模块热更新(HMR)速度飞快。Vite在处理环境变量时,也有一套自己的约定和构建时替换机制。但UniApp作为一个上层框架,它对构建流程有自己的一套封装和扩展,特别是在处理多平台(如微信小程序、App)时,其构建目标和过程与纯Web项目有所不同。这就导致了一个常见的困境:直接套用Vite或Vue CLI那套环境变量配置方法,在UniApp里可能行不通,或者只在H5端生效,到了小程序和App端就“失灵”了。

因此,理清在UniApp + Vue3 + Vite技术栈下,环境变量如何正确配置、如何在代码中安全获取、以及如何适配多端构建,就成了一个必须扎实掌握的核心技能。这不仅仅是配几个文件那么简单,它关系到项目的可维护性、团队协作的规范性以及最终交付的可靠性。接下来,我就结合最近几个项目的实战经验,把这套配置体系的思路、具体做法和踩过的坑,系统地梳理一遍。

2. 环境变量配置的核心思路与方案选型

在开始动手写配置之前,我们必须先想清楚目标:我们需要一套怎样的环境变量管理方案?结合UniApp多端发行的特点,我认为一个理想的方案需要满足以下几个核心需求:

  1. 环境隔离:清晰地区分开发(development)、测试(staging)、生产(production)等不同环境,互不干扰。
  2. 多端一致:配置方案需要在H5、各家小程序(微信、支付宝等)、App(iOS/Android)等所有UniApp支持的目标平台上都生效。
  3. 安全可控:敏感信息(如密钥)不应出现在前端代码仓库中,而应通过安全的渠道注入。
  4. 开发友好:在开发时能方便地切换和预览不同环境的效果,且支持热更新。
  5. 构建集成:能无缝融入Vite的构建流程,并正确参与UniApp特有的编译过程。

基于这些需求,直接使用Vite原生环境变量(.env文件)是起点,但并非终点。Vite使用dotenv从项目根目录的.env文件中加载环境变量,并通过import.meta.env对象暴露给客户端代码。这是Vite的标准做法,在纯Web项目中工作良好。

然而,UniApp的构建过程比纯Web项目复杂。当你运行npm run dev:mp-weixin开发微信小程序时,UniApp CLI会调用Vite(如果你配置了Vite模式)进行源码编译,但最终生成的是小程序的代码结构。在这个过程中,Vite的环境变量替换是发生在源码编译阶段的。问题在于,UniApp编译到不同平台时,可能会对源码进行特定的转换和封装,import.meta.env这个ES模块的元属性在某些平台(特别是小程序环境,其JavaScript运行环境并非标准的浏览器或Node)可能无法被正确识别或访问。

因此,更稳健的方案是采用一种“双轨制”或“适配层”的思路:

  • 构建时注入:利用Vite的define配置,将环境变量在构建时静态替换为具体的值。这样,最终生成的代码里直接就是字符串常量,不依赖于运行时的import.meta.env对象,兼容性最好。
  • 运行时封装:同时,我们也可以创建一个统一的配置模块,根据构建模式或平台特性,安全地读取环境变量,并提供统一的API给业务代码使用。

经过多个项目的实践,我总结出一套以“Vite环境变量文件为源,通过define进行构建时替换,并辅以统一配置模块”为核心的配置方案。这套方案能较好地平衡灵活性、兼容性和安全性。

2.1 方案对比与决策

在具体实施前,我们简单对比几种常见做法:

方案优点缺点适用场景
import.meta.envVite原生支持,简单直接在小程序/App端可能无法访问;变量值在构建后仍可能被查看(非敏感信息可接受)纯H5项目或仅用于非敏感、非关键的配置
Vitedefine替换构建时静态替换,生成字面量,兼容性极佳;可混淆敏感值需要预先明确所有变量名;热更新需要重启服务(修改.env文件时)UniApp多端项目推荐,尤其适合需要跨平台稳定运行的配置
运行时HTTP请求加载配置可动态更新,无需重新打包增加首屏加载依赖和复杂度;需要处理加载失败和等待状态配置需要频繁变动的后台管理系统或微前端场景
平台条件编译UniApp原生支持,可针对不同平台写死不同配置配置散落在代码中,难以维护;无法根据构建环境(dev/prod)切换仅用于平台特性差异极大的配置,不推荐用于环境变量

对于大多数UniApp项目,“Vitedefine替换”为主,“统一配置模块”为辅的方案是最佳实践。它确保了配置在构建阶段就被确定并固化到产物中,避免了运行时的兼容性问题,同时通过配置模块提供了清晰的接口。

3. 项目结构与环境变量文件设计

明确了方案,我们先来规划项目的目录结构和环境变量文件。一个清晰的结构是后续一切操作的基础。

3.1 目录结构规划

我建议在项目根目录下创建env文件夹(或直接放在根目录),专门管理环境变量相关文件。一个典型的结构如下:

your-uniapp-project/ ├── env/ # 环境变量目录 │ ├── .env.development # 开发环境 │ ├── .env.staging # 测试环境 │ ├── .env.production # 生产环境 │ └── .env # 所有环境的默认值(可选) ├── src/ ├── vite.config.ts # Vite 配置文件 ├── manifest.json # UniApp 应用配置 └── package.json

注意.env文件通常包含敏感信息,务必将其添加到.gitignore中,避免提交到代码仓库。可以将.env.example.env.local(仅包含变量名,不含真实值)提交,供团队成员参考。

3.2 环境变量文件内容示例

每个.env.[mode]文件对应一种构建模式(mode)。Vite默认会根据你运行的命令(如vitevite build)自动加载对应的文件。在UniApp中,我们需要在package.json的 scripts 里指定模式。

env/.env.development(开发环境)

# 开发环境配置 VITE_APP_TITLE = '我的应用(开发版)' VITE_API_BASE_URL = 'https://dev-api.example.com' VITE_APP_DEBUG = true VITE_WEIXIN_APPID = 'wx1234567890abcdef' # 开发用小程序AppID

env/.env.staging(测试环境)

# 测试环境配置 VITE_APP_TITLE = '我的应用(测试版)' VITE_API_BASE_URL = 'https://staging-api.example.com' VITE_APP_DEBUG = true VITE_WEIXIN_APPID = 'wxstaging1234567890'

env/.env.production(生产环境)

# 生产环境配置 VITE_APP_TITLE = '我的应用' VITE_API_BASE_URL = 'https://api.example.com' VITE_APP_DEBUG = false VITE_WEIXIN_APPID = 'wxproduction1234567890'

关键规则说明:

  1. 变量命名:为了在客户端代码中能够被Vite捕获并处理,自定义环境变量必须以VITE_开头。这是Vite的强制约定,否则变量不会被载入import.meta.env
  2. 值类型:等号右边的值会被解析为字符串。truefalse在代码中获取时会是字符串"true""false",需要自行转换。
  3. 模式覆盖:Vite启动时,会先加载.env文件(所有模式共享),然后根据--mode指定的模式加载对应的.env.[mode]文件,后者会覆盖前者的同名变量。

4. 核心配置:Vite与UniApp的融合

这是整个配置过程中最关键的一步,我们需要修改vite.config.ts文件,让Vite在构建UniApp时,正确地将环境变量“注入”到最终代码中。

4.1 修改vite.config.ts配置文件

首先,需要安装@types/node以便在Vite配置中使用process等Node.js模块(如果尚未安装):

npm install -D @types/node

然后,在vite.config.ts中,我们需要做两件事:

  1. 使用loadEnv函数加载指定模式的环境变量。
  2. 通过define选项,将环境变量定义为全局常量。
// vite.config.ts import { defineConfig, loadEnv } from 'vite'; import uni from '@dcloudio/vite-plugin-uni'; import path from 'path'; // https://vitejs.dev/config/ export default defineConfig(({ mode, command }) => { // 1. 加载环境变量 // process.cwd() 返回项目根目录 // 第三个参数 '' 表示加载所有以 `VITE_` 开头的变量 const env = loadEnv(mode, process.cwd() + '/env', ''); // 注意:我们指定了env目录 // 2. 准备需要注入的 define 对象 const define = {} as Record<string, any>; // 遍历所有以 VITE_ 开头的环境变量,将其注入 for (const key in env) { if (key.startsWith('VITE_')) { // 注意:这里值需要 JSON.stringify,因为 define 是做字符串替换 // 例如,VITE_API_BASE_URL: 'https://api.com' 会被替换为 "https://api.com" define[`import.meta.env.${key}`] = JSON.stringify(env[key]); } } // 也可以注入一些通用的、非 VITE_ 前缀的变量或方便使用的别名 define['import.meta.env.MODE'] = JSON.stringify(mode); define['import.meta.env.PROD'] = JSON.stringify(command === 'build'); define['import.meta.env.DEV'] = JSON.stringify(command !== 'build'); return { plugins: [uni()], // 3. 定义全局常量替换 define, // 其他配置,如resolve.alias... resolve: { alias: { '@': path.resolve(__dirname, 'src'), }, }, }; });

这段配置的核心逻辑解释:

  • loadEnv(mode, process.cwd() + '/env', ''):从项目根目录下的/env文件夹中,加载对应模式(mode)的环境变量。''作为第三个参数意味着我们只加载前缀为''(即所有)的变量,但实际上我们后续通过if (key.startsWith('VITE_'))进行了过滤,这是一种更灵活的控制方式。你也可以直接写loadEnv(mode, process.cwd() + '/env', 'VITE_')来只加载VITE_开头的变量。
  • define对象:这是Vite的配置项,它会在构建阶段,将代码中出现的define对象的键(如import.meta.env.VITE_API_BASE_URL)直接替换为对应的值(如"https://api.example.com")。这是一个静态文本替换的过程,替换后的代码里不再有import.meta.env这个引用,因此兼容性极高。
  • JSON.stringify():这是必须的。因为define是做简单的字符串替换。如果不加JSON.stringify(),假设env[key]https://api.com,替换后代码会变成import.meta.env.VITE_API_BASE_URL = https://api.com,这缺少引号,会导致语法错误。经过JSON.stringify()后,值变成了"https://api.com"(带双引号的字符串),替换后代码语法正确。

4.2 修改package.json的 scripts

接下来,我们需要修改package.json中的启动和构建脚本,通过--mode参数指定要使用的环境模式。

{ "scripts": { "dev:h5": "uni -p h5 --mode development", "build:h5": "uni build -p h5 --mode production", "dev:mp-weixin": "uni -p mp-weixin --mode development", "build:mp-weixin": "uni build -p mp-weixin --mode production", "dev:app": "uni -p app --mode development", "build:app": "uni build -p app --mode production", // 可以添加自定义模式,如测试环境 "build:staging:h5": "uni build -p h5 --mode staging", "build:staging:mp-weixin": "uni build -p mp-weixin --mode staging" } }

关键点:

  • --mode development:告诉Vite使用development模式,从而加载env/.env.development文件。
  • --mode production:对应加载env/.env.production
  • --mode staging:对应加载env/.env.staging(这是我们自定义的模式)。

现在,当你运行npm run dev:mp-weixin时,Vite就会加载开发环境的变量并注入到代码中。

5. 在代码中安全、优雅地使用环境变量

配置好了构建过程,接下来就是在业务代码中使用了。虽然经过define替换后,我们可以直接使用import.meta.env.VITE_XXX,但为了更好的类型提示、默认值处理和统一管理,我强烈建议创建一个专门的配置模块。

5.1 创建统一的环境配置模块

src目录下创建config文件夹,并新建env.ts文件:

// src/config/env.ts /** * 应用运行环境类型 */ export type AppEnv = 'development' | 'staging' | 'production'; /** * 获取当前构建模式 * 通过 import.meta.env.MODE 获取,该值已在 vite.config.ts 中通过 define 注入 */ export const getEnvMode = (): AppEnv => { const mode = import.meta.env.MODE as string; if (['development', 'staging', 'production'].includes(mode)) { return mode as AppEnv; } // 默认返回生产环境,确保线上安全 return 'production'; }; /** * 应用配置对象 * 所有环境变量在此集中定义,并提供类型安全和默认值 */ const appConfig = { // 应用标题 title: import.meta.env.VITE_APP_TITLE || 'UniApp', // API 基础地址 apiBaseUrl: import.meta.env.VITE_API_BASE_URL || '', // 是否调试模式 isDebug: (import.meta.env.VITE_APP_DEBUG || 'false') === 'true', // 微信小程序 AppID (如果需要) weixinAppId: import.meta.env.VITE_WEIXIN_APPID || '', // 当前环境模式 mode: getEnvMode(), // 是否是生产环境 isProd: import.meta.env.PROD === 'true', // 是否是开发环境 isDev: import.meta.env.DEV === 'true', } as const; // 导出一个冻结的对象,防止意外修改 export default Object.freeze(appConfig);

这个模块的优势:

  1. 类型安全:为配置对象提供了明确的类型定义。
  2. 默认值处理:避免了环境变量未定义导致的undefined错误。
  3. 逻辑转换:将字符串类型的VITE_APP_DEBUG转换为布尔值isDebug,使用起来更直观。
  4. 集中管理:所有配置在一个地方,方便查找和修改。
  5. 只读保证:通过Object.freeze防止运行时意外修改配置。

5.2 在业务代码中使用配置

现在,在Vue组件、Composables或工具函数中,你可以像下面这样使用:

<script setup lang="ts"> import { ref, onMounted } from 'vue'; import envConfig from '@/config/env'; // 使用别名@指向src // 直接使用配置 const appTitle = ref(envConfig.title); const apiBaseUrl = envConfig.apiBaseUrl; onMounted(() => { console.log(`当前环境: ${envConfig.mode}`); console.log(`API地址: ${apiBaseUrl}`); if (envConfig.isDebug) { console.warn('当前处于调试模式,请注意控制台信息。'); } // 发起网络请求示例 fetch(`${apiBaseUrl}/user/profile`) .then(res => res.json()) .then(data => console.log(data)); }); </script> <template> <view> <text>{{ appTitle }}</text> <!-- ... --> </view> </template>

5.3 处理平台特定配置

有时,某些配置可能因平台而异。例如,微信小程序的AppID在H5环境下是无用的。我们可以在配置模块中增加平台判断逻辑。UniApp提供了uni.getSystemInfoSync().platform或条件编译。

方法一:运行时判断(适用于逻辑简单的场景)

// 在 env.ts 中补充 import { getSystemInfoSync } from 'uni-get-system-info'; // 或使用 uni.getSystemInfoSync const systemInfo = getSystemInfoSync(); const isWeixinMiniProgram = systemInfo.platform === 'devtools' || systemInfo.platform === 'wechat'; // 需精确判断 export const platformConfig = { // 可以在这里根据平台返回不同的值 getAppId() { if (isWeixinMiniProgram) { return envConfig.weixinAppId; } return ''; } };

方法二:条件编译(更彻底,代码更清晰)条件编译是UniApp的强项,适合平台差异大的配置。

// src/config/env.ts let platformSpecificApi = ''; // #ifdef H5 platformSpecificApi = '/h5-api-proxy'; // #endif // #ifdef MP-WEIXIN platformSpecificApi = 'https://api.weixin.qq.com'; // #endif // #ifdef APP platformSpecificApi = envConfig.apiBaseUrl; // App可能用同一个 // #endif const appConfig = { // ... 其他配置 platformApi: platformSpecificApi, };

重要提示:条件编译的注释 (// #ifdef) 是UniApp编译器识别的特殊注释,它们会在编译到特定平台时被保留或移除。这确保了最终每个平台的代码只包含自己需要的部分。

6. 多环境构建与部署实战

配置写好了,最终我们要打包部署。不同环境对应不同的命令和产物。

6.1 配置 package.json scripts

如前所述,我们在package.json中已经配置了不同模式和平台的脚本。例如:

  • 开发微信小程序npm run dev:mp-weixin(使用.env.development)
  • 构建生产环境H5npm run build:h5(使用.env.production)
  • 构建测试环境微信小程序npm run build:staging:mp-weixin(使用.env.staging)

6.2 构建产物验证

构建完成后,如何确认环境变量正确注入了呢?由于define是静态替换,我们可以检查构建出的代码文件。

  1. 对于H5:检查dist/build/h5目录下的.js文件。搜索你定义的变量名,例如VITE_API_BASE_URL,你应该会发现它已经被替换成了具体的字符串值,而不是import.meta.env.VITE_API_BASE_URL
  2. 对于小程序:检查dist/dev/mp-weixindist/build/mp-weixin目录下的.js文件(如app.js或页面js)。同样搜索变量名,确认已被替换。

一个快速验证的方法:在你的配置模块env.ts中,添加一个在控制台打印配置的语句(仅在开发环境):

if (import.meta.env.DEV) { console.log('[Env Config]', appConfig); }

在开发时,浏览器或开发者工具控制台会输出完整的配置对象。在构建生产包时,由于import.meta.env.DEV被替换为false,这段代码不会被执行,不会泄露信息。

6.3 敏感信息处理与安全建议

绝对不要将真实的密钥、密码等敏感信息提交到代码仓库,即使是测试环境的。

  1. 使用.env.local或个人环境文件
    • 创建.env.local文件,并加入.gitignore
    • vite.config.ts中,可以调整loadEnv逻辑,优先加载本地覆盖文件。
    // 简化示例:Vite本身支持 .env.local 覆盖 .env // 确保 .gitignore 包含 .env.local const env = loadEnv(mode, process.cwd() + '/env', '');
  2. CI/CD 管道注入
    • 在 Jenkins、GitLab CI、GitHub Actions 等持续集成/部署平台中,将敏感信息设置为流水线的“机密变量”。
    • 在构建脚本中,通过命令行参数或环境变量动态生成.env.production文件。
    # 示例:在CI脚本中 echo "VITE_API_BASE_URL=$PRODUCTION_API_URL" > ./env/.env.production echo "VITE_APP_KEY=$PRODUCTION_APP_KEY" >> ./env/.env.production npm run build:h5
  3. 后端代理与鉴权
    • 最安全的做法是,所有涉及敏感操作或数据的请求,都不应该依赖前端环境变量中的密钥。前端只持有用于标识自身(如AppID)的非敏感信息,具体的密钥应由后端服务器保管,前端通过安全的鉴权流程(如OAuth 2.0)获取访问令牌。

7. 常见问题、排查技巧与实战心得

在实际项目中,环境变量配置看似简单,却容易遇到各种“坑”。下面是我总结的一些典型问题及解决方法。

7.1 问题排查清单

问题现象可能原因排查步骤与解决方案
环境变量undefined1. 变量名不是以VITE_开头。
2..env文件未放在正确目录,或文件名与--mode不匹配。
3.vite.config.ts中的loadEnv路径或前缀参数错误。
4.define配置未正确注入该变量。
1. 检查变量名,确保以VITE_开头。
2. 检查package.json脚本中的--mode参数,确认对应的.env.[mode]文件存在且路径正确。
3. 在vite.config.tsconsole.log(env)打印加载的环境变量对象,确认是否加载成功。
4. 检查define对象中是否包含了该变量的键值对。
H5正常,小程序/App异常1. 代码中直接使用import.meta.env.VITE_XXX,但该对象在小程序环境不可用。
2. 未使用define进行构建时替换。
确保使用了define配置。这是解决多端兼容性的关键。构建后检查小程序产物的js文件,确认变量已被替换为字面量。
修改.env文件后,热更新不生效Vite 的环境变量热更新仅在服务启动时读取。define配置的替换是静态的。重启开发服务器。修改.env文件后,需要停止并重新运行npm run dev:*命令。
类型错误:import.meta.env上不存在属性TypeScript 无法识别自定义的VITE_变量。src目录下创建env.d.ts文件进行类型声明:
interface ImportMetaEnv { readonly VITE_APP_TITLE: string; readonly VITE_API_BASE_URL: string; }
构建后,代码中仍存在process.env项目中可能混用了Webpack或Node.js的process.env写法。UniApp + Vite 项目应统一使用import.meta.env。全局搜索process.env并替换。Vite的define也可以用于替换process.env.NODE_ENV等。
不同开发者本地环境不一致.env.development文件被意外提交并覆盖,或者各自本地有未跟踪的配置。1.确保.env*.local.env.development等在.gitignore
2. 提交一个.env.example文件,列出所有需要的变量名(不含值),供团队成员复制参考。
3. 考虑使用dotenv库在代码中显式加载指定路径的文件,但不如Vite集成方案优雅。

7.2 实战心得与技巧

  1. 环境变量命名规范化:团队内部制定规范,例如VITE_APP_前缀表示应用级配置,VITE_API_前缀表示接口相关,VITE_THIRD_表示第三方服务。一目了然,便于管理。
  2. 为配置模块编写单元测试:虽然配置简单,但写个简单的测试用例来验证getEnvMode()函数在不同import.meta.env.MODE值下的返回,以及配置对象的默认值逻辑,能有效防止后续修改时引入错误。
  3. 善用条件编译处理平台差异:对于真正因平台而异的配置(如图片上传的API、社交分享的SDK初始化参数),使用// #ifdef条件编译比在运行时通过if-else判断更干净,还能减少无用代码被打包到其他平台。
  4. 构建脚本自动化:在package.json的 scripts 中,可以组合命令。例如,先清理旧构建产物,再执行构建:
    "scripts": { "clean": "rimraf dist", "build:prod:h5": "npm run clean && uni build -p h5 --mode production", }
    (需要安装rimraf包:npm i -D rimraf
  5. 关注vite.config.ts的缓存:有时修改了vite.config.ts但感觉没生效,可能是Vite的缓存问题。可以尝试在启动命令后加上--force选项,或者删除node_modules/.vite缓存目录。

7.3 类型声明的完善

为了让TypeScript更好地支持我们的环境变量,创建src/env.d.ts文件:

// src/env.d.ts /// <reference types="vite/client" /> // 扩展 ImportMetaEnv 接口,定义自定义环境变量的类型 interface ImportMetaEnv { // 每个变量都应该是只读的 readonly VITE_APP_TITLE: string readonly VITE_API_BASE_URL: string readonly VITE_APP_DEBUG: string // 注意:从.env读取的是字符串 readonly VITE_WEIXIN_APPID: string // 添加更多变量... } interface ImportMeta { readonly env: ImportMetaEnv }

完成这一步后,你在代码中键入import.meta.env.,IDE就会自动提示出VITE_APP_TITLE等变量,并且有正确的类型约束。

经过以上从思路到实践,从配置到排查的完整梳理,相信你已经能够驾驭UniApp + Vue3 + Vite项目中的环境变量配置了。这套方案的核心在于理解Vite的构建时替换机制,并利用它来规避UniApp多端运行时环境的差异。记住,好的配置管理是项目工程化的基石,花时间把它搭建稳健,能为后续的开发、测试和部署省去无数麻烦。如果在实践中遇到新的问题,不妨回头检查一下构建产物的代码,看看变量是否被正确替换,这往往是定位问题的捷径。

← 返回列表