首页 / Electron 入门教程 / 应用生命周期

Electron 入门教程

应用生命周期

本教程共 45 篇 · 第 6 篇 · 更新于 2026-08-03

Electronapp生命周期单实例

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-quitwill-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-fileopen-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 / LinuxmacOS
关掉所有窗口默认退出应用应用驻留,等 dock 唤起
单实例强制requestSingleInstanceLockFinder 双击系统自动强制
激活事件不常用点 dock 图标触发 activate

你会发现 macOS 总是那个”特殊”的平台,它的应用哲学强调驻留和重新激活。把这些分支写进 whenReady 回调里,是桌面应用最典型的跨平台处理模式。

6-12 生命周期事件速查

事件不少,实际写代码时常用的就那么几个。下面这张表按”什么时候触发、拿它干什么”整理,忘了随时回来翻。

事件触发时机典型用途
readyElectron 初始化完毕建窗口,推荐用 app.whenReady()
activatemacOS 点击 dock 图标没窗口时补建一个
window-all-closed全部窗口关闭非 macOS 平台调 app.quit()
second-instance第二个实例被启动把已有窗口提到前台
before-quit开始关窗口之前询问用户、拦截退出
will-quit窗口都关完、即将退出存盘、释放资源
quit应用真正退出打日志,别再做异步操作

留意最后一条:quit 触发时进程随时可能结束,在里面写异步任务大概率跑不完。要存盘就放 will-quit,那才是最后的安全窗口。

6-13 启动慢时的排查思路

有时你会觉得应用启动偏慢,先别急着装优化库。启动过程大致是:Electron 二进制起来、app 触发 ready、你建窗口、页面加载渲染。慢点往往出在最后两步。

页面里引了过大的库、没做代码分割,会拖慢首次渲染。窗口创建前塞了太多同步初始化,也会让 ready 到出窗口之间卡顿。把重活挪到窗口显示之后再异步做,体验会好很多。

另外 appready 之前不要做耗时操作。那段代码在主进程启动早期同步执行,会直接延后整个应用可见时间。保持入口轻量,是启动优化的第一原则。

6-14 本章小结

app 模块用事件驱动生命周期:whenReady 后建窗口、window-all-closed 控退出、activate 补 macOS 窗口、单实例用 requestSingleInstanceLocksecond-instance

这些事件组合,几乎是所有 Electron 应用的生命周期骨架。下一章我们深入窗口本身 BrowserWindow