应用生命周期
本教程共 45 篇 · 第 6 篇 · 更新于 2026-08-03
6. 应用生命周期
本节目标
- 掌握 app 模块的 ready 事件与 app.whenReady 的用法。
- 用 window-all-closed 处理 Windows/Linux 的退出逻辑。
- 用 activate 事件补齐 macOS 重新激活窗口的行为。
- 理解 app.quit 与退出前的清理时机。
- 用 requestSingleInstanceLock 实现单实例并响应二次启动。
窗口能建了,但应用整体怎么管?app 模块就是 Electron 的”应用总管”,负责启动、激活、休眠、退出。本章把最常用的生命周期事件串起来,再补一个实用的单实例需求。
6-1 app 模块管什么
app 模块提供一组事件和方法,控制应用级行为。比如程序化退出、修改 dock、显示关于面板,都归它管。主进程里直接 require('electron').app 就能拿到。
它本质是 Node.js 的事件发射器,所以大量能力通过 .on('事件名', 回调) 来监听。生命周期相关的事件,是写桌面应用最先要熟悉的。
6-2 ready:一切开始的信号
窗口只能在 app 就绪后创建。前面我们用 app.whenReady() 等待这个时机,它返回 Promise,比直接监听 ready 事件更稳妥。
// 主进程
const { app } = require('electron/main')
app.whenReady().then(() => {
console.log('应用已就绪,可以建窗口了')
})
如果你尝试在 ready 之前 new BrowserWindow,Electron 会直接报错。所以所有创建窗口的代码,都该放进 whenReady 之后。
6-3 window-all-closed:关掉所有窗口
Windows 和 Linux 的习惯是:所有窗口关了,应用就退出。监听 window-all-closed 事件,在非 macOS 平台调 app.quit()。
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
macOS 例外,因为苹果用户习惯关掉窗口后应用仍驻留,等从 dock 重新点开。所以这里用 process.platform !== 'darwin' 把 macOS 排除掉。
6-4 activate:macOS 重新激活
macOS 上点击 dock 图标时,若没有窗口,应当重建一个。这靠 activate 事件实现。它只在 ready 之后监听才有意义。
app.whenReady().then(() => {
createWindow()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
})
BrowserWindow.getAllWindows() 返回当前所有窗口实例。数量为 0 说明都关了,这时补建一个,体验才符合 macOS 规范。
6-5 quit:主动退出
app.quit() 会尝试关闭所有窗口,然后触发退出。你也可以监听 before-quit、will-quit 做退出前的清理,比如保存配置。
app.on('before-quit', (event) => {
// 需要拦截时可调用 event.preventDefault()
console.log('准备退出,这里可以存盘')
})
提示:用户按 Cmd+Q(macOS)或调用 app.quit() 时,Electron 先关窗口再发 will-quit,此时不会触发 window-all-closed。两者职责不同,别混用。
6-6 单实例:只让一个程序跑
很多桌面应用希望全局只有一个实例。比如用户双击了两次图标,第二次应该把已开的窗口提到前台,而不是再开一个。
Electron 提供 app.requestSingleInstanceLock()。主实例拿到锁返回 true,第二个实例拿不到返回 false,应当直接退出。
// 主进程
const { app, BrowserWindow } = require('electron/main')
let myWindow = null
const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
// 不是主实例,直接退出
app.quit()
} else {
app.on('second-instance', (event, commandLine, workingDirectory) => {
// 第二实例启动时被触发,这里把主窗口提到前台
if (myWindow) {
if (myWindow.isMinimized()) myWindow.restore()
myWindow.focus()
}
})
app.whenReady().then(() => {
myWindow = new BrowserWindow({ width: 800, height: 600 })
myWindow.loadFile('index.html')
})
}
6-7 second-instance:处理二次启动
second-instance 事件只在主实例里触发,参数带第二个实例的命令行与工作目录。你可以据此处理”用文件打开应用”之类的场景。
app.on('second-instance', (event, commandLine, workingDirectory) => {
// commandLine 里可能带有用户双击的文件路径
console.log('第二实例参数:', commandLine)
if (myWindow) myWindow.focus()
})
macOS 在 Finder 里双击第二份时,系统会强制单实例并改发 open-file、open-url 事件。命令行启动才会绕过系统机制,这时 requestSingleInstanceLock 才派上用场。
6-8 一个完整的生命周期骨架
把上述事件拼起来,主进程骨架就完整了:
// main.js —— 主进程
const { app, BrowserWindow } = require('electron/main')
const createWindow = () => {
const win = new BrowserWindow({ width: 800, height: 600 })
win.loadFile('index.html')
return win
}
// 注意用 if / else 包住:app.quit() 之后代码仍会往下跑
if (!app.requestSingleInstanceLock()) {
app.quit()
} else {
app.whenReady().then(() => {
const win = createWindow()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
app.on('second-instance', () => {
if (win.isMinimized()) win.restore()
win.focus()
})
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})
}
这里的 if / else 不是可有可无的排版。app.quit() 只是发起退出流程,它不会立刻中断当前脚本,后面的代码照样执行。不用 else 包住,第二实例仍会注册一堆监听器,甚至真的把窗口建出来。
6-9 退出前的清理时机
有些应用需要在退出前存盘或释放资源。before-quit 在窗口开始关闭前触发,will-quit 在窗口都关掉、即将退出时触发。
// 主进程;saveConfig 是你自己实现的存盘函数
app.on('will-quit', () => {
// 在这里写配置、关数据库连接
saveConfig()
})
如果你在 before-quit 里调用了 event.preventDefault(),退出会被取消。这适合”还有未保存内容,先问用户”的场景,但要小心别让应用永远关不掉。
6-10 单实例锁的摆放顺序
requestSingleInstanceLock() 的调用要尽量早,最好放在文件顶部、whenReady 之前。这样第二实例一启动就被拦下退出,不会做无谓的初始化。
// 放在最前面,先于任何窗口创建
if (!app.requestSingleInstanceLock()) {
app.quit()
}
另一个常见误区是忘了处理 macOS。在 Finder 里双击第二份时系统会强制单实例,只有命令行启动才需要你的锁。两套机制互补,别以为锁能覆盖所有平台行为。
6-11 平台差异对照
生命周期行为在三大系统上并不完全一致,下面这张表帮你快速对照。写跨平台应用时,这些差异要用 process.platform 分别处理。
| 行为 | Windows / Linux | macOS |
|---|---|---|
| 关掉所有窗口 | 默认退出应用 | 应用驻留,等 dock 唤起 |
| 单实例强制 | 需 requestSingleInstanceLock | Finder 双击系统自动强制 |
| 激活事件 | 不常用 | 点 dock 图标触发 activate |
你会发现 macOS 总是那个”特殊”的平台,它的应用哲学强调驻留和重新激活。把这些分支写进 whenReady 回调里,是桌面应用最典型的跨平台处理模式。
6-12 生命周期事件速查
事件不少,实际写代码时常用的就那么几个。下面这张表按”什么时候触发、拿它干什么”整理,忘了随时回来翻。
| 事件 | 触发时机 | 典型用途 |
|---|---|---|
ready | Electron 初始化完毕 | 建窗口,推荐用 app.whenReady() |
activate | macOS 点击 dock 图标 | 没窗口时补建一个 |
window-all-closed | 全部窗口关闭 | 非 macOS 平台调 app.quit() |
second-instance | 第二个实例被启动 | 把已有窗口提到前台 |
before-quit | 开始关窗口之前 | 询问用户、拦截退出 |
will-quit | 窗口都关完、即将退出 | 存盘、释放资源 |
quit | 应用真正退出 | 打日志,别再做异步操作 |
留意最后一条:quit 触发时进程随时可能结束,在里面写异步任务大概率跑不完。要存盘就放 will-quit,那才是最后的安全窗口。
6-13 启动慢时的排查思路
有时你会觉得应用启动偏慢,先别急着装优化库。启动过程大致是:Electron 二进制起来、app 触发 ready、你建窗口、页面加载渲染。慢点往往出在最后两步。
页面里引了过大的库、没做代码分割,会拖慢首次渲染。窗口创建前塞了太多同步初始化,也会让 ready 到出窗口之间卡顿。把重活挪到窗口显示之后再异步做,体验会好很多。
另外 app 的 ready 之前不要做耗时操作。那段代码在主进程启动早期同步执行,会直接延后整个应用可见时间。保持入口轻量,是启动优化的第一原则。
6-14 本章小结
app 模块用事件驱动生命周期:whenReady 后建窗口、window-all-closed 控退出、activate 补 macOS 窗口、单实例用 requestSingleInstanceLock 加 second-instance。
这些事件组合,几乎是所有 Electron 应用的生命周期骨架。下一章我们深入窗口本身 BrowserWindow。