Electron 43.1.0:修掉应用菜单热替换崩溃,桌面应用可以更稳地更新菜单

2026-07-09 30 预计阅读时间: 1 分钟
来源: oschina.net AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:9 分钟

Electron v43.1.0 已发布。这是一个偏修复型的版本,最值得桌面应用开发者注意的一点是:修复了替换已经打开的应用程序菜单时可能触发的崩溃。对于依赖动态菜单的应用,比如根据登录状态、当前文档、插件能力或窗口焦点实时调整菜单的产品,这类修复比看起来更关键。

这次更新影响的是哪类应用

Electron 本身把 Chromium、Node.js 和原生桌面能力封装在一起,让开发者用 JavaScript、HTML 和 CSS 构建 macOS、Windows、Linux 三端桌面应用。菜单系统是 Electron 应用里比较靠近操作系统的一层:你写的是 JavaScript,但最终会映射到平台原生菜单行为。

这次 v43.1.0 提到的修复点是“替换已打开的应用程序菜单时触发的崩溃”。换句话说,风险通常出现在这种场景:

  • 用户已经打开了应用顶部菜单或上下文菜单;
  • 应用代码此时调用 Menu.setApplicationMenu() 替换菜单;
  • 替换时机和原生菜单生命周期撞在一起,导致进程崩溃。

这不是每个 Electron 应用都会遇到的问题。一个静态菜单、启动后基本不变的应用,受影响面较小。但如果你的应用会频繁重建菜单,就值得尽快验证 v43.1.0。

动态菜单为什么容易踩坑

很多桌面应用都会把菜单当成状态的一部分。例如:

  • 未登录时显示“登录”,登录后显示“同步”“退出登录”;
  • 编辑器有选中文本时启用“复制”“格式化”,没有选区时禁用;
  • 当前文件是只读时禁用“保存”;
  • 插件安装或卸载后刷新菜单项;
  • 多窗口应用根据当前激活窗口切换菜单。

这些逻辑本身没问题,但菜单不是普通 DOM。DOM 节点更新失败,通常只是界面不对;原生菜单更新撞上平台状态,严重时可能直接影响主进程稳定性。Electron v43.1.0 对“打开中的应用菜单被替换”崩溃做了修复,说明这类边界状态在真实应用里足够重要。

可以这样实践:集中管理菜单刷新

下面是一个最小 Electron 示例,演示如何把菜单构建逻辑集中在主进程里,并通过 IPC 根据渲染进程状态刷新菜单。示例可以直接改造成你的应用结构。

运行前需要安装依赖:

mkdir electron-menu-refresh-demo
cd electron-menu-refresh-demo
npm init -y
npm install electron@43.1.0 --save-dev

package.json 改成:

{
  "name": "electron-menu-refresh-demo",
  "version": "1.0.0",
  "private": true,
  "main": "main.js",
  "scripts": {
    "start": "electron ."
  },
  "devDependencies": {
    "electron": "43.1.0"
  }
}

创建 main.js

const { app, BrowserWindow, Menu, ipcMain } = require('electron');

let mainWindow;
let state = {
  signedIn: false,
  documentDirty: false
};

function buildApplicationMenu() {
  const template = [
    {
      label: 'File',
      submenu: [
        {
          label: state.documentDirty ? 'Save Changes' : 'Save',
          enabled: state.documentDirty,
          click: () => {
            if (mainWindow) {
              mainWindow.webContents.send('menu:save');
            }
          }
        },
        { type: 'separator' },
        { role: 'quit' }
      ]
    },
    {
      label: 'Account',
      submenu: [
        {
          label: state.signedIn ? 'Sign out' : 'Sign in',
          click: () => {
            state.signedIn = !state.signedIn;
            refreshApplicationMenu();
            if (mainWindow) {
              mainWindow.webContents.send('account:changed', state.signedIn);
            }
          }
        }
      ]
    }
  ];

  return Menu.buildFromTemplate(template);
}

function refreshApplicationMenu() {
  Menu.setApplicationMenu(buildApplicationMenu());
}

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 900,
    height: 600,
    webPreferences: {
      preload: `${__dirname}/preload.js`
    }
  });

  mainWindow.loadFile('index.html');
  refreshApplicationMenu();
}

ipcMain.on('document:setDirty', (_event, documentDirty) => {
  state.documentDirty = Boolean(documentDirty);
  refreshApplicationMenu();
});

app.whenReady().then(createWindow);

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

创建 preload.js

const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('appApi', {
  setDocumentDirty: (dirty) => ipcRenderer.send('document:setDirty', dirty),
  onSaveFromMenu: (handler) => ipcRenderer.on('menu:save', handler),
  onAccountChanged: (handler) => ipcRenderer.on('account:changed', (_event, signedIn) => handler(signedIn))
});

创建 index.html

<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Electron Menu Refresh Demo</title>
  </head>
  <body>
    <h1>Electron Menu Refresh Demo</h1>
    <p id="status">Document is clean.</p>
    <p id="account">Signed out.</p>
    <button id="dirty">Mark document dirty</button>
    <button id="clean">Mark document clean</button>

    <script>
      const status = document.querySelector('#status');
      const account = document.querySelector('#account');

      document.querySelector('#dirty').addEventListener('click', () => {
        window.appApi.setDocumentDirty(true);
        status.textContent = 'Document has unsaved changes.';
      });

      document.querySelector('#clean').addEventListener('click', () => {
        window.appApi.setDocumentDirty(false);
        status.textContent = 'Document is clean.';
      });

      window.appApi.onSaveFromMenu(() => {
        window.appApi.setDocumentDirty(false);
        status.textContent = 'Saved from application menu.';
      });

      window.appApi.onAccountChanged((signedIn) => {
        account.textContent = signedIn ? 'Signed in.' : 'Signed out.';
      });
    </script>
  </body>
</html>

启动:

npm start

这个示例的重点不是 UI,而是菜单刷新策略:菜单模板由主进程统一生成,渲染进程只发送状态变化。升级到 v43.1.0 后,可以重点测试“菜单打开时状态变化并触发菜单替换”的场景。

升级时要测什么

建议把 v43.1.0 当作稳定性补丁来验证,而不是只看应用能否启动。可以按下面清单跑一遍:

  • 在 macOS、Windows、Linux 上分别打开应用菜单后触发状态变化;
  • 在菜单展开时执行登录、退出登录、切换工作区、插件启停等会重建菜单的操作;
  • 检查主进程是否有崩溃、未捕获异常或菜单项状态错乱;
  • 如果应用有自动更新机制,确认新版本安装后菜单行为一致;
  • 对长期运行的应用,测试多次重复刷新菜单后的内存和响应情况。

升级命令很直接:

npm install electron@43.1.0 --save-dev
npm test
npm start

如果你使用 pnpmyarn,对应命令可以改成:

pnpm add -D electron@43.1.0
# 或
 yarn add -D electron@43.1.0

采用建议

如果你的 Electron 应用存在动态菜单,尤其是会在用户交互过程中调用 Menu.setApplicationMenu(),v43.1.0 值得优先纳入验证。这个版本的公开重点是修复崩溃,不适合期待它带来明显功能变化;它的价值在于减少一个靠近原生菜单生命周期的稳定性风险。

边界也要说清楚:升级 Electron 只能修掉框架层已经修复的问题,不能替代应用自身的菜单状态管理。菜单刷新仍应集中、可追踪,避免多个窗口、插件或异步任务同时争抢全局应用菜单。对复杂桌面应用来说,稳定的菜单系统不是小装饰,而是主进程质量的一部分。


相关推荐