Electron 进阶主题
原生模块、多进程通信、单实例、协议注册、托盘常驻与崩溃恢复。
原生模块
npm install better-sqlite3
npx electron-rebuild -f -w better-sqlite3Native 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.
{
"scripts": {
"postinstall": "electron-rebuild -f -w better-sqlite3"
},
"build": {
"asarUnpack": ["**/node_modules/better-sqlite3/**"]
}
}单实例
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.
自定义协议
// 注册 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.
托盘常驻
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.
崩溃恢复
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.多窗口通信
// 主进程做中转
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.
原生模块
npm install better-sqlite3
npx electron-rebuild -f -w better-sqlite3原生模块是按 Node 的 ABI 编译的,Electron 的 ABI 不同,所以必须重编译。electron-rebuild 会针对当前 Electron 版本重新构建 .node 文件。
{
"scripts": {
"postinstall": "electron-rebuild -f -w better-sqlite3"
},
"build": {
"asarUnpack": ["**/node_modules/better-sqlite3/**"]
}
}单实例
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);
});
}没有单实例锁时,用户双击两次图标就有两个进程在跑,各自读写同一个配置文件——数据很容易互相覆盖。带文件关联的应用必须加。
自定义协议
// 注册 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);
});myapp://order/42 这种深链接常用于「网页点一下,打开桌面应用并跳到对应页面」。三个平台的传递机制不同,要分别处理。
托盘常驻
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 里拦下并隐藏,before-quit 里放行。少了后者,用户点托盘「退出」也退不掉。
崩溃恢复
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);
});render-process-gone 的 reason 常见值有 crashed、oom、killed。oom 说明渲染进程内存耗尽——通常是页面里存了过大的数据结构,重新加载只是暂时缓解。多窗口通信
// 主进程做中转
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]);窗口之间不能直接通信,必须经过主进程中转。MessageChannelMain 建一条点对点通道后,两个窗口可以直连,适合高频消息(比如拖拽同步)。