Electron 主进程与 IPC 能力
本页介绍 Hydrogen Music 的 Electron 主进程入口及其已在源码中确认的 IPC 能力:应用生命周期、主窗口管理、网易云 API 就绪状态、Cookie 清理、更新检查标记,以及主进程向渲染进程发送播放控制事件。
Purpose and Scope
本页聚焦桌面端 Electron 主进程的启动与进程间通信边界,入口文件为 background.js。覆盖内容包括:
- Electron 单实例锁与应用生命周期;
BrowserWindow主窗口的创建入口及窗口状态持久化依赖;ipcMain.on与ipcMain.handle注册的 IPC 通道;- 网易云 API 启动就绪状态的异步等待机制;
- 网易云 API Cookie 的清理流程;
- macOS Dock 菜单向渲染进程发送播放控制事件;
- 快捷键注销和窗口关闭行为。
渲染进程中的 Vue 组件、Pinia 状态、具体 IPC 调用封装,以及 src/electron/ipcMain.js 中未在本次源代码窗口内展开的处理逻辑不在本页中详细展开;源码只确认 background.js 与该文件存在协同关系。因此,若需要记录完整的渲染端调用方,应另设渲染进程或前端状态页面。
Overview
Electron 主进程以 background.js 作为 package.json 的 main 入口。启动后,它首先加载日志清理模块和网易云 API 服务;在 Linux 上按需加载 MPRIS 集成;随后从 Electron 获取 app、BrowserWindow、ipcMain、session、screen 等主进程能力。
应用启动采用以下顺序:
- 调用
app.requestSingleInstanceLock(),阻止第二个实例继续运行; - 在
app.whenReady()中创建主窗口; - 异步启动网易云 API,并将结果广播给等待者;
- 注册 IPC 通道,包括
check-for-update、ncm-api-ready-state和ncm-api-cookie-clear; - 在 macOS 激活事件中恢复或重新创建窗口;
- 在退出前注销全局快捷键。
这种设计将需要主进程权限的操作集中在 background.js:渲染进程不直接管理 Electron 会话 Cookie、应用单实例锁或系统窗口,而是通过 IPC 请求主进程完成这些操作。
Architecture
Source: background.js
架构中的直接证据来自 background.js:它导入 startNeteaseMusicApi、条件导入 createMpris,从 Electron 解构出 app、BrowserWindow、ipcMain、session、screen 等对象,并导入 createMainWindowState。package.json 则确认 background.js 是 Electron 主入口,并声明 electron-store、electron-updater 与 MPRIS 相关依赖。
IPC 边界
当前主进程明确注册的通道如下:
| 通道 | 注册方式 | 主进程行为 |
|---|---|---|
check-for-update | ipcMain.on | 只设置 manualUpdateCheckInProgress = true,不返回值 |
ncm-api-ready-state | ipcMain.handle | 返回 API 就绪 Promise;若尚未就绪则等待 |
ncm-api-cookie-clear | ipcMain.handle | 清理两个本地 API Origin 的 Cookie,并返回布尔值 |
music-playing-control | webContents.send | macOS Dock 菜单触发,向渲染进程发送播放/暂停事件 |
music-song-control | webContents.send | macOS Dock 菜单触发,携带 last 或 next |
Source: background.js
Main Content
应用单实例与生命周期
主进程通过 app.requestSingleInstanceLock() 取得单实例锁。锁失败时立即调用 app.quit();锁成功时注册 second-instance 事件。第二个实例启动后,已有窗口会被恢复、显示并聚焦,而不是创建第二个主窗口。这避免了多个播放器进程同时持有窗口、快捷键或本地 API 资源。
app.whenReady() 是主初始化入口。代码先调用 createWindow(),再启动 startNeteaseMusicApi()。API 启动成功或失败都会调用 resolveNcmApiReady,因此等待 IPC 的调用方不会因为启动异常而永久悬挂;失败会以 { ready: false, error } 形式发布。
1const gotTheLock = app.requestSingleInstanceLock()
2
3if (!gotTheLock) {
4 app.quit()
5} else {
6 app.on('second-instance', (event, commandLine, workingDirectory) => {
7 if (myWindow) {
8 if (myWindow.isMinimized()) myWindow.restore()
9 if (!myWindow.isVisible()) myWindow.show()
10 myWindow.focus()
11 }
12 })
13
14 app.whenReady().then(async () => {
15 createWindow()
16 startNeteaseMusicApi()
17 .then((result) => {
18 const payload = result && typeof result == 'object'
19 ? { ready: !!result.ready, ...(result.error ? { error: result.error } : {}) }
20 : { ready: true }
21 resolveNcmApiReady(payload)
22 })
23 .catch((err) => {
24 const errorMessage = err && err.message ? err.message : 'unknown error'
25 resolveNcmApiReady({ ready: false, error: errorMessage })
26 })
27 })
28}Source: background.js
窗口生命周期遵循 Electron 平台约定:activate 时,如果没有窗口则重新创建,否则显示并聚焦现有窗口;window-all-closed 在非 macOS 平台退出应用;will-quit 注销所有全局快捷键;before-quit 设置 forceQuit 标记。源码片段表明这些行为集中在主进程,而非交给渲染端决定。
主窗口创建与状态持久化
createWindow() 设置应用名 Hydrogen Music,为设置和窗口状态分别创建 electron-store 实例,并把它们传给 createMainWindowState。主窗口的 BrowserWindow 配置根据平台选择不同的边框策略:macOS 使用原生交通灯和隐藏式标题栏,其他平台使用无边框窗口。
1process.env.DIST = path.join(__dirname, './')
2const indexHtml = path.join(process.env.DIST, 'dist/index.html')
3const Store = require('electron-store').default
4const settingsStore = new Store({ name: 'settings' })
5const windowStateStore = new Store({ name: 'window-state' })
6const windowState = createMainWindowState(
7 settingsStore,
8 windowStateStore,
9 screen.getPrimaryDisplay().workAreaSize
10)
11const isMac = process.platform === 'darwin'
12const win = new BrowserWindow({
13 ...windowState.windowOptions,
14 frame: isMac ? true : false,
15 titleBarStyle: isMac ? 'hiddenInset' : undefined,
16 titleBarOverlay: isMac
17 ? { color: '#00000000', symbolColor: '#000000', height: 35 }
18 : undefined,
19 title: 'Hydrogen Music'
20})Source: background.js
源码窗口在 BrowserWindow 配置之后被截断,因此本页不对 loadFile、loadURL、webPreferences 或窗口级 IPC 转发作额外推断。已确认的设计意图是:窗口选项由 createMainWindowState 统一生成,状态数据由 electron-store 保存,主进程负责把显示区域尺寸传入状态工厂。
API 就绪状态:一次性解析与等待队列
API 就绪状态使用三个主进程变量保存:ncmApiReadyResolved 表示是否已经完成;ncmApiReadyPayload 保存最终结果;ncmApiReadyWaiters 保存尚未完成时的 Promise resolver。
resolveNcmApiReady 具有幂等保护:如果已经解析,后续调用直接返回;首次解析时保存 payload,并逐一调用等待者。每个等待者独立使用 try/catch,因此某个 resolver 出错不会阻断其他等待者。
waitForNcmApiReady 对已完成状态立即返回 Promise.resolve(ncmApiReadyPayload);未完成时则把 resolver 放入队列。这使 ipcMain.handle('ncm-api-ready-state') 可以安全地被渲染进程提前调用,而不需要轮询。
1function resolveNcmApiReady(payload) {
2 if (ncmApiReadyResolved) return;
3 ncmApiReadyResolved = true;
4 ncmApiReadyPayload = payload;
5 while (ncmApiReadyWaiters.length > 0) {
6 const waiter = ncmApiReadyWaiters.shift();
7 try {
8 waiter(payload);
9 } catch (_) {}
10 }
11}
12
13function waitForNcmApiReady() {
14 if (ncmApiReadyResolved) return Promise.resolve(ncmApiReadyPayload);
15 return new Promise(resolve => {
16 ncmApiReadyWaiters.push(resolve);
17 });
18}Source: background.js
Cookie 清理 IPC
clearNcmApiCookies() 只在 session.defaultSession 存在时继续执行,否则返回 false。它遍历两个本地 API 地址:http://localhost:36530 与 http://127.0.0.1:36530。每个地址先读取 Cookie,再通过 Promise.all 并发删除对应名称的 Cookie。单个 Origin 的读取或删除失败会被忽略,函数仍会继续处理另一个 Origin,最后返回 true。
这说明该操作是“尽力清理”而非事务性清理:源码没有收集删除失败,也没有抛出异常给 IPC 调用方。调用方只能根据布尔返回值判断会话是否存在并进入了清理流程,无法知道每个 Cookie 的删除结果。
1async function clearNcmApiCookies() {
2 if (!session || !session.defaultSession) return false
3
4 for (const url of NCM_API_COOKIE_URLS) {
5 try {
6 const cookies = await session.defaultSession.cookies.get({ url })
7 await Promise.all(cookies.map(({ name }) =>
8 session.defaultSession.cookies.remove(url, name)
9 ))
10 } catch (_) {
11 // ignore cookie cleanup failures for this origin
12 }
13 }
14
15 return true
16}Source: background.js