Electron 打包后 .env 读不到?配置在 userData 目录而非项目根
Electron 应用开发时配置读得好好的,用 electron-builder 打包安装后,所有 process.env.XXX 全部变 undefined,依赖配置的模块接连报错。
在为客户构建数据采集工具时遇到此问题,记录根因与解法。
TL;DR
打包后 .env 不在 process.cwd() 里,而在 app.getPath('userData') 目录。
三步解法:
- 主进程启动时检测
userData/.env是否存在,没有就从包内.env.example复制一份(播种) - 把
userData/.env的完整路径写进process.env.ENV_PATH - 配置模块用
dotenv.config({ path: process.env.ENV_PATH || '.env' })读取
问题现象
开发阶段(electron .)一切正常;打包安装后(双击图标或开始菜单启动),配置全空:
undefined
undefined
TypeError: Cannot read properties of undefined (reading 'xxx')
process.cwd() 跟着启动方式走:从哪个目录启动就是哪个目录。所以从应用自身目录手动启动时恰好能读到,双击图标启动就读不到——同一个包,启动方式不同行为不同。
根因:process.cwd() 在打包后不可依赖
dotenv.config() 不传参数时读的是 process.cwd()/.env。process.cwd() 是「进程启动时所在的目录」,不是「应用安装的目录」——两者只在开发阶段恰好相同:
| 启动方式 | process.cwd() |
|---|---|
开发:项目根目录跑 electron . | 项目根目录 ✓ .env 在这 |
| Windows 双击 exe | exe 所在目录 |
| macOS 双击 .app | /(根目录) |
打包安装后,.env 想跟用户交互只有两条路,一条都不通:
- asar 归档只读:electron-builder 把应用资源打進
app.asar,运行时只读。就算把.env打进去,用户也改不了,重新打包才能更新配置 - 安装目录不可写:Program Files、/Applications 这类目录写文件需要提权,普通运行写不进去
Electron 为「属于这个用户的运行时数据」提供了规范目录:app.getPath('userData')。它按平台落在用户目录下(macOS ~/Library/Application Support/<应用名>、Windows %APPDATA%\<应用名>、Linux ~/.config/<应用名>),保证存在、保证可写、卸载重装不丢。.env 应该住这里。
解法:播种 + ENV_PATH 指路
改动集中在两个文件,共十几行。
第一步:主进程启动时播种(electron/main.js,放在创建窗口、启动 Express 之前):
const fs = require('fs');
const path = require('path');
const { app } = require('electron');
// ── .env path setup ──
const userDataPath = app.getPath('userData');
const envPath = path.join(userDataPath, '.env');
// rootDir = 应用打包根目录,取 app.getAppPath() 或按项目结构定义
const envExamplePath = path.join(rootDir, '.env.example');
if (!fs.existsSync(envPath) && fs.existsSync(envExamplePath)) {
fs.copyFileSync(envExamplePath, envPath);
console.log(`[electron] .env copied to ${envPath}`);
}
process.env.ENV_PATH = envPath;
逻辑:userData 下没有 .env 就从包内的 .env.example 复制一份(.env.example 在 asar 里只读没关系,它只是模板),然后把最终路径挂到 process.env.ENV_PATH。
第二步:配置模块按指路读取(src/config.js):
import dotenv from 'dotenv';
dotenv.config({ path: process.env.ENV_PATH || '.env' });
|| 后面的 '.env' 兜底非 Electron 场景(比如同一份代码直接用 Node 跑),开发时行为不变。
第三步:用户改配置零重装。 配置错了或者要换环境,直接编辑 userData 目录里的 .env,完全退出应用再启动即可——dotenv 只在进程启动时读一次文件,改完不重启不生效。
这套「userData 存运行时状态」的模式还能装下别的:登录态、缓存、日志,都放这里。我们同一个工具里 Cookie 登录态的存放也用了它,那次的完整排查见 Puppeteer 被反爬检测拦截?从 Chrome CDP 到 Electron 的替代方案。
注意事项
process.env.ENV_PATH 必须在配置模块被 import 之前设置好——dotenv.config() 只在调用那一刻读文件。主进程入口文件的第一段就做播种,别放进 app.whenReady() 回调里:如果有模块在更早的 import 链上就 require('dotenv').config(),会读不到路径。
常见问题
Electron 打包后配置文件放哪里?
放 app.getPath('userData') 目录——macOS 是 ~/Library/Application Support/<应用名>,Windows 是 %APPDATA%\<应用名>,Linux 是 ~/.config/<应用名>。这是 Electron 保证的用户级可写目录;打包产物里的 asar 归档是只读的,项目根目录在用户机器上也不存在。
Electron 打包成安装包后 .env 为什么读不到?
dotenv 默认读 process.cwd() 下的 .env,开发时 cwd 是项目根所以正常;打包安装后 cwd 是启动器所在目录(macOS 双击启动时甚至是 /),应用资源又被打进只读的 asar 归档,.env 根本不在读取路径上。解法是主进程启动时把 .env 播种到 userData,再把路径交给 dotenv.config({ path })。
.env.example 需要打包进应用吗?
需要。它作为首次运行的播种模板随包分发(在 asar 里只读正好),主进程检测到 userData 下没有 .env 时复制一份过去,用户直接编辑 userData 里的 .env 就能改配置,不用重新安装。