Electron 打包后 runtime asset 消失:extraResources + process.resourcesPath 的正确姿势
用 Electron 做桌面 app,开发模式一切正常:托盘图标显示正常、配置文件读取正常。npm run dist 打出 dmg,安装启动,托盘变成空白,或者日志直接抛 ENOENT。翻了半天,文件明明在源码目录里——但打包后的 .app 里根本没有它。
这是 Electron 桌面开发里最经典的"dev/prod 路径撕裂"问题,踩一次之后很难忘。
现象
// dev 模式正常,packaged app 报 Empty Image 或 ENOENT
const iconPath = resolve(process.cwd(), '../assets/icon.png')
const tray = new Tray(iconPath)或者:
const config = JSON.parse(
fs.readFileSync(path.join(__dirname, '../config/defaults.json'), 'utf-8')
)
// packaged 后:Error: ENOENT: no such file or directory根因
electron-builder 的 files 字段控制的是代码进 ASAR——它只打 JS bundle 和 package.json:
files:
- dist/src/**/*
- package.jsonassets/、config/、build/icon.png 这些 runtime asset 完全不在 packaged app 里。
更深一层:打包后 process.cwd() 和 __dirname 指向的路径彻底变了:
- dev 模式:cwd = 项目根目录,
__dirname= 源码目录,sibling 路径可以正常引用 - packaged 模式(mac):cwd =
<App>.app/Contents/MacOS/,源码 sibling 路径根本不存在
很多人的第一反应是用 process.env.NODE_ENV !== 'production' 做分支。但 packaged app 里 NODE_ENV 有时仍然是 'development',判断会失效。
必须用 app.isPackaged,这是 Electron 提供的权威标志,打包后永远是 true。
electron-builder 专门为此提供了 extraResources 字段——把任意文件作为 runtime resource 复制到 packaged app 的资源目录:
- mac:
<App>.app/Contents/Resources/<to> - win / linux:
<InstallDir>/resources/<to>
对应的 runtime 路径通过 process.resourcesPath 拿到,跨平台一致。
正解
第一步:在 electron-builder.yml 声明 extraResources:
extraResources:
- from: build/icon.png # 源路径相对于 electron-builder.yml 所在目录
to: icon.png # 目标路径相对于 Contents/Resources(mac)
- from: config/defaults.json
to: config/defaults.json
- from: assets/
to: assets/
filter:
- "**/*.ttf" # 可按需过滤第二步:runtime 用 app.isPackaged 做分支解析:
// asset-paths.ts
import { app } from 'electron'
import { join, resolve } from 'node:path'
export function runtimeAssetPath(relativeToResources: string): string {
if (app.isPackaged) {
return join(process.resourcesPath, relativeToResources)
}
// dev 模式回到源码目录
return resolve(__dirname, '../../build', relativeToResources)
}
// 用法
const tray = new Tray(runtimeAssetPath('icon.png'))第三步:加 try/catch 防止漏打 asset 时整 app 崩掉:
try {
const tray = new Tray(runtimeAssetPath('icon.png'))
} catch (err) {
console.error('tray icon load failed, running without tray', err)
}三件套缺一不可:
electron-builder.yml里extraResources把 asset 复制进去app.isPackaged判断当前是 dev 还是 packaged(不要用 NODE_ENV)process.resourcesPath拿 packaged 资源目录的绝对路径
验证方法
打包后直接 ls 看资源目录是否有文件:
# mac
ls "dist-package/mac/<AppName>.app/Contents/Resources/" | grep icon
# win:解压 installer 或安装后查 <InstallDir>/resources/同时在主进程日志里打一下 process.resourcesPath 的实际值,mac 下应该是以 *.app/Contents/Resources 结尾的绝对路径。
延伸
extraResources不参与 ASAR 压缩,文件原样保留,适合需要fs.readFile或外部进程直接访问的资源- 不要和
extraFiles混淆:extraFiles落在 app root 旁,不进Resources/,路径不同 - ASAR 内的文件读取用
app.getAppPath(),和process.resourcesPath是不同目录,别搞混 - electron-forge 用户对应字段是
extraResource(注意没有s)
一句话外卖
electron-builder的files只打代码,runtime asset 必须走extraResources+process.resourcesPath,用app.isPackaged而非NODE_ENV做 dev/packaged 分支判断。