知行札记
专题技术实践桌面应用实践Electron 桌面应用

Electron 诊断与发布路径

沿白屏、路径、ABI、资源、图标、签名和更新问题定位原因。

本页整合 Electron 归档分稿中的机制、接口和接线。归档使用 Electron 44 系列作为其版本讨论背景;本文接口依据官方文档及 breaking changes 核查,实际采用时仍需固定项目版本。包版本、维护状态和原稿实测均是对应日期的历史信息。本次未启动 Electron、构建安装包、签名、公证或验证更新。代码块按各自标注区分片段与组合示例。

调研时间:2026-10-04。本课以** Electron 44.x**(当日 npm latest 为 44.5.1,Chromium 152 / Node.js 24)与 Electron Forge 8.0.1 为准;文中所有版本号、默认值与命令均来自原调研当日记录(npm registry、本机 npm run make 全流程)与文末所列官方文档,无法核实处已标注「待核实」。

13.0 本课的打开方式

前两课你已经能写出「主进程 + 渲染进程 + preload」的最小应用。这一课换个视角:专门讲你一定会撞上的报错。每条按「现象 → 原因 → 解决办法」组织,先说清楚这个概念是什么、为什么,再给完整可复制的代码。

两条贯穿全课的经验,先说在前面:

  1. 报错的第一现场是日志,不是白屏。 主进程报错打在终端里,渲染进程报错打在 DevTools(开发者工具,Chromium 自带的调试面板)的 Console 里。先看日志,再搜报错原文。
  2. 开发模式与打包后的运行环境不一样。 下面一多半的坑(路径、require、asar、图标、更新)都源于「npm start 一切正常,打包完就坏」。

怎么使用本课:如果你正被某个报错卡住,直接跳到对应小节按「解决办法」操作;如果你在系统学习,建议按顺序读,因为不少小节互相引用(比如 13.10 的打包实测会回头印证 13.3/13.4/13.5 的结论)。每条坑的「来源或复现依据」都写在正文里:标「官方文档」的结论请以文档原文为准,标「实测」的都是我在本机真实跑出来的输出。环境说明:本课命令均在 Node 24.12.0 下执行,与 Electron 44 内嵌的 Node 24 同代,建议你也使用 Node 22 或更新的 LTS 版本。

13.1 白屏 / 页面加载失败:开发与生产的路径差异

是什么/为什么:渲染进程的本质是一个浏览器页面,主进程用 loadFile(加载本地 HTML 文件)或 loadURL(加载网址)把页面装进来。开发时你的文件在项目根目录;打包后,文件被装进 app.asar(见 13.5),__dirname 的值随之改变。原调研在打包流程中 打包并启动应用后,从进程参数里看到 --app-path 指向的是 …/Resources/app.asar——这就是「同一份代码,两种运行位置」的直观证据。如果路径写错,页面加载不出来,窗口就只剩一片白。官方 API 文档对 did-fail-load(加载失败事件)的定义是:参数依次为 event、errorCode(错误码,如 -6 表示 ERR_FILE_NOT_FOUND)、errorDescription、validatedURL、isMainFrame 等完整信息。理解了这一点,白屏就不再是「玄学」,而是「一次被吞掉的加载失败」。

现象:窗口打开是纯白/纯黑;或控制台出现 net::ERR_FILE_NOT_FOUND、Not allowed to load local resource 等。

原因(按出现频率排):

  1. loadFile 的路径在「开发」和「打包后」指向了不同的位置。开发时应用目录是项目根;打包后是 asar 归档内部。
  2. 用了 loadURL('file://...') 手拼文件路径,遇到中文、空格、特殊字符没有编码,导致加载失败。
  3. CSP(Content Security Policy,内容安全策略)配置过严,把样式或脚本拦了。官方第一课的 index.html 就自带一条 default-src 'self'; script-src 'self',任何外部资源都会被静默拦截。

解决办法:第一步,把失败原因「打出来」,不要靠猜。第二步,用 app.isPackaged(只读属性,应用被打包后为 true)区分开发/生产两种加载方式;路径一律用 path.join 拼,不要手写斜杠。下面是一份完整、可直接替换官方模板(src/index.js)的主进程文件:

// src/index.js —— 主进程(基于官方模板,Node 的 CommonJS 语法)
const { app, BrowserWindow } = require('electron');
const path = require('node:path');

// Windows 安装/卸载时由 Squirrel 创建/删除快捷键,需要提前退出(官方模板自带)
if (require('electron-squirrel-startup')) {
  app.quit();
}

const createWindow = () => {
  const mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      // preload 脚本路径永远用 __dirname 相对拼接,开发与打包后都成立
      preload: path.join(__dirname, 'preload.js'),
    },
  });

  // 第一步:监听加载失败事件,把错误码和 URL 打到终端(参数定义见 web-contents 文档)
  mainWindow.webContents.on(
    'did-fail-load',
    (event, errorCode, errorDescription, validatedURL) => {
      console.error(`[加载失败] ${validatedURL} → ${errorCode} ${errorDescription}`);
    }
  );

  if (!app.isPackaged) {
    // 开发模式:例如加载 Vite dev server(按你的实际端口改)
    mainWindow.loadURL('http://localhost:5173');
    mainWindow.webContents.openDevTools(); // 开发时顺手打开 DevTools
  } else {
    // 生产模式:从打包产物目录加载,路径用 path.join
    mainWindow.loadFile(path.join(__dirname, 'index.html'));
  }
};

app.whenReady().then(() => {
  createWindow();
  app.on('activate', () => {
    // macOS 惯例:点击 Dock 图标时若无窗口则重建
    if (BrowserWindow.getAllWindows().length === 0) createWindow();
  });
});

// Windows/Linux 惯例:关掉所有窗口就退出;macOS 惯例是留在 Dock
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') app.quit();
});

排查顺序建议:先看终端里 did-fail-load 打印的 validatedURL——它就是「Electron 实际在加载的那个地址」,90% 的白屏在这一步就能看出是路径错了;再打开 DevTools 看 Console 与 Network 面板是否有被 CSP 拦截的资源;最后确认 __dirname 是否还是你以为的值(打包后它指向 asar 内部,详见 13.5)。

13.2 require is not defined:nodeIntegration 默认关闭的正确理解

是什么/为什么:nodeIntegration(Node 集成)指「渲染进程页面里能不能直接用 Node 的 require」。从 Electron 5 起默认 false;contextIsolation(上下文隔离,页面脚本与 preload 脚本运行在隔离的 JS 环境)从 Electron 12 起默认 true;sandbox(渲染进程沙箱)从 Electron 20 起默认 true——而且一旦你手动开 nodeIntegration: true,沙箱会被隐式关闭。这是官方刻意的安全设计:网页里能随便 require('fs') 等于把整台电脑交给了页面里任何一段脚本(包括被注入的恶意脚本)。

现象:渲染进程 Console 报 Uncaught ReferenceError: require is not defined,教程里「页面直接 require Electron 模块」的老代码全部失效。

原因:你的代码写法来自 Electron 4 及更早年代(或抄了那个年代的老教程),而当前默认值早已翻转。不要用 nodeIntegration: true 去「修复」它——那是在降低安全性。正确姿势是 preload(预加载脚本)+ contextBridge(上下文桥):preload 脚本在隔离世界里运行、可以 require,再通过 contextBridge.exposeInMainWorld() 把少量安全的函数挂到页面 window 上。

解决办法(官方教程《Using Preload Scripts》的完整三件套,可直接复制):

// src/preload.js —— 预加载脚本:唯一允许接触 Node/Electron API 的渲染侧代码
const { contextBridge, ipcRenderer } = require('electron');

// 把一个白名单 API 挂载到页面的 window.versions 上
contextBridge.exposeInMainWorld('versions', {
  node: () => process.versions.node,       // 只回传字符串,不暴露 process 本身
  chrome: () => process.versions.chrome,
  electron: () => process.versions.electron,
  ping: () => ipcRenderer.invoke('ping'),  // 包装 IPC 调用,而不是暴露整个 ipcRenderer
});
// src/index.js 的 createWindow 里,注册主进程这端的 IPC 处理器
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('node:path');

app.whenReady().then(() => {
  // 主进程应答 'ping' 通道(需放在 createWindow 之前或同一初始化函数里)
  ipcMain.handle('ping', () => 'pong');
  createWindow(); // createWindow 定义同 13.1
});
// src/renderer.js —— 页面脚本:只能用 window 上挂出来的白名单 API
const information = document.getElementById('info');
information.innerText =
  `Chrome (v${window.versions.chrome()}), Node.js (v${window.versions.node()}), ` +
  `Electron (v${window.versions.electron()})`;

const func = async () => {
  const response = await window.versions.ping(); // 发起 IPC,拿到 'pong'
  console.log(response);
};
func();

两个官方强调的细节:preload 里包装函数而不是直接 exposeInMainWorld('ipcRenderer', ipcRenderer)——否则页面又能向主进程发任意消息;BrowserWindow 的 webPreferences 里只需要配 preload 一项,nodeIntegration/contextIsolation/sandbox 保持默认即是安全状态。如果你确实要在旧项目里临时打开 nodeIntegration: true,请把它理解成「技术债 + 安全降级」,而不是修复方案。

13.3 跨平台路径与 userData 目录

是什么/为什么:userData(用户数据目录)是 Electron 为你的应用预分配的「可写目录」,存放数据库、配置、缓存(Cache、GPUCache、Local Storage 等子目录都在它下面)。官方 app.getPath 文档写明各平台 appData 的位置,userData 默认是 appData 下以应用名命名的子目录:

平台userData 默认位置(应用名为 my-app 时)
macOS~/Library/Application Support/my-app
Windows%APPDATA%\my-app(即 C:\Users\<用户>\AppData\Roaming\my-app)
Linux$XDG_CONFIG_HOME/my-app 或 ~/.config/my-app

我在本机把 13.10 打包出的 my-app.app 运行起来,进程参数里可以直接看到 --user-data-dir=/Users/<我>/Library/Application Support/my-app,与上表一致。

现象:Windows 上 fs.readFileSync('C:\Users\tom\ AppData\...') 路径斜杠被转义;或把数据写进安装目录,升级安装后数据丢失;或团队里三人分别在 mac/Win/Linux 上跑,数据散落在三个不同的地方。

原因:手拼路径字符串。Windows 用 \、macOS/Linux 用 /;而且 JS 字符串里 \ 是转义字符,'C:\Users\tom' 里的 \U、\t 都会被吃掉(\t 甚至会变成制表符,让报错看起来莫名奇妙)。另一个方向的问题是写错了地方:安装目录(Program Files、/Applications、以及打包后的 app.asar 内部)要么不可写、要么不该写——升级安装会整体覆盖这些位置,数据就丢了。数据必须进 userData,它由系统按平台惯例分配,卸载重装、应用更新都不动它。

解决办法:路径只用 path.join/path.resolve 拼;可写位置只取 app.getPath('userData')。完整示例(主进程):

// src/index.js(节选)—— 在主进程准备一个跨平台的 JSON 存储
const { app } = require('electron');
const fs = require('node:fs');
const path = require('node:path');

// 三个平台各自返回正确的可写目录,代码一行都不用改
const dbFile = path.join(app.getPath('userData'), 'notes.json');

if (!fs.existsSync(dbFile)) {
  // 首次运行:写入空数据。注意用 utf-8 显式编码,避免 Windows 默认编码问题
  fs.writeFileSync(dbFile, JSON.stringify({ notes: [] }, null, 2), 'utf-8');
}
console.log('数据文件位置:', dbFile); // 打出来看看,你会对「用户数据在哪」建立直觉

两个高频追问:①应用名从哪来? 取自 package.json 的 name(打包后通常被 productName 影响),所以改这两个字段会让应用「换一个新的 userData 目录」,老用户数据像丢了——迁移前先固定命名。②开发时会污染正式数据吗? 会,开发与正式默认共用同一个 userData;隔离办法是把 app.setPath('userData', ...) 写在 app.whenReady() 之前并按 app.isPackaged 指向不同目录。另外,如果你的应用要以「便携模式」放在 U 盘里跑,官方文档也提供 app.setPath 改写任意命名路径的能力。

13.4 原生模块 ABI 报错与 @electron/rebuild

是什么/为什么:原生模块(native module)是含 C/C++/Rust 编译产物(.node 文件)的 npm 包,如 better-sqlite3、sharp。这类模块必须按宿主的 ABI(Application Binary Interface,二进制接口,Node 生态用 NODE_MODULE_VERSION 整数标记)编译。Electron 内嵌的 Node 与你本机安装的 Node ABI 不同,所以「在 Node 里能用、在 Electron 里报错」是必然而非偶然。

现象:启动或 require('better-sqlite3') 时抛出形如 Error: The module '…binding.node' was compiled against a different Node.js version using NODE_MODULE_VERSION X. This version requires NODE_MODULE_VERSION Y 的错误;Windows 上也可能表现为 A dynamic link library (DLL) initialization routine failed。(此报错原文格式来自 Node.js 与社区的通用错误模板;NODE_MODULE_VERSION 机制见 Node 官方 process.versions 文档。)

原因:npm install 默认按本机 Node 的 ABI 编译了原生模块,而运行你的应用的是 Electron 内嵌的 Node。

解决办法:用官方 @electron/rebuild(2026-10-04 npm 最新 4.2.0)按 Electron 的 ABI 重编。命令来自官方《Native Node Modules》文档:

# 在你的项目目录下执行
npm install --save-dev @electron/rebuild

# 每次 npm install 之后执行一次(macOS / Linux)
./node_modules/.bin/electron-rebuild

# Windows 对应写法(文档特别给出的 .cmd 后缀)
.\node_modules\.bin\electron-rebuild.cmd

三条省心的捷径:①用 Electron Forge 就不用手动跑——官方文档原话:「如果你在使用 Electron Forge,该工具会在开发模式和制作分发包时自动使用」。原调研在打包流程中 的 npm run make 日志里能看到独立的 Preparing native dependencies 步骤,就是它在工作;Forge 新项目模板还默认带 @electron-forge/plugin-auto-unpack-natives 插件,自动把 .node 文件移出 asar。②优先选自带 prebuild 预编译二进制的模块(prebuild/node-pre-gyp 机制会直接下载匹配 Electron 的版本,注意此时不要设置 npm_config_build_from_source 环境变量)。③换 Electron 大版本后记得重跑 rebuild——每次大版本都是新 ABI。

13.5 打包后找不到资源:asar 与 __dirname 的变化

是什么/为什么:asar 是 Electron 自创的归档格式,把你的整个应用(代码 + node_modules)打成一个 app.asar 文件,放在安装包的 resources 目录里。官方文档说明:大多数 Node fs API 会「透明地」读进 asar,但 child_process 的 exec/spawn 这类把路径当「命令字符串」执行的 API 不能读 asar 内的文件,含原生 .node 的模块也需要解包。原调研在打包流程中 原调研记录:npm run make 生成的 my-app.app/Contents/Resources/app.asar 里,用 npx @electron/asar list 能列出全部源码和 node_modules——你的代码确实在归档里,这就是打包后 __dirname 变成 …/resources/app.asar/src 的原因。

现象:npm start 正常;打包后 spawn 外部 exe 报 ENOENT、读取 ffmpeg 等二进制失败、图标/字体/模型文件读不到。

原因:这些文件被关进了 asar,而只有部分 API 认识 asar;或者你用 process.cwd()(工作目录,双击启动时是别的目录)当资源路径用。

解决办法分三招:

第一招,跟随入口的路径一律 __dirname 相对拼接(打包后自动落到 asar 内部,fs.readFileSync 之类直接可用):

// 主进程:读打进 asar 里的静态资源,用 __dirname 拼(官方模板加载 index.html 同款写法)
const path = require('node:path');
const raw = fs.readFileSync(path.join(__dirname, 'assets', 'tpl.txt'), 'utf-8');

第二招,**必须以「真实文件」存在的资源放到 resources 目录并用 process.resourcesPath(官方只读属性,resources 目录的绝对路径)定位;同时告诉 Forge 别把它卷进 asar、也别打进包:

// 主进程:读取随安装包分发、但必须在真实文件系统上的大文件
const path = require('node:path');
const ffmpeg = path.join(process.resourcesPath, 'bin', 'ffmpeg'); // 打包后 resources/bin/ffmpeg
// forge.config.js —— 给「不进 asar、原样复制」的文件留出口
module.exports = {
  packagerConfig: {
    asar: true,
    // extraResource 里的文件会被复制到 resources/ 根下,不进 asar
    extraResource: ['./bin/ffmpeg'],
  },
  // …(makers 与 plugins 同 13.10 的默认配置)
};

第三招,只想解包个别文件时用 asar 的 --unpack 机制(Forge 的 packagerConfig.asar 对象使用 unpack 或 unpackDir 子选项,见 Packager ASAR 配置;@electron/asar CLI 会生成 app.asar.unpacked 目录,须与 app.asar 一起分发)。排查时两条命令常驻手边:npx @electron/asar list app.asar(看文件在不在里面)与官方文档提到的 process.noAsar = true(临时关掉 fs 的 asar 兼容层,用于验证「到底是不是 asar 引起的」)。另一个易混点:app.getAppPath() 开发时返回项目根、打包后返回 app.asar 路径,官方教程正是推荐用它配合 app.isPackaged 判断运行环境。

13.6 应用图标不显示

是什么/为什么:应用本体、安装器和桌面入口各有图标位置——macOS 的 .app 用 .icns、Windows 的 exe/安装器用 .ico、Linux 桌面用 .png;Windows 上还要区分「应用本体图标」与「安装器(Setup.exe)图标」。Electron Forge 官方指南《Custom App Icons》给出规格:从一张 1024×1024px 源图开始,转换成各平台格式;Windows .ico 至少含 256×256,Linux .png 建议 512×512;macOS 26 及以上还支持 Apple Icon Composer 的新 .icon 格式(需 Xcode 26+ 的 actool 打包,可 .icns/.icon 双格式并存)。

现象:打包后图标仍是默认的 Electron 图标;或 macOS 正常、Windows 桌面快捷方式不对;或安装包 Setup.exe 是「白板图标」。

原因:①没配 packagerConfig.icon,默认就用 Electron 自带图标;②格式与平台不匹配——Forge 指南特别警告:把 .png 直接改名为 .ico 不行,会报 Fatal error: Unable to set icon;③只配了应用图标,没配安装器图标(它们是两套配置);④改了图标但没重新 make。

解决办法(以 Forge 为例,官方指南原文的完整配置形态):

// forge.config.js —— 图标相关配置汇总
module.exports = {
  packagerConfig: {
    asar: true,
    icon: './assets/icon', // 无需扩展名:packager 会自动按平台补 .icns/.ico/.png
    // macOS 26+ 想同时带新格式时才用数组,且必须写全扩展名:
    // icon: ['./assets/icon.icns', './assets/icon.icon'],
  },
  makers: [
    {
      name: '@electron-forge/maker-squirrel', // Windows Squirrel 安装器
      config: {
        // 安装后「控制面板 > 程序和功能」里显示的图标,必须是一个可下载的 URL
        iconUrl: 'https://example.com/icon.ico',
        // 生成的 Setup.exe 自身的图标(本地文件路径)
        setupIcon: './assets/icon.ico',
      },
    },
    {
      name: '@electron-forge/maker-dmg', // macOS DMG
      config: { icon: './assets/icon.icns' }, // DMG 窗口里展示的图标
    },
    {
      name: '@electron-forge/maker-wix', // Windows MSI
      config: { icon: './assets/icon.ico' },
    },
    {
      name: '@electron-forge/maker-deb', // Linux deb
      config: { options: { icon: './assets/icon.png' } },
    },
  ],
};

制作图标的最短路径:准备一张 1024×1024 的 PNG,用转换工具(Forge 指南推荐「线上各类转换工具」)生成 .icns 与多尺寸 .ico;macOS 上也可用系统自带 iconutil 从 iconset 目录生成 .icns(具体命令随 macOS 版本有差异,以 Apple 开发者文档为准——待核实)。改完务必重新 npm run make 再验收。

13.7 Mac 未签名 / 未公证:应用「无法打开」

是什么/为什么:macOS 的 Gatekeeper 根据应用来源、开发者签名、公证和安全策略判断是否允许打开。签名确认开发者身份及产物完整性;公证由 Apple 检查提交的软件是否存在已知恶意内容。未签名或未公证的下载应用可能显示阻止提示,具体结果受隔离属性、系统版本和管理策略影响。Apple:安全地打开应用、Forge:macOS 签名与公证。

原调研记录的一次未配置 Developer ID 签名的 make 产物带有 ad-hoc 签名:codesign -dv 显示 Signature=adhoc、TeamIdentifier=not set,标识符为默认的 com.github.Electron。这个历史产物没有取得 Developer ID 身份与公证保证,本机观察不能证明其他用户能够正常打开。

现象:双击提示「无法打开"xxx",因为无法验证开发者」;或更吓人的「"xxx" 已损坏,无法打开」(官方文档专门配了 macOS Sonoma 的这张 Gatekeeper 截图)。

原因:应用没有 Developer ID 签名,或签名了但没做公证,或下载时被 macOS 打上隔离属性(quarantine)。

解决办法分两层:

临时打开(用户侧,仅限确信来源可靠时):Apple 官方支持文档《Safely open apps on your Mac》给的流程是——先尝试打开一次并关掉弹窗,然后打开「系统设置 → 隐私与安全性」,滚动到安全区块,点「仍要打开(Open Anyway)」,再确认一次。注意:macOS Sequoia(15)起右键/Control-点按 → 打开的旧捷径已被 Apple 移除,网上老教程一律过时。开发者自测也可以用 xattr 清除隔离属性:xattr -dr com.apple.quarantine /Applications/你的应用.app(xattr 为系统自带工具;此命令仅用于已知可信产物的开发诊断,移除隔离属性会改变系统的来源检查条件)。

正式解决(开发者侧):加入 Apple Developer Program(年费以官网现价为准),取得 Developer ID 证书后,在 Forge 里配置签名与公证——Forge 底层就是官方的 @electron/osx-sign 与 @electron/notarize(官方 Code Signing 文档明确说明),在 forge.config.js 里补 packagerConfig.osxSign 与 osxNotarize 即可(需要 Apple ID、应用专用密码/Api Key 等凭据,详细步骤见 Forge 的《Signing macOS Apps》指南)。对内网分发的企业应用,也可以接受「ad-hoc 签名 + IT 统一放行」的折中方案。

13.8 Windows SmartScreen 误报:「Windows 已保护你的电脑」

是什么/为什么:SmartScreen 是微软基于信誉(reputation)的下载防护。微软官方文档《SmartScreen reputation for Windows app developers》有一张关键表格:未签名应用首启显示「Windows protected your PC」,用户必须点「更多信息 → 仍要运行(Run anyway)」(企业策略可以直接禁掉这个继续按钮);持有效 OV/EV 证书签名的应用同样先警告,显示已验证的发布者名称,直到信誉积累起来。

现象:用户下载你的 Setup.exe,弹蓝色全屏警告「Windows 已保护你的电脑」,转化率暴跌,客服被问「你们是不是病毒」。

原因:新应用、新版本、新证书都没有下载信誉——这是所有新桌面应用的必经阶段,不代表你做错了什么。特别提醒(2026 年的重要变化,微软官方原文):EV 证书已不再自动绕过 SmartScreen——「多年前 EV 证书默认获得正向信誉的行为已不复存在……只为躲避 SmartScreen 警告而买高价 EV 已不划算」。网上「买 EV 证书立解」的教程全部过时。

解决办法:

  1. 用户侧引导:在下载页写明「点『更多信息』→『仍要运行』」,并放上截图。这是零成本、最有效的手段。
  2. 开发者侧签名:用 OV 代码签名证书或 Azure Artifact Signing(微软云签名服务,与 OV 等效)签名,警告文案里至少能展示你的公司名;Forge 通过 windowsSign 配置统一接入(官方 Code Signing 文档)。
  3. 信誉是熬出来的:同一发布者持续发布、用户持续下载且无恶意行为,信誉逐步建立,警告随之消失;着急可以对特定文件走微软的恶意软件提交申诉通道(具体入口与时效见 Microsoft Learn——待核实)。
  4. 想完全无警告:走 Microsoft Store(MSIX)分发,商店证书天然覆盖,这是微软文档标注「无警告」的唯一通道。

13.9 自动更新不生效:排查清单

是什么/为什么:Electron 内置 autoUpdater 模块,官方更新教程的推荐形态是「Squirrel 框架 + 静态云存储」的 serverless 方案:你把新版本和元数据发布到对象存储,应用定期比对、后台下载、择机安装。macOS 端基于 Squirrel.Mac,Windows 端基于 Squirrel.Windows,两端元数据格式不同(macOS 是 JSON、Windows 是 RELEASES 文件),官方教程建议按 process.platform 区分端点。另一条路线是社区 electron-builder 的 electron-updater(2026-10-04 npm 最新 6.8.9,支持 generic/S3 等私有 feed 与差量下载)。

更新「不生效」时,按下面清单从上往下查(每条都对应官方文档的明确要求):

  1. 你在开发模式里测更新。 官方教程原话:更新代码「只在打包后的应用里执行,不要在开发时执行」,用 app.isPackaged 做门控。
  2. macOS 上应用没签名。 官方 autoUpdater 文档原话:「macOS 上自动更新必须签名,这是 Squirrel.Mac 的要求」。ad-hoc 签名同样不行(见 13.7)。
  3. setFeedURL 没有在 checkForUpdates() 之前调用。 文档明确要求;同时 checkForUpdates() 调两次会把更新下载两次。
  4. 服务器版本号不比本地大。 无论 Squirrel 还是 electron-updater,都是「feed 里的版本 > 当前版本」才触发更新;你把 1.0.0 装着、feed 里还是 1.0.0,当然「没反应」。
  5. quitAndInstall() 时机不对。 它只能在 update-downloaded 事件之后调用;官方还提醒 quitAndInstall 触发时不会先发 before-quit,收尾逻辑要挂 before-quit-for-update 事件。
  6. macOS 响应头/格式不对。 Squirrel.Mac 要求端点返回 JSON 元数据;此外 ATS(App Transport Security)要求更新流量走 HTTPS(需要例外时才在 plist 加 NSAllowsArbitraryLoads)。
  7. Windows 的 RELEASES 没就位。 Squirrel.Windows 客户端会请求 feed 的 /RELEASES 子路径,拿到的是 RELEASES 构建产物。

一份符合官方教程骨架的最小主进程代码(静态存储 serverless 形态):

// src/updater.js —— 自动更新(主进程,只在打包后启用)
const { app, autoUpdater, dialog } = require('electron');

const server = 'https://example.com/updates'; // 你的静态存储基址
const url = `${server}/update/${process.platform}/${app.getVersion()}`;

function setupAutoUpdater() {
  if (!app.isPackaged) return; // 清单第 1 条:开发模式直接跳过

  autoUpdater.setFeedURL({ url }); // 清单第 3 条:先设 feed
  autoUpdater.checkForUpdates();   // 有更新会自动后台下载

  autoUpdater.on('update-downloaded', async (_event, releaseNotes, releaseName) => {
    const { response } = await dialog.showMessageBox({
      type: 'info',
      title: '发现新版本',
      message: `新版本 ${releaseName} 已下载,是否立即重启?`,
      detail: releaseNotes?.toString() ?? '',
      buttons: ['稍后', '立即重启'],
      defaultId: 1,
    });
    // 只在 update-downloaded 之后调用;再次启动时也会应用更新
    if (response === 1) autoUpdater.quitAndInstall();
  });

  // 排查期把这两个事件也打出来,「静默失败」会现出原形
  autoUpdater.on('error', (err) => console.error('[updater]', err));
  autoUpdater.on('checking-for-update', () => console.log('[updater] checking…'));
}

module.exports = { setupAutoUpdater };

开源免费方案:官方 update.electronjs.org 服务可托管更新 feed,官方 README 列出的准入条件是:应用运行在 macOS 或 Windows、有公开的 GitHub 仓库、构建发布到 GitHub Releases、且构建经过代码签名(macOS 一侧强制)。若用 electron-builder 全家桶,则把上面的 autoUpdater 换成 electron-updater 的 autoUpdater.checkForUpdates(),并发布 latest.yml/latest-mac.yml 元数据——两套体系不要混用。

13.10 第一次打包体验:Forge 一条命令产出安装包

是什么/为什么:Electron Forge 是官方主推的打包工具链(底层组合 @electron/packager、@electron/osx-sign 等)。它把「打包 → 制作分发物 → 发布」做成 package/make/publish 三级命令。以下全流程为 2026-10-04 在 macOS(arm64,Node 24.12.0)实测,Windows/Linux 命令完全一致。

第 1 步:创建项目(这条命令不需要先装任何全局工具):

npm init electron-app@latest my-app   # 官方脚手架,等价于 npx create-electron-app
cd my-app

归档产物要点:src/index.js + src/preload.js + src/index.html 三件套(即 13.1/13.2 讲的结构);package.json 精确锁定 "electron": "44.5.1";开发依赖含 @electron-forge/cli@^8.0.1 与四个 maker(Squirrel.Windows、zip、deb、rpm)、plugin-auto-unpack-natives、plugin-fuses;脚本为 start/package/make/release。没有模板选择交互,默认即 Vanilla JS 模板;要 Vite 等模板,官方模板文档给出的命令是 npx create-electron-app@latest my-new-app --template=vite(webpack、TypeScript 同理,见 Forge Templates 页)。

第 2 步:先跑起来确认无误:

npm start   # 等价于 electron-forge start,热重载看改动

第 3 步:一条命令产出本机安装包:

npm run make

归档实验输出(节选):Packaging for arm64 on darwin → Preparing native dependencies(内置 @electron/rebuild 在工作,呼应 13.4)→ Making a zip distributable for darwin/arm64 → Artifacts available at: …/out/make。产物两处:

  • out/my-app-darwin-arm64/my-app.app——可直接运行的应用(macOS 形态是「应用包」而非安装器,拖进 /Applications 即完成「安装」);
  • out/make/zip/darwin/arm64/my-app-darwin-arm64-1.0.0.zip——分发用的压缩包。

第 4 步:安装并运行(原调研记录通过):

# macOS:直接打开(本机自产无隔离属性,不会触发 Gatekeeper)
open out/my-app-darwin-arm64/my-app.app

我在进程列表确认了它真正跑起来:--app-path=…/Resources/app.asar(代码在 asar 里,呼应 13.5)、--user-data-dir=…/Application Support/my-app(呼应 13.3)、渲染进程带 --enable-sandbox(呼应 13.2)。Windows 上的对应体验:out/make/squirrel.windows/x64/ 下会得到 Setup.exe(Squirrel 安装器,双击安装,自动建快捷方式,electron-squirrel-startup 负责首启处理);Linux 上得到 deb/rpm 包用系统包管理器安装。想出 macOS 的 DMG 或 Windows 的 MSI,往 forge.config.js 的 makers 里加 @electron-forge/maker-dmg/maker-wix(先 npm i -D 对应包)。

到这里,「写代码 → 本机跑 → 打包 → 安装」的完整闭环你已经亲手走通了一遍。剩下的分发难题(签名、公证、SmartScreen、自动更新)正是 13.7–13.9 的主题。

13.11 学习路径:从「跑起来了」到「能上线」

13.11.1 官方文档怎么读

  • 入口分层:electronjs.org/docs 左侧从上到下大致是 Getting Started(含端到端 Tutorial)、Processes(进程模型)、Best Practices(Security/Performance 检查单)、Examples(暗色模式、通知、菜单等场景示例)、Development、Native Node Modules、Distribution(打包/签名/更新)、Testing And Debugging、API Reference。先读 Tutorial 再查 API,别倒过来。
  • 每个 API 条目自带「户口」:页面顶部标 Process: Main 还是 Renderer(标错进程是最常见的误用,比如在渲染进程调 app 模块);参数表下方有 Version history,标注该选项自哪个版本引入/默认值何时改变——本课 13.2 的三个默认值就是从这里读出来的。读 API 文档的顺序建议:先看顶部的进程标记和一句描述,再看「Events/Instance Methods」清单建立目录感,最后才精读某个方法的参数表;从头逐字读到尾效率最低。
  • 版本策略:官方支持最近 3 个稳定大版本(当下即 42/43/44),最新稳定线获得全部修复;45 计划 2026-10-20 进入稳定。阅读旧博客时先对照这张时间表(electronjs.org 的 Releases 页),凡是教你「打开 nodeIntegration」的教程默认按过时处理。
  • 教程代码即官方仓库代码:每篇教程页的示例对应 electron/electron 仓库 docs/fiddles/ 下的目录(我核实了 docs/fiddles/tutorial-first-app/main.js 存在),可用 Electron Fiddle(官方试验场应用)一键打开运行,改坏了刷新即复原。
  • 一个重要更名:老教程人手一份的 electron/electron-quick-start 已改名为 electron/minimal-repro,定位是「最小复现模板」(提 bug 时用它),官方明确不再推荐它作为新项目起点,新项目请从 npm init electron-app 开始——这是 2026 年读老教程时最需要警惕的一条。

13.11.2 推荐资源与示例仓库

类型资源用途
官方教程electronjs.org/docs/latest/tutorial(First App → Preload → Features → Packaging → Distribution)系统学习主线
官方工具链文档electronforge.ioForge 配置、makers、签名公证指南
官方示例electron/electron 仓库 docs/fiddles/ + Fiddle 应用逐特性小例子
官方复现模板github.com/electron/minimal-repro(原 quick-start)提 issue 前做最小复现
社区工具文档electron.build(electron-builder/electron-updater)非官方打包路线的事实标准
系统方文档Apple《Safely open apps》+ Microsoft《SmartScreen reputation》13.7/13.8 的权威出处
社区Electron Discord、Stack Overflow electron 标签、GitHub Discussions提问前先搜 issue

13.11.3 下一步该学什么

本周(巩固):①把 13.10 的示例应用改造成自己的小工具(待办/剪贴板/看图),练 contextBridge + ipcMain.handle 的双向通信;②亲手制造一次本课的每个报错(改错一个路径、删掉 preload、装个 better-sqlite3 不 rebuild),再修好——踩坑的免疫力来自复现;③给应用加自定义图标并重新 make。

本月(工程化):①引入 Vite/React/Vue 前端构建(Forge 官方模板 --template=vite),理解 13.1 的双环境加载;②数据落 userData(13.3),配好 electron-store 或 SQLite;③打通 CI(GitHub Actions 三平台矩阵构建)与签名公证(13.7);④发布 1.0.0 并接上自动更新(13.9),完整走一次 1.0.1 的推送。

进阶(深入原理):①安全与性能两份官方 checklist 逐条对照自己的应用(禁用 nodeIntegrationInSubFrames、启用 ASAR 完整性校验、Fuses 硬化——13.10 模板已默认开启一组);②学 session/protocol/ServiceWorker 等中阶 API,理解网络与存储层;③读《Native Code and Electron》试着写一个自己的原生模块(13.4 的进阶);④研究多窗口/多进程架构与进程间状态同步;⑤关注 Electron 大版本升级的 Breaking Changes(每次大版本发布博客),把应用的升级变成例行公事。

参考来源

最后更新于

本页目录