本节摘要:app、BrowserWindow、Menu、Tray 是主进程的四大核心模块,分别掌管应用生命周期、窗口实体、菜单体系与托盘常驻。本节讲每个模块的关键 API 与生命周期归属、多窗口管理的两种工程模式(集中注册表与窗口基类),并用一个"带托盘的后台常驻应用"把四件套串成完整实现。
app 模块是主进程的节拍器,所有全局性的事件都挂它身上。工程上真正高频的钩子其实不多,一张表说清:
| 钩子/方法 | 触发时机 | 典型用途 |
|---|---|---|
| whenReady | 完成基础初始化 | 创建首个窗口、注册协议 |
| window-all-closed | 所有窗口关闭 | 非 mac 平台退出应用 |
| before-quit | 退出流程启动前 | 落盘未保存状态 |
| will-quit | 即将完全退出 | 注销全局快捷键 |
| getPath | 任意时刻 | 取用户数据、临时目录 |
| requestSingleInstanceLock | 启动早期 | 单实例应用 |
其中两条值得多说一句。单实例锁:桌面用户双击图标两次,默认会起两个应用实例、两份数据、一地鸡毛。标准解法在入口最顶端申请实例锁,拿不到就立刻退出:
// 单实例应用的标准骨架 const { app } = require('electron'); const gotLock = app.requestSingleInstanceLock(); if (!gotLock) { app.quit(); // 已有实例在跑,本实例立刻退场 } else { app.on('second-instance', () => { // 用户再次双击图标:把已有实例的主窗口拉到前台 const win = BrowserWindow.getAllWindows()[0]; if (win) { win.isMinimized() && win.restore(); win.focus(); } }); app.whenReady().then(createMainWindow); }
数据目录:app.getPath('userData') 给每个应用划定了专属目录(配置、缓存、日志都该住这里),不同操作系统的物理位置不同,但抽象路径一致——这是"一次编写三端运行"在文件系统层的兑现。
BrowserWindow 每次实例化就诞生一个渲染进程(2.1 节)。构造参数里工程价值最高的一族是窗口外观与行为的平台适配:
const win = new BrowserWindow({ width: 1024, height: 720, minWidth: 640, minHeight: 480, // 防止用户把界面拖成不可用形态 show: false, // 先画好再出场,避免白屏闪烁 backgroundColor: '#f7f8fa', // 提前铺底色,同上 titleBarStyle: 'hiddenInset', // macOS 融合式标题栏的常见选择 autoHideMenuBar: true, // Windows/Linux 折叠菜单栏 webPreferences: { preload: path.join(__dirname, 'preload.js'), sandbox: true, contextIsolation: true } }); win.once('ready-to-show', () => win.show()); // 首帧就绪再亮相
show:false 加 ready-to-show 这对组合是"消除启动白闪"的入门操作,第五章讲启动优化时还会升级成窗口状态记忆方案。另外记住窗口的两个告别方式:close 事件可拦截(弹"未保存"确认框),destroy 是不可逆的当场销毁——处理多窗口时用错会把用户数据问句直接吞掉。
Menu 管两处:应用菜单栏与上下文菜单(右键菜单)。Windows/Linux 上菜单栏挂在窗口上,macOS 上挂在系统顶部——同一份模板,两种挂载位置,跨平台差异被 API 抹平了。工程要点有二:其一,菜单不只是 UI,它还是快捷键的注册处(accelerator 字段),复制粘贴等编辑键在 mac 上必须靠菜单声明才能生效,这是新手应用"cmd+c 没反应"的头号原因;其二,上下文菜单要在渲染进程的 contextmenu 事件里经 IPC 请主进程弹出。
// 应用菜单模板:mac 编辑键声明 + 开发者工具入口 const menu = Menu.buildFromTemplate([ ...(process.platform === 'darwin' ? [{ role: 'appMenu' }] : []), { label: '编辑', submenu: [ { role: 'copy', accelerator: 'CmdOrCtrl+C' }, { role: 'paste', accelerator: 'CmdOrCtrl+V' } ] }, { label: '视图', submenu: [ { role: 'reload' }, { role: 'toggleDevTools' }, { role: 'zoomIn' }, { role: 'zoomOut' } ] } ]); Menu.setApplicationMenu(menu);
Tray 则是"驻守模式"的化身:托盘图标让应用在所有窗口关闭后仍活在系统角落。它的工程精髓在于关闭行为的重定义——托盘应用通常把"点关闭"翻译成"藏窗口"而不是"退应用"。

背景:做一个截图贴图工具,要求"关窗口不退出、托盘常驻、再次唤起聚焦、真正退出走托盘菜单"。
操作:四件套各司其职——app 负责单实例与退出策略;主窗口 close 事件被拦截为隐藏;Tray 图标点击时唤起窗口;托盘菜单提供"显示"与"退出"两项,退出时用 app.quit 走正门。
const { app, BrowserWindow, Tray, Menu, nativeImage } = require('electron'); const path = require('node:path'); let tray = null, mainWin = null; app.whenReady().then(() => { mainWin = new BrowserWindow({ width: 800, height: 560, show: false }); // 关闭重定义:藏起来,应用继续驻守 mainWin.on('close', (e) => { if (!app.isQuitting) { e.preventDefault(); mainWin.hide(); } }); tray = new Tray(nativeImage.createFromPath(path.join(__dirname, 'icon.png'))); tray.setToolTip('截图贴图工具'); tray.setContextMenu(Menu.buildFromTemplate([ { label: '显示主窗口', click: () => { mainWin.show(); mainWin.focus(); } }, { type: 'separator' }, { label: '退出', click: () => { app.isQuitting = true; app.quit(); } } ])); tray.on('click', () => mainWin.isVisible() ? mainWin.hide() : mainWin.show()); });
结果:三种关闭路径行为全部正确——点窗口关闭钮是隐藏、点托盘是显隐切换、托盘菜单的退出才真正退出。用户反复唤起零残留。
解读:app.isQuitting 这个自设标志位是模式的关键一环:它让 close 事件的拦截逻辑知道"这次是真的要退了"。变式:若产品还要支持"开机自启"与"全局快捷键唤起",就把 7.1 节的系统集成能力叠加进来,骨架不变。
四大件装配完毕,下一节接上外部的电线:文件、剪贴板、通知、快捷键这些系统能力的标准接法。