先说句实话:Electron 环境搭建,网上教程一抓一大把,但八成都是“装个 Node、npm install electron、跑个 demo”三步就完事。等你真把代码往下写,开始考虑业务打包、遇到网络问题、分发到国产系统、壳子里嵌 URL 的时候,那些教程一个都用不上,还得自己踩坑。
这篇文章我不想再重复一遍官方 Quick Start,而是把“从零开始搭一套真正能写业务的 Electron 开发环境”这件事拆开讲清楚。你在热搜里也看到了,“electron 想用 URL 打包进去是否可行”“electron 壳子内的页面打开 URL”“国产系统分发”“菜单”“获取系统语言”这些词,都是大家真实开发中高频遇到的问题,说明环境搭建从来不只是装个软件,而是一整套工程决策的起点。
这套环境搭好之后,不仅 Windows 上能跑,macOS 和 Linux 也能交叉出包;不仅本地能调试,后续接自动构建、加自动更新也有基础。如果你是第一次接触 Electron,或者已经在写但一直被各种玄学问题卡住,建议顺着往下过一遍。
1. 环境搭建背后到底在搭什么
1.1 Electron 的真实依赖结构
Electron 本质上是一个“用 Node.js 和 Chromium 帮你打包桌面应用”的运行时。你在 npm 里装的 electron 包,并不是一份源码,主要干两件事:
- 下载一个对应你当前操作系统和 CPU 架构的预编译二进制文件
- 提供一个让开发者通过 Node.js API 启动桌面应用壳子的入口
这个二进制文件里内置了两个核心运行时:Chromium,负责渲染你写的页面;Node.js,负责提供主进程里的系统能力。理解这一点之后,你才能明白为什么环境搭建的关键不只是“装上了”,而是“装对了版本、装到了能跑的位置、装的过程中没被网络坑、跑起来之后主进程和渲染进程能正常通信”。
很多新人最迷惑的一个点:Electron 项目里为什么有两个 package.json?其实这不是强制要求,但属于比较推荐的工程结构。根目录的 package.json 管理主进程代码和整个应用的依赖,electron-builder 或 electron-forge 这类工具会打包时读取配置;如果你把主进程和渲染进程拆成了 src/main、src/renderer 两个目录,再配一个 electron 项目模板管理构建脚本,很多老项目确实是这么组织的。
这个结构本身不是环境搭建的必备项,但你在搭环境时最好就规划好,因为后面引入打包工具时,目录混乱会让你怀疑人生。
1.2 为什么选 Electron 而不是别的方案
在正式开始装之前,把“为什么选 Electron”想清楚,可以帮你后面省掉很多不必要的工作。
现阶段桌面端跨平台方案主要就这么几条路:
- Qt / C++:性能好,但 UI 开发和前端技术栈割裂,招人成本和团队学习成本高
- Tauri:Rust + WebView,包体积小、内存占用低,但生态和周边能力不如 Electron 成熟,而且各系统的 WebView 兼容性偶尔需要额外适配
- Electron:包体积大、内存占用高,这是硬伤,但胜在生态最成熟,开发模式接近写网页,Chromium 的内核版本统一,几乎不用操心浏览器兼容问题
我的经验是:如果你的产品是一个内容密集、交互复杂、需要频繁发版迭代的桌面应用,Electron 的开发效率优势非常明显。尤其是团队里已经有前端工程师的情况下,Electron 能让你一天之内把网页业务跑成一个桌面壳子,这个代价在原型验证阶段实在太诱人了。
值得提一句的是,Electron 31 版本以后,渲染进程的高 DPI 缩放和 Windows 显示缩放比例之间的适配,跟 Chromium 版本的升级有直接关系。如果你要在应用里基于显示器缩放比例动态计算窗口尺寸,环境搭建阶段就该把 Electron 版本锁定策略想好,而不是随便用最新版。
1.3 环境搭建阶段的常见误区
很多人会把环境搭建简单等同于“跑通 hello world”,然后就去写业务代码,等到某个节点开始集中爆雷。比如下面这几种,几乎每个踩过 Electron 坑的人多多少少遇到过:
- 主进程和渲染进程混在一份代码里,没有隔离,后面想加 preload 脚本都无从下手
- 用
npm install electron时没有配镜像,下载成功靠缘分,版本和本地 Node 不匹配 - 所有逻辑全写在
index.html和main.js的全局作用域里,变量互相污染,调试起来极其痛苦 - 不区分生产环境和开发环境,直接在本地加载远程 URL 开发,发布时才发现有跨域、路径一系列问题
- 完全依赖默认的 Chromium 内核设置,字体、缩放、路由策略全按照浏览器习惯来
所以我建议环境搭建阶段就直接按照工程化标准来。虽然第一步看着慢一点,但后面每个环节都在往回找补时间。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 动手之前先梳理关键选型
2.1 Node.js 版本与 npm 镜像源
Electron 的二进制是“版本敏感”的。老的 Electron 版本,比如 12.x、13.x,对 Node 版本要求低一些;新版 Electron(比如 30+)则需要相对较新的 Node.js 才能正常跑构建脚本,一般建议 LTS 版本起步,目前安装 20 或 22 都比较稳妥。
如果你本机需要同时维护多个 Node 版本,建议直接用 nvm-windows 或者 macOS 上的 nvm。不要图省事直接去官网下安装包装全局 Node,因为换项目时版本切换会让你崩溃。装完 Node 后,务必确认 npm config get registry 返回的是国内可访问的镜像地址,否则后面 electron 二进制下载失败会非常折磨。
2.2 Electron 版本锁定策略
Electron 版本更新的节奏非常快,基本每两个月出一个大版本。但对业务开发来说,并不建议无脑追新,因为 Chromium 内核每次升级都会带来一些渲染表现变化,可能在你完全没碰到的代码里引发问题。
我个人的习惯是:
- 新项目直接使用当前最新稳定版,获取较完整的安全补丁
- 老项目锁住主版本,只在小版本内升级,除非有明确需求或安全漏洞
- 在
package.json里用"electron": "^30.x.x"而不是"electron": "30.0.0",避免锁死补丁升级 - 下载完成后,将 Electron 的二进制缓存路径固定下来,方便离线复用
2.3 官方脚手架选哪个,还是自己手搭
Electron 官方提供了两个体验完全不同的工具:electron-forge 和 electron-builder。
electron-forge 是 Electron 官方团队主推的一体化工具,从开发到打包发布一条龙,深度整合了 Vite/Webpack 等构建工具,用它生成的项目后期加插件比较顺滑。electron-builder 则是社区老牌工具,配置项细、定制能力强,支持多平台构建、自动更新配置。
如果你对前端工程化没那么熟,也懒得折腾配置文件,推荐直接用 electron-forge 的 Vite 模板:
bash复制npm init electron-app@latest my-app -- --template=vite
如果你需要把远程 URL 直接打包成桌面应用,而且希望控制更细粒度的打包逻辑,我一般建议自己维护一个最小工程模板。这样你对主进程的启动逻辑是可控的,不会被脚手架的抽象层限制住。
从环境搭建粒度上来讲,我下面会给大家一条“最简但可控”的路线,不依赖重型脚手架,手动了解每一层在做什么。
3. 从零开始搭建一套可用的 Electron 开发环境
3.1 基础依赖安装与镜像配置
以 Windows 10/11 为例,首先安装 Node.js LTS 版本。我习惯用 nvm 做版本管理,不做全局占用:
bash复制nvm install 20
nvm use 20
node -v
npm -v
接着配置 npm 镜像:
bash复制npm config set registry https://registry.npmmirror.com
这里有一点必须提醒:registry 镜像只解决 npm 包下载速度,但 Electron 的二进制是从 GitHub Releases 下载的,国内网络经常直接失败。所以还得单独设置 Electron 的二进制镜像:
bash复制npm config set electron_mirror https://npmmirror.com/mirrors/electron/
如果是通过 .npmrc 来管理,内容类似这样:
code复制registry=https://registry.npmmirror.com
electron_mirror=https://npmmirror.com/mirrors/electron/
electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/
后面这个是给 electron-builder 准备打包工具链时用的,比如 winCodeSign、nsis 等资源,不同系统下会用到。建议从环境搭建阶段就写好,否则等打包时才想起,又是一轮下载抽风。
注意:不同操作系统的下载缓存位置不一样。Windows 通常在
%LOCALAPPDATA%/electron/Cache,macOS 在~/Library/Caches/electron/,Linux 在~/.cache/electron/。如果下载失败,可以手动从镜像站下载对应版本 zip,放到这个目录下,重试就可以跳过重新下载。
3.2 初始化项目与最小目录规划
开始动手创建一个目录,然后初始化项目:
bash复制mkdir my-electron-app
cd my-electron-app
npm init -y
安装 Electron 作为开发依赖:
bash复制npm install --save-dev electron@latest
安装完成后建议马上验证一下当前版本:
bash复制npx electron --version
如果能看到类似 v30.x.x 的输出,说明安装成功。很多人在这一步会卡住,迟迟没反应或者报错,大概率就是二进制下载失败了。这时候回到 3.1 的镜像配置,然后重新安装一次。
接下来规划一个比较清晰的目录结构,不用一步到位,但最好从第一天就开始遵守:
text复制my-electron-app/
├── src/
│ ├── main/ # 主进程代码
│ │ └── main.js
│ ├── preload/ # preload 脚本
│ │ └── preload.js
│ └── renderer/ # 渲染进程(业务页面)
│ └── index.html
├── package.json
└── .npmrc
为什么要分 main 和 preload?Electron 的安全模型要求渲染进程不能直接使用 Node.js API。如果你需要在页面里读取系统信息或调用原生能力,必须通过 preload 脚本暴露受限接口。这个模式从环境搭建阶段就养成习惯,后面写代码不会“这里怎么访问不到 require”这类问题绕晕头。
3.3 主进程入口文件的最小实现
新建 src/main/main.js,写入这段基础代码:
javascript复制const { app, BrowserWindow } = require('electron')
const path = require('path')
const createWindow = () => {
const win = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
preload: path.join(__dirname, '../preload/preload.js'),
contextIsolation: true,
nodeIntegration: false,
sandbox: true
}
})
win.loadFile(path.join(__dirname, '../renderer/index.html'))
}
app.whenReady().then(() => {
createWindow()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})
这段代码里两个关键点值得展开说一下:
一是 contextIsolation 和 nodeIntegration 的设置。网上很多老教程会让你把 nodeIntegration 设为 true,这是完全为了省事而埋雷的做法,一旦页面加载了不安全的远程内容,等于把本地的 Node.js 能力直接暴露给了网页。现在 Electron 15 以后默认就是 contextIsolation: true、nodeIntegration: false,你只要不主动改回去,就天然处于安全模式。
二是 preload 脚本在远端 URL 场景下依然会执行。如果你想把某个人人网 IP 页面或内网地址打包进 Electron 壳子,preload 提供了在页面加载前注入脚本的机会,方便你给页面补充桥接方法。
3.4 preload 脚本与页面通信基础
新建 src/preload/preload.js:
javascript复制const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
getSystemLanguage: () => ipcRenderer.invoke('get-system-language'),
openExternalUrl: (url) => ipcRenderer.invoke('open-external-url')
})
这里用到了 ipcRenderer.invoke,它在渲染进程里发起一个异步请求,主进程通过 ipcMain.handle 来响应。这样写的好处是渲染进程只知道自己调用了一个接口,完全不知道底层实现,同时也不会直接暴露完整的 ipcRenderer 对象,安全边界清晰。
注意 preload 脚本里不要写 Node.js 专用的大段逻辑,它只是在页面加载时执行一层薄薄的桥接而已。主进程里加对应的 handler:
javascript复制const { app, BrowserWindow, ipcMain, shell } = require('electron')
ipcMain.handle('get-system-language', () => {
return app.getLocale()
})
ipcMain.handle('open-external-url', (event, url) => {
shell.openExternal(url)
})
这套模式熟练之后,你能做很多好玩的事。比如在热搜里看到的“electron 获取系统语言”,本质上就是 app.getLocale();又比如“electron 菜单”,就涉及 Menu.setApplicationMenu。这些都是后续业务扩展时加分的能力。
3.5 渲染进程的入口页面
最简单的 src/renderer/index.html 可以这样写:
html复制<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Electron 环境验证</title>
</head>
<body>
<h1>Electron 环境搭建成功</h1>
<button id="check-locale">获取系统语言</button>
<p id="result"></p>
<script src="./renderer.js"></script>
</body>
</html>
再新建一个 renderer.js,负责前端逻辑:
javascript复制const result = document.getElementById('result')
const button = document.getElementById('check-locale')
button.addEventListener('click', async () => {
const locale = await window.electronAPI.getSystemLanguage()
result.textContent = `当前系统语言: ${locale}`
})
这里你会发现,页面脚本里没有出现任何 require('electron') 的代码,因为那在启用 contextIsolation 后根本不可用,我们是通过 window.electronAPI 来访问能力的。实际上这套开发模式和你正常写网页几乎没有差别,这正是 Electron 的优点。
在 package.json 的 scripts 里加一行:
json复制"scripts": {
"start": "electron ."
}
然后运行:
bash复制npm start
正常的话,桌面会出现一个带标题的窗口,页面里有一个按钮。点击后可以拿到系统语言,整个链路就算通了。
4. 环境验证的体检清单
4.1 快速检查应用是否跑在 Electron 环境
业务代码写多了,容易遇到一个场景:同样的页面需要在浏览器和 Electron 壳子里都能跑。为了区分运行环境,渲染进程可以检查用户代理或全局变量:
javascript复制const isElectron = window.navigator.userAgent.includes('Electron')
更干净的方案是在 preload 里直接暴露一个环境标识:
javascript复制contextBridge.exposeInMainWorld('appEnv', {
platform: process.platform,
versions: process.versions,
isPackaged: process.env.NODE_ENV === 'production'
})
这样页面里就能根据环境来决定是否启用桌面专属功能。如果你观察过一些混合架构的应用,它们在浏览器端的降级策略基本就是这个思路。
4.2 渲染进程打开远程 URL 与“把 URL 打包进去是否可行”
热搜里反复出现“我想使用 electron 把 url 打包进去,是否可行”,这其实是 Electron 最常见的用法之一,完全可行。我平时写内部工具时也会把一个内网 Web 系统直接封成桌面壳子,体验比浏览器标签页好不少,还能额外加菜单、快捷键、系统托盘、自动启动等原生能力。
如果你只是想简单加载远程 URL,不依赖 preload 注入能力,那入口文件会简化到只剩这几行:
javascript复制const { app, BrowserWindow } = require('electron')
app.whenReady().then(() => {
const win = new BrowserWindow({
width: 1400,
height: 900
})
win.loadURL('https://example.com')
})
就这么简单。但实际跑起来你肯定会遇到几个全新的问题:
- 页面里弹出的
window.open新窗口默认会被 Electron 当成新 BrowserWindow 打开,而它里面没有 preload,行为完全不可控 - 页面里有些链接默认会导航到外部浏览器,你需要用
shell.openExternal来接管 - 远程页面里的登录状态依赖 Cookie,不同域名的 Cookie 隔离策略要做好适配
- 页面里如果有下载文件、访问摄像头、调用麦克风等操作,系统权限弹窗在 Electron 里的表现和浏览器有细微差异
所以我建议环境搭建阶段,就顺手把 setWindowOpenHandler 写好:
javascript复制app.whenReady().then(() => {
const win = new BrowserWindow({
width: 1400,
height: 900,
webPreferences: {
contextIsolation: true,
sandbox: true
}
})
win.webContents.setWindowOpenHandler(({ url }) => {
shell.openExternal(url)
return { action: 'deny' }
})
win.loadURL('https://example.com')
})
这里做的事情是:当页面里任何代码尝试弹新窗口时,Electron 不创建本地新窗口,而是把它交给系统默认浏览器打开,避免打开的窗口脱离你的安全控制。
4.3 远程页面如何调试
开发远程 URL 页面时,你可能看不到构建日志。调试思路其实和普通网页一致,因为 Electron 里的页面本质就是 Chromium 页面。
javascript复制win.webContents.openDevTools({ mode: 'detach' })
这条代码放在主进程里,能在应用启动时自动打开开发者工具。配合 --remote-debugging-port=9222 参数,你还能使用 Chrome 开发者工具连接调试:
bash复制npx electron . --remote-debugging-port=9222
然后访问 http://localhost:9222/json 就能拿到调试地址。这个技巧在处理“页面跑了但界面不对”的问题时极其有用。
4.4 页面白屏与加载失败排查
Electron 环境里最诡异的一个问题就是:应用能启动,窗口也正常,但页面一片空白。原因基本集中在下面几个方面:
loadURL地址不可达,或者目标地址有 TLS 证书错误- 本地文件路径写错,
loadFile找不到文件 - 渲染进程的 JavaScript 报错,而且是启动时立即报错,整个页面看不到内容
- CSP(内容安全策略)设置太严格,挡住了页面加载的资源
- preload 脚本执行出错,导致页面初始化逻辑没有跑
快速排查方式是在主进程里监听渲染进程的异常事件:
javascript复制win.webContents.on('render-process-gone', (event, details) => {
console.error('渲染进程崩溃:', details.reason)
})
win.webContents.on('did-fail-load', (event, errorCode, errorDescription) => {
console.error('页面加载失败:', errorCode, errorDescription)
})
win.webContents.on('console-message', (event, level, message) => {
console.log('页面 console:', message)
})
把这些监听器在环境搭建阶段就加进去,能省掉后面大量“盲人摸象”的时间。
5. 开发体验优化与踩坑实录
5.1 Electron 应用的资源占用问题
Electron 被吐槽最多的就是吃内存。这是运行时的架构决定,没办法完全避开,但可以通过一些手段缓解。
首先是确认 webPreferences 里没有打开多余的功能,不需要的权限都关掉:
javascript复制webPreferences: {
nodeIntegration: false,
contextIsolation: true,
sandbox: true,
webSecurity: true,
allowRunningInsecureContent: false
}
其次是不要为每打开一个窗口就创建一个新的渲染进程,如果业务里窗口数量多,需要考虑窗口池或者单实例约束。app.requestSingleInstanceLock() 可以确保只启动一个应用实例,不仅省资源,还能避免多开时数据互相竞争。
5.2 开发环境与打包环境的路径差异
Electron 项目里最容易埋坑的就是资源路径。开发时你可能用 path.join(__dirname, '../renderer/index.html'),跑起来当然没问题。但打包的时候,文件会重新组织,资源被压缩进 app.asar,这时再按原来的相对路径去找,很可能找不到。
所以从环境搭建阶段就应该养成一个习惯:所有资源读取都走 main 进程的路径解析,不要依赖渲染进程里的相对路径。我这里写一个开发的解决方案:
javascript复制const isDev = !app.isPackaged
if (isDev) {
win.loadURL('http://localhost:5173') // 开发服务器
} else {
win.loadFile(path.join(__dirname, '../renderer/index.html')) // 打包产物
}
如果你用 Vite 做渲染进程构建,开发时起一个本地服务,打包时把构建产物指向 dist 目录。这套模式是 vue-cli-plugin-electron-builder 等主流模板的核心思路,原因就是开发体验和生产路径分离。
5.3 国产系统及 Linux 平台分发注意事项
近期有不少人在搜“electron 国产系统分发”“银河麒麟 electron 版本”,这里有必要展开说说。
Electron 官方的预编译二进制对 Windows、macOS、主流 Linux 发行版支持都很好。国产系统通常基于 Linux 内核,但桌面环境和库版本各有差异,所以直接拿通用 Linux 包跑可能碰上问题。常见的情况有:
- 浏览器内核依赖的
libnss3、libatk、libgtk-3等系统库缺失 - 缺少
libgbm,新版 Chromium 在部分发行版上启动直接崩溃 - 显示服务器协议兼容问题,尤其是部分国产系统的桌面环境
- 中文输入法无法在 Electron 里正常使用,涉及
ibus或fcitx的对接
如果你有国产系统分发需求,在环境搭建阶段就建议装一台目标系统虚拟机,提前验证启动、输入、字体和性能表现,别等代码写完才去适配。
打包时用 electron-builder,针对 Linux 目标加 AppImage 和 deb:
json复制"build": {
"linux": {
"target": ["AppImage", "deb"],
"category": "Utility"
}
}
AppImage 是免安装形态,适合快速分发验证;deb 更适合在 Debian 系系统里正式安装。
5.4 使用缓存加速 Electron 二进制下载
如果你经常在不同电脑或 CI 环境上重新安装 Electron,重新下载庞大二进制非常浪费时间。Electron 会优先从缓存目录读取,你可以手动把下载好的 zip 文件放到缓存目录实现“离线安装”。
更进一步,团队内部可以搭建一个 npm 私服或二进制镜像服务。把 electron_mirror 指向内网地址,所有机器都从内网下载,速度稳定且合规。这个方案对 VPS、云主机等远程开发环境尤其友好,毕竟远程机器访问外网本身不稳定。
6. 常见问题与排查技巧实录
这部分整理我经常处理或了解到的 Electron 环境问题,按发生频率倒序写。每个问题都是我实际遇到或在社区高频看到的,直接给出能落地的解决办法。
6.1 npm install electron 卡住不动或失败
很多人第一步就倒在这里。原因很明确:npm 安装 Electron 时会下载几十到上百 MB 的二进制文件,默认走 GitHub Releases,网络经常不通。
处理方式:
bash复制npm config set electron_mirror https://npmmirror.com/mirrors/electron/
然后删除 node_modules 和 package-lock.json,重新安装。如果仍然失败,可以手动下载对应版本的 zip 文件,放到本文 3.1 节提到的缓存目录里再装。
6.2 窗口能打开但显示空白
先看控制台输出。主进程启动时如果没开异常监听,很多错误都会被静默吞掉。建议项目初期直接把下面这段监听加到主进程:
javascript复制process.on('uncaughtException', (error) => {
console.error('未捕获异常:', error)
})
然后注意排查 preload 路径是否正确。路径错了 Electron 不会弹窗,而是直接忽略,你在页面上使用 window.electronAPI 就会得到 undefined。
6.3 electron 模块在渲染进程里找不到
这属于对安全模型不了解导致的经典问题。electron 模块只能在主进程和 preload 脚本中使用。如果你的渲染进程代码里写了 require('electron'),应该改成通过 preload 暴露接口的方式访问能力。
如果确实需要调试某个功能,可以用 win.webContents.executeJavaScript 在渲染进程里执行脚本临时调用原生能力,但这只是调试手段,不要写进生产代码。
6.4 高分屏下窗口尺寸错误、模糊
Windows 显示缩放比例如果设置为 150% 或 125%,Electron 窗口的 CSS 像素尺寸和屏幕物理像素之间会有一个缩放关系。很多应用没做适配,就表现为窗口看起来模糊、字体发虚、明明设置了 1920 宽度却超出屏幕范围。
常规做法是在 main 进程入口处禁用 GPU 加速或调整缩放策略:
javascript复制app.commandLine.appendSwitch('disable-gpu')
但这不是根本解法。更合理的做法是监听显示器的缩放比例变化,动态调整窗口大小:
javascript复制const { screen } = require('electron')
screen.on('display-metrics-changed', (event, display, changedMetrics) => {
if (changedMetrics.scaleFactor) {
const bounds = display.bounds
mainWindow.setSize(Math.round(bounds.width / display.scaleFactor * 0.8), Math.round(bounds.height / display.scaleFactor * 0.8))
}
})
在新版本 Electron 中,这部分行为继续变化,所以环境搭建阶段用某个固定版本跑通后再锁版本,能减少这类困扰。
6.5 chatgpt failed to start. unable to locate the codex cli binary 报错
这个报错虽然带了 ChatGPT 字样,但实质是开发侧环境变量配置问题:Electron 应用启动时找不到 codex cli 二进制路径。它提示你需要设置 CODEX_CLI_PATH 环境变量,或者确保 Electron 资源目录里包含了 bin/codex。
如果是自己集成,原因往往是 SDK 安装后没有把二进制放到正确位置,环境变量没导出。在 .bashrc 或 .zshrc 里加上导出,然后重启终端就行。如果是在 Electron 打包后的应用里集成这类二进制,需要确认打包配置里的 extraResources 有没有带上这个可执行文件,并且运行时路径解析要处理好。
6.6 打包后提示找不到文件或路径错误
先确认入口文件里写的路径都是基于 __dirname 的,而不是当前工作目录。比如:
javascript复制app.setAppUserModelId('com.example.app')
const mainWindow = new BrowserWindow({
...,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
path.join(__dirname, 'preload.js') 在主进程代码被打包进 asar 后,会自动指向 asar 内部的路径,Electron 能正确读取。但如果你的代码用了 process.cwd() 去定位文件,运行方式稍有变化就出问题,发布后尤其明显。
6.7 electron-builder 打包失败、下载辅助工具出错
打包时经常要下载 winCodeSign、nsis 等辅助工具,国内网络经常失败。解决方案是在 .npmrc 里配好 electron-builder 的镜像:
code复制electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/
也可以添加环境变量:
bash复制export ELECTRON_BUILDER_BINARIES_MIRROR="https://npmmirror.com/mirrors/electron-builder-binaries/"
6.8 菜单栏如何定制
Electron 提供了原生的应用菜单能力,通过 Menu 模块实现,可自定义标题栏下的菜单项、快捷键、点击行为,适合承载一些 Web 页面没有的桌面入口。
javascript复制const { Menu } = require('electron')
const template = [
{
label: '文件',
submenu: [
{ label: '离开', role: 'quit' }
]
},
{
label: '编辑',
submenu: [
{ label: '撤销', role: 'undo' },
{ label: '重做', role: 'redo' },
{ type: 'separator' },
{ label: '复制', role: 'copy' }
]
}
]
Menu.setApplicationMenu(Menu.buildFromTemplate(template))
菜单的点击事件支持绑定 IPC 消息,例如在“帮助”下添加一项“关于我们”,点击后发 IPC 给页面触发弹窗。菜单不仅能提升桌面专业感,还能有效减少页面内操作层级。
7. 在环境搭建阶段就建立的安全与工程规范
7.1 什么是主进程/渲染进程合理的隔离
很多新手写 Electron,容易把所有代码都塞到主进程里,页面里也直接用 window.require。这套写法在单机 demo 里很顺畅,但生产环境一旦加载了第三方网页、接入外部数据源,安全隐患会被瞬间放大。
合理的隔离设计应该是这样的:
- 主进程持有窗口生命周期、应用菜单、系统托盘、自动更新等原生模块能力
- preload 脚本作为“契约层”,只暴露业务需要的 API,不要一股脑把 Node 能力全传下去
- 渲染进程只负责 UI 和交互,需要原生能力时走 IPC
这样设计之后,即使某个页面出现 DOM XSS,因为页面本身没有 Node 权限,攻击者也没办法通过页面直接读写本地文件。再配合 sandbox: true,整个应用的安全边界是清晰的。
7.2 preload 脚本里的 API 设计
preload 脚本不应该只是简单地把 ipcRenderer 都暴露出去。建议按业务模块来设计 API 面,例如:
javascript复制contextBridge.exposeInMainWorld('appWindow', {
minimize: () => ipcRenderer.send('window-minimize'),
maximize: () => ipcRenderer.send('window-maximize'),
close: () => ipcRenderer.send('window-close'),
setTitle: (title) => ipcRenderer.send('window-set-title', title)
})
这样渲染进程每次只需要关注“这个动作是干什么”,而不是底层实现。后续如果有人给你提需求“窗口关闭前要提示一下”,你在主进程的 window-close handler 里加个判断就行,页面代码完全不用改。
7.3 环境变量管理
环境变量在 Electron 项目里也是必须处理好的环节。开发阶段可以用 dotenv 加载 .env 文件,构建时注意不要把敏感信息(比如 API Key、密钥)打进包里。
主进程读取环境变量:
javascript复制const apiKey = process.env.MY_APP_API_KEY
如果你在打包时还需要区分不同服务器域名,可以在打包脚本里根据 --env 参数写不同配置。这些从环境搭建阶段就规划好,后面部署到云主机、走 CI 流水线时都会顺畅很多。
8. 多阶段开发环境的经验之谈
8.1 远程 URL 场景与本地页面场景如何兼顾
有些 Electron 应用完全是纯本地,没有网络也能跑;有些则面向远程 Web 系统。环境搭建阶段就要决定支持哪种形态,并且做好代码层面的切换判断。
最灵活的做法是设置一个配置开关,比如在 src/config.js 中定义:
javascript复制module.exports = {
devUrl: 'http://localhost:5173',
prodUrl: 'https://app.example.com',
isRemote: true // 为 false 时用本地文件
}
然后主进程读取配置决定加载策略。这套方案适合需要同时在浏览器、桌面端分发的项目,核心逻辑都能复用。实际情况里,官方文档的 Quick Start 往往只覆盖本地页面形态,而你真的想“把 URL 打包进去”,最常被忽略的就是“混合加载模式下的窗口管理”。
8.2 预发布环境的配置策略
个人开发可以生产、测试环境都从环境变量里取 URL。团队协作时我更推荐用构建参数传入:
json复制"build:test": "electron-builder --config electron-builder-test.yml"
把测试环境专用字段写成独立配置文件,避免测试时误连生产接口。这个经验在写了几个有明确运营后台的 Electron 应用后深有体会。
8.3 CI/CD 集成
环境搭好之后,代码仓库建议直接接上 CI 构建。Electron 工程在 Windows 和 macOS 上构建产物不同,不能一台机器全搞定,所以 CI 流水线要考虑多平台并行构建。在 .github/workflows/build.yml 或者自己的 GitLab Runner 中,基本套路是:
- 安装 Node.js LTS
- 执行
npm ci - 执行测试/静态检查
- 执行打包命令并收集产物
构建机上同样要配置好 Electron 二进制镜像,否则每次 CI 跑都去 GitHub 拉二进制,网络一抖动整个流程全黄。
8.4 自动更新的前置考虑
如果你的应用计划长期维护并需要发版更新,环境搭建阶段就要给后续集成 electron-updater 留好余地。打包时提供 latest.yml、latest-mac.yml、latest-linux.yml 等更新元数据,然后上传到静态资源服务即可。这个设计与 electron-builder 天然集成,比你自己在应用里写“检测下载覆盖”要可靠得多。
自动更新的接入时机最好在应用第一个可用版本发布前,否则等到用户已经装了好几个版本,再想从旧版跳转到新版更新链,适配成本会陡然上升。
最后说点实在话
Electron 环境搭建是整个项目里最简单,却也最容易被低估的一个环节。简单在于它确实只是命令行几条指令的事,被低估在于它在后续每一个节点都默默影响着你的体验。下载卡住、路径不对、白屏、打包缺东缺西,这些问题如果能在开始时就规避掉,后面开发的体感会舒服很多。
我个人操作下来的体会是:版本锁定、镜像配置、目录规划、IPC 模式这四件事做在前面,就能覆盖掉后面绝大多数的坑。不要嫌这些基础工作琐碎,Electron 这类项目不像纯前端那样“跑起来就行”,它涉及系统集成和二进制分发,基本的问题往往发生在你觉得“不该有问题”的地方。
如果你也正在筹划一个 Electron 桌面应用,不管目标是“把网页打包成桌面壳子”,还是做一个原生功能丰富的工具类软件,建议把上面这套流程先跑通,再开始写业务。环境搭好了,后面每一步才会顺起来。
