
Electron 40.0.0 正式发布了。做跨平台桌面应用开发的人应该都知道这个版本号意味着什么——又一轮 Chromium 升级、Node.js 升级、API 清理和一堆需要重新测试的老项目。我的第一反应不是去翻 release note而是把手头几个还停在旧版本的项目先过了一遍看看有没有踩到破坏性变更。今天这篇就结合这次发布把 Electron 从开发到打包、从菜单到串口通信这些实操内容好好梳理一遍尤其是 Linux 打包时的 fpm 报错、pnpm 配置、serialport 这些高频坑一次性讲透。1. Electron 40.0.0 的大版本解读这次升级到底升了什么1.1 版本号背后的三层依赖关系Electron 的版本号不是随便跳的它一个大版本同时捆绑了三样东西Chromium、Node.js 和 V8 引擎。40.0.0 跟随的是 Chromium 的新版本周期内部 Node.js 也同步升级到了更新的主版本。这意味着三件事同时发生了渲染进程的网页兼容性变了CSS 和 JS 引擎的行为可能有调整。Node.js 侧的原生模块 ABI 变了所有带 .node 文件的依赖基本都要重新编译。V8 引擎升级后内存管理和垃圾回收的默认行为也可能有细微变化。所以每次大版本发布我关心的从来不是新功能列表而是升级成本。Electron 官方其实对这个很敏感他们一般会保留一些 API 的废弃过渡期但破坏性变更该来的还是会来。另外要理解 Electron 40.0.0 的版本发布节奏。Electron 大约每两个月出一个大版本一年六个版本每个大版本都会同步 Chromium 的主版本号。这个节奏意味着你要么跟着升级要么会积累越来越多的技术债。我个人比较推荐的是小版本跟着升大版本在一个稳定窗口内评估后再升别一发布就立刻切生产环境也别拖超过一个大版本周期。1.2 对普通开发者影响最大的三个变化从 Electron 40.0.0 这个版本往后看有几个变化方向是明显能感受到的。第一个变化是安全默认值的进一步收紧。Electron 从很早开始就强调上下文隔离contextIsolation和沙箱sandbox40.x 里这些安全相关的默认行为更严格了。以前很多老教程喜欢在渲染进程里直接开 nodeIntegration然后通过 remote 模块访问系统能力这种写法在新版本里会越来越难走通也绝对不建议用。我见过太多项目因为贪图方便开了 nodeIntegration结果 XSS 漏洞直接变成远程代码执行。这个问题的严重性在桌面端比 Web 端更高因为渲染进程能拿到系统权限。第二个变化是原生模块的生态适配。每次 Electron 大版本升级electron-rebuild 和 prebuild 机制都会重新跑一遍。如果你在项目里用了 serialport、robotjs、sqlite3 这类带原生模块的依赖升级后第一件事就是重新编译不然后面打包出来的应用会直接崩溃而且崩溃信息非常不直观。第三个变化是打包工具的适配节奏。electron-builder、electron-forge、electron-vite 这些工具会在 Electron 发布后陆续跟进。如果你想用 electron-builder 打包 40.0.0最好先把构建工具升级到较新的版本否则可能因为内部依赖了旧 app-builder-bin 而不兼容新的 Electron 二进制。从使用者的角度Electron 依然是最成熟的跨平台桌面应用开发工具。所谓“跨平台”不是说你写一套代码就能在所有平台跑得一模一样而是说你可以用同一套 Web 技术栈把主进程、渲染进程、原生模块的组合管理好然后针对各个平台处理细节问题。后面我讲的都是这些细节。2. 跨平台桌面应用的核心进程模型、安全边界与通信协议2.1 主进程、渲染进程和 preload谁该干什么活Electron 应用启动后至少有两个进程一个主进程若干个渲染进程。主进程运行在 Node.js 环境里负责创建窗口、管理应用生命周期、调用系统原生能力渲染进程负责显示页面本质上就是一个 Chromium 标签页。很多人刚接触 Electron 时搞不清楚 preload 是干嘛的。其实 preload 就是一个在渲染进程加载页面之前运行的脚本它能同时访问一部分 Node.js API 和浏览器 API是主进程和渲染进程之间搭桥的地方。我建议把职责划分想清楚主进程窗口管理、应用菜单、系统托盘、原生对话框、文件写入、串口通信。渲染进程页面 UI、用户交互、业务逻辑展示。preload通过 contextBridge 暴露安全 API 给渲染进程调用。不要在渲染进程里直接 require 任何 Node 模块哪怕是 fs 也不行。渲染进程的代码最终要跑在浏览器环境里而且要考虑沙箱开启的情况。正确姿势是主进程干完活通过 IPC 把结果返回给渲染进程。这个架构虽然看起来多了一层但好处非常明显。出了性能问题你能知道瓶颈在哪一层出了安全问题你能控制渲染进程的权限边界后续要做多窗口协同也更容易。2.2 安全配置别乱动contextIsolation、sandbox 与 nodeIntegration现在新建 Electron 项目时默认已经是 contextIsolation: true、sandbox: true、nodeIntegration: false。这套组合是安全底线。但我在实际项目里还是经常看到有人为了图省事把 nodeIntegration 打开把 contextIsolation 关掉然后在渲染进程里直接用 require。这种写法在开发阶段确实很爽但代价是你把一个有权限的 Node.js 环境暴露给了所有渲染出来的网页内容。你的应用只要加载了一个第三方 URL、一个被污染的 markdown 渲染结果甚至一个不安全的用户头像链接攻击者就能拿到操作系统权限。用 preload 加 contextBridge 的方式暴露 API虽然麻烦一点但它是 Electron 官方推荐的模式也是跨平台桌面应用开发工具链里最值得认真理解的部分。代码大概长这样// preload.js const { contextBridge, ipcRenderer } require(electron); contextBridge.exposeInMainWorld(api, { saveFile: (content) ipcRenderer.invoke(file:save, content), });渲染进程里只需要访问 window.api.saveFile完全不知道底层的 IPC 和 Node 模块是怎么实现的。这样既完成了功能又没有把 Node 能力整个暴露给页面。有一点容易忽略sandbox 开启后preload 里能用的 API 其实也受限了。你不能用完整的 require只能通过 electron 模块里允许的那几个接口。所以如果 preload 里要调用外部模块得把逻辑放到主进程或者临时给这个 BrowserWindow 单独配置 sandbox: false。我个人建议能不开就不开保持默认把逻辑都收敛到主进程反而更清晰。2.3 IPC 通信的两种姿势invoke/handle 与 send/onElectron 的 IPC 通信有两种主流姿势。第一种是请求-响应模式用 ipcRenderer.invoke 和 ipcMain.handle 配对。这种方式适合读取数据、执行操作后返回结果逻辑清晰代码可读性好。// 主进程 ipcMain.handle(serial:list, async () { const ports await SerialPort.list(); return ports; }); // 渲染进程通过 preload 暴露 const ports await window.api.listSerialPorts();第二种是单向推送模式用 webContents.send 和 ipcRenderer.on 配对。这种方式适合主进程主动向渲染进程推送事件比如串口收到数据、下载进度更新、菜单点击通知等。// 主进程 mainWindow.webContents.send(serial:data, data); // preload 暴露订阅方法 contextBridge.exposeInMainWorld(api, { onSerialData: (callback) ipcRenderer.on(serial:data, (_event, data) callback(data)), });很多人用 IPC 时犯的错误是事件名不统一、监听了不销毁、渲染进程重载后回调重复注册。我的习惯是所有 IPC 通道名统一定义在一个常量文件里preload 里暴露的每个 on 方法都返回一个销毁函数这样组件卸载时可以清理监听。3. 从零搭建 Vue3 Electron 模板项目pnpm 配置是重点3.1 为什么推荐 electron-vite 而非手动集成 webpack现在做 Vue3 Electron 的模板项目我基本只推荐 electron-vite。它把主进程、preload、渲染进程三个部分的构建配置统一管理开发模式下同时启动 Vite 的 HMR 和主进程的重新加载不用自己写一堆脚本去协调两个构建流程。手动用 Vite 或 webpack 去集成 Electron 不是不行但你要处理的事情太多了主进程要不要打包、preload 的 cjs 格式怎么处理、渲染进程的 base 路径怎么设置、开发环境下的进程启动顺序、生产环境下的文件复制……这些 electron-vite 已经全部解决。创建一个 Vue3 Electron 模板项目很简单npm create quick-start/electronlatest my-app -- --template vue或者用 pnpmpnpm create quick-start/electronlatest my-app --template vue创建出来之后目录结构大概是src/main主进程入口src/preloadpreload 脚本src/rendererVue3 渲染进程接下来直接 pnpm install 就能跑。开发模式下pnpm dev启动开发服务器主进程和渲染进程都有热更新。pnpm build会输出构建产物然后用 electron-builder 打安装包。3.2 pnpm 下打包 electron 的关键配置pnpm 现在用的人很多但它在 Electron 打包场景下有一个比较隐蔽的坑pnpm 默认使用符号链接来组织 node_modules而 electron-builder 在处理依赖关系时不总是能正确遍历到这些链接的包导致构建结果缺依赖或者某些包被打包时版本不对。解决方式是在项目根目录创建一个 .npmrc 文件node-linkerhoisted加了这一行之后pnpm 安装依赖时会用扁平化的 node_modules 结构和 npm 更接近electron-builder 处理起来就没问题了。代价是安装速度稍微慢一点磁盘占用略微大一点但换来的是打包流程不抽风绝对值得。还有一个必配项是 Electron 二进制的下载镜像。Electron 安装的时候要从 GitHub 下载二进制文件国内环境经常超时。在 .npmrc 里加上electron_mirrorhttps://npmmirror.com/mirrors/electron/ electron_builder_binaries_mirrorhttps://npmmirror.com/mirrors/electron-builder-binaries/这样 electron 的 zip 包和 electron-builder 用到的 app-builder-bin、winCodeSign、nsis 等工具都能走国内镜像速度能快几个数量级。3.3 开发体验优化HMR 与主进程热重启开发阶段用 electron-vite 的体验比传统方式好不少。渲染进程用 Vite 的 HMR改样式、改组件都是秒级刷新。主进程代码改动后会触发 Electron 应用整体重启这个功能由 electron-vite 自动完成。不过主进程重启后某些状态会丢比如临时缓存的变量、调试连接、第三方设备的连接状态。如果你在做串口通信、socket 长连接这类有状态的功能开发时最好单独处理一下重连机制不然每次改主进程代码设备就断开一次调试起来会非常崩溃。4. Linux 打包避坑实录fpm 报错排查完整流程4.1 fpm 是怎么混进 Electron 打包流程的electron-builder 在 Linux 下打包成 .deb 或 .rpm 格式时需要借助 fpm 这个工具。fpm 是一个把目录打包成各种 Linux 包格式的命令行工具由 Ruby 编写electron-builder 在背后调用它来生成 Debian 和 RedHat 系的安装包。很多人在 Windows 或 macOS 上开发得好好的一放到 Linux CI 上打包就报 fpm 相关错误就是因为本机没装 fpm、Ruby 环境缺失或者网络问题导致 fpm 下载失败。如果只是做内部署或者免安装分发完全可以绕开 deb/rpm用 tar.gz 或 AppImage 格式。本来 AppImage 在 Linux 桌面端就是很常见的分发方式免安装、双击即用。electron-builder 里直接把 target 配成 AppImage 就不需要 fpm 了。但如果你明确需要 deb 包给用户安装那还是得把 fpm 问题解决。4.2 常见 fpm 报错清单与排查方法我把实际项目中遇到过的 fpm 相关报错整理了一下报错现象原因解决办法fpm --version提示命令不存在系统没安装 fpm安装 Ruby 和 fpmgem install fpm打包时报CreateProcessError: spawn fpm ENOENTelectron-builder 找不到 fpm 可执行文件确认 fpm 在 PATH 中或配置fpm路径fpm 下载超时或卡住网络无法访问 GitHub/官方源配置 electron_builder_binaries_mirror 镜像报Need rpmbuild to build rpm缺少 rpm 构建工具安装 rpmapt-get install rpmfpm 打包时提示Failed to build package: deb缺少 dpkg 或 fpm 版本不兼容安装dpkg或升级/降级 fpm 版本报fpm: invalid option -- trimfpm 版本过旧升级 fpm 到最新版Ruby 版本过低导致 fpm 安装失败系统自带 Ruby 太旧用rvm或apt安装新版 Ruby我在 Ubuntu 20.04 的服务器上实际操作时一套比较稳的安装命令是apt-get update apt-get install -y ruby-dev build-essential rpm gem install fpm如果用的还是老版本的 electron-builder可能要检查它的 fpm 参数是否和最新 fpm 兼容。遇到invalid option之类的报错时优先想到是 fpm 版本差异。4.3 CI 环境下的打包策略在 CI 里打包 Linux 版本我的建议是直接在 docker 容器里做。electronuserland/builder 镜像里已经把 fpm、rpm、dpkg 这些工具都装好了很多常见问题根本不会出现。你只需要在构建镜像里把 Node 和 pnpm 装好。还有一个很容易踩的坑不同 CI 平台的架构差异。如果宿主机是 ARM 架构打包出来的 deb 包也是 ARM 的不能直接给 x86 用户用。做多架构打包时要么用矩阵构建要么在 docker 里模拟目标架构别指望一套产物通吃所有 Linux 发行版。5. 桌面端特色功能实操菜单、串口通信与硬件联动5.1 原生菜单别再把菜单做成网页里的按钮Electron 应用的原生菜单是真正的操作系统级菜单macOS 的顶部菜单栏、Windows 的窗口菜单栏不是网页里自己画的按钮。很多从 Web 转过来的开发者习惯把菜单功能做成页面右上角的下拉框然后发现体验总是差那么一点。原生菜单是用主进程的 Menu 模块创建的const { Menu, app } require(electron); const template [ { label: 文件, submenu: [ { label: 打开串口, click: () handleOpenSerial() }, { type: separator }, { label: 退出, role: quit }, ], }, ]; const menu Menu.buildFromTemplate(template); Menu.setApplicationMenu(menu);菜单项点击后怎么和渲染进程沟通我的做法是在主进程的 click 回调里用 mainWindow.webContents.send 推一个事件渲染进程通过 preload 暴露的订阅方法响应然后更新 Vue 组件状态。这样菜单逻辑全部在主进程渲染进程只负责表现职责非常清晰。macOS 上要注意应用的第一个菜单项通常显示为应用名Windows 和 Linux 上则是文件、编辑那一套。做跨平台时可以判断 process.platform 来生成不同的菜单模板这样能避免 macOS 下菜单行为不正常的尴尬。5.2 serialport 在 Electron 里的正确打开方式Electron 里用 serialport 做串口通信核心问题不是怎么读写数据而是 native 模块的匹配问题。serialport 是 C 插件它必须在和 Electron 完全一致的 Node ABI 版本下编译过才能正常使用。如果你刚把 Electron 升级到 40.0.0然后发现 require(serialport) 报错或者打开串口时直接崩溃九成是 ABI 不匹配。解决办法是在项目根目录执行npx electron/rebuild -f -w serialport这个命令会读取当前 Electron 版本把 serialport 重新编译成匹配的版本。如果用的 pnpm可能还要先确认 node-linkerhoisted 生效否则 electron-rebuild 可能找不到正确的模块路径。5.3 打包串口应用时的三个细节第一native 模块不要压进 asar 包里。electron-builder 默认会把 js 代码打包成 asar 归档但 .node 后缀的原生模块在 asar 里运行时是没用的必须在配置里排除。asarUnpack: - **/node_modules/serialport/**第二Linux 下打开串口需要权限。普通用户经常没有 /dev/ttyUSB0 的读写权限要么把自己加入 dialout 组要么在部署文档里写上 udev 规则。这不是 Electron 的问题是 Linux 串口权限的通用坑。第三串口逻辑一定要放主进程不要让渲染进程直接访问。串口数据通常是持续推流的走 IPC 推送模式最自然。而且主进程里可以集中处理断线重连、数据缓冲、协议解析渲染进程只负责展示。如果放渲染进程里Vue Router 切换组件时生命周期销毁了串口连接逻辑就会变得很混乱。6. 从 40.0.0 回顾整个升级经验哪些坑我替你踩过了6.1 升级前的检查清单升级 Electron 前我强烈建议照着这份清单过一遍确认现有的 electron-builder / electron-vite / electron-forge 版本是否支持新版 Electron。把所有带 .node 原生模块的依赖列出来规划重建计划。搜索代码里所有被废弃的 Electron API尤其是 remote 模块相关的用法。确认 CI 环境里的 Node 版本满足新版 Electron 的要求。在 dev 分支先升级跑一遍完整的自动化测试。打包一次 linux 测试安装包确认 fpm 相关依赖还在。这份清单看着麻烦但能省掉后面无穷无尽的排查时间。我遇到最惨的一次是没检查 remoted 模块被移除结果上线后用户一打开窗口设置就直接崩溃。6.2 native 模块重编译永远绕不开的一步Electron 升级后原生模块的重编译是逃不掉的。每个 Electron 大版本都伴随 Node ABI 变化不重编译旧的原生模块就无法在新版 Electron 里加载。轻则报NODE_MODULE_VERSION不匹配重则直接段错误崩溃。在项目里维护一个固定的 rebuild 脚本是个好习惯。我在 package.json 里一般会加rebuild: electron-rebuild -f -w serialport sqlite3然后每次升级 Electron 之后先跑一遍这个命令再启动应用。如果有人升级了 Node 版本发现模块又崩了也是同样处理。6.3 给开发者的版本节奏建议最后说下版本节奏。我现在的策略是Electron 新版本发布后等一个月左右看社区反馈和主要工具链的适配情况没问题再升级。一般跳过两代以内都还好跳得太多升级成本会叠加反而更难搞定。Electron 作为跨平台桌面应用开发工具它的价值不在某个版本的新功能而在于它提供的完整生态和一致的开发模型。40.0.0 只是一个新的起点真正决定项目质量的是你对进程模型、构建链路和原生模块管理这些基础细节的理解程度。按照上面这些方式把工程化基础铺好后续每次升级都只是例行公事不会每次都是事故现场。