Electron 进阶主题

原生模块、多进程通信、单实例、协议注册、托盘常驻与崩溃恢复。

Advanced
Key Points进阶能力大多围绕「和系统更深地结合」:注册协议、常驻托盘、调用 C++ 模块、多窗口协同。

原生模块

Bash
npm install better-sqlite3
npx electron-rebuild -f -w better-sqlite3

Native modules compile against Node's ABI, which differs from Electron's, so they must be rebuilt. electron-rebuild rebuilds the .node file for the current Electron version.

JSON
{
  "scripts": {
    "postinstall": "electron-rebuild -f -w better-sqlite3"
  },
  "build": {
    "asarUnpack": ["**/node_modules/better-sqlite3/**"]
  }
}

单实例

JavaScript
const gotLock = app.requestSingleInstanceLock();

if (!gotLock) {
  app.quit();
} else {
  app.on('second-instance', (_e, argv) => {
    // 用户又双击了一次图标,激活已有窗口
    if (mainWindow) {
      if (mainWindow.isMinimized()) mainWindow.restore();
      mainWindow.show();
      mainWindow.focus();
    }
    // 还能拿到第二次启动的命令行参数(比如打开的文件)
    const file = argv.find((a) => a.endsWith('.md'));
    if (file) mainWindow.webContents.send('open-file', file);
  });
}

Without the lock, double-clicking the icon twice runs two processes writing the same config file, and they overwrite each other. Any app with file associations needs it.

自定义协议

JavaScript
// 注册 myapp:// 协议,用户点网页里的链接就能唤起应用
if (process.defaultApp) {
  if (process.argv.length >= 2) {
    app.setAsDefaultProtocolClient('myapp', process.execPath,
        [path.resolve(process.argv[1])]);
  }
} else {
  app.setAsDefaultProtocolClient('myapp');
}

// Windows / Linux:从命令行参数拿到 URL
app.on('second-instance', (_e, argv) => {
  const url = argv.find((a) => a.startsWith('myapp://'));
  if (url) handleDeepLink(url);
});

// macOS:从 open-url 事件拿到
app.on('open-url', (e, url) => {
  e.preventDefault();
  handleDeepLink(url);
});

Deep links like myapp://order/42 let a web page open the desktop app at a specific screen. The delivery mechanism differs across the three platforms and must be handled separately.

托盘常驻

JavaScript
let isQuitting = false;

win.on('close', (e) => {
  if (!isQuitting) {
    e.preventDefault();
    win.hide();                       // 关窗口只是隐藏
  }
});

app.on('before-quit', () => { isQuitting = true; });

tray.setContextMenu(Menu.buildFromTemplate([
  { label: '显示', click: () => win.show() },
  { label: '退出', click: () => { isQuitting = true; app.quit(); } }
]));

Close-to-tray needs both halves: intercept close and hide, then allow quit in before-quit. Without the second, the tray's Quit item does nothing.

崩溃恢复

JavaScript
app.on('render-process-gone', (_e, contents, details) => {
  console.error('渲染进程崩溃', details.reason);
  if (details.reason !== 'clean-exit') {
    dialog.showMessageBox({
      type: 'error',
      buttons: ['重新加载', '退出'],
      message: '页面崩溃了,是否重新加载?'
    }).then(({ response }) => {
      if (response === 0) contents.reload();
      else app.quit();
    });
  }
});

app.on('child-process-gone', (_e, details) => {
  console.error('子进程退出', details.type, details.reason);
});
reason is typically crashed, oom or killed. oom means the renderer ran out of memory, usually from an oversized data structure in the page; reloading only postpones it.

多窗口通信

JavaScript
// 主进程做中转
ipcMain.on('broadcast', (_e, msg) => {
  BrowserWindow.getAllWindows().forEach((w) => {
    w.webContents.send('message', msg);
  });
});

// 或直接用 MessageChannelMain 建一条直连通道
const { MessageChannelMain } = require('electron');
const { port1, port2 } = new MessageChannelMain();
winA.webContents.postMessage('port', null, [port1]);
winB.webContents.postMessage('port', null, [port2]);

Windows cannot talk directly; everything goes through the main process. MessageChannelMain creates a peer channel afterwards, which suits high-frequency traffic such as drag synchronisation.

Friends & Partners