Skip to content

Commit c49d22c

Browse files
committed
perf: 🚀 The main panel supports single instance startup
1 parent 6c4242c commit c49d22c

File tree

4 files changed

+205
-18
lines changed

4 files changed

+205
-18
lines changed

electron/helpers/emitter.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { EventEmitter } from 'node:events'
2+
3+
const eventEmitter = new EventEmitter()
4+
5+
export {
6+
EventEmitter,
7+
eventEmitter,
8+
}
9+
10+
export default eventEmitter

electron/helpers/single.js

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { app, BrowserWindow } from 'electron'
2+
3+
/**
4+
* 确保 Electron 应用只运行一个实例的工具函数
5+
* @typedef {Object} SingleInstanceOptions
6+
* @property {Function} [onSecondInstance] - 当第二个实例启动时的回调函数
7+
* @property {boolean} [enableSandbox=false] - 是否启用沙箱模式
8+
* @property {Function} [onSuccess] - 成功获取单例锁后的回调函数
9+
* @property {Function} [onShowWindow] - 主窗口已展示回调
10+
* @property {boolean} [forceFocus=true] - 是否强制聚焦已存在的窗口
11+
* @property {boolean} [silentMode=false] - 静默模式,不显示任何提示
12+
* @property {Function} [onError] - 错误处理回调函数
13+
*/
14+
15+
/**
16+
* 第二个实例启动时的回调函数类型
17+
* @callback OnSecondInstanceCallback
18+
* @param {Event} event - Electron 事件对象
19+
* @param {string[]} commandLine - 命令行参数数组
20+
* @param {string} workingDirectory - 工作目录
21+
* @param {BrowserWindow|null} mainWindow - 主窗口实例,如果存在的话
22+
*/
23+
24+
/**
25+
* 确保应用程序只运行单个实例
26+
* @param {SingleInstanceOptions} options - 配置选项
27+
* @returns {boolean} 是否成功获取单例锁
28+
*
29+
* @example
30+
* // 基础使用
31+
* ensureSingleInstance({
32+
* onSuccess: () => {
33+
* app.whenReady().then(createWindow)
34+
* }
35+
* });
36+
*
37+
* @example
38+
* // 高级使用
39+
* ensureSingleInstance({
40+
* onSecondInstance: (event, commandLine, workingDirectory, mainWindow) => {
41+
* if (mainWindow) {
42+
* mainWindow.webContents.send('new-instance-launched', commandLine);
43+
* }
44+
* },
45+
* onSuccess: () => {
46+
* console.log('Successfully acquired lock');
47+
* createWindow();
48+
* },
49+
* onError: (error) => {
50+
* console.error('Error in single instance check:', error);
51+
* },
52+
* forceFocus: true,
53+
* silentMode: false
54+
* });
55+
*
56+
* @throws {Error} 如果在非 Electron 环境中调用
57+
*/
58+
function ensureSingleInstance(options = {}) {
59+
// 参数解构与默认值设置
60+
const {
61+
onSecondInstance,
62+
enableSandbox = false,
63+
onSuccess,
64+
onShowWindow,
65+
onError,
66+
forceFocus = true,
67+
silentMode = false,
68+
} = options
69+
70+
// 验证运行环境
71+
if (!app || !BrowserWindow) {
72+
const error = new Error('ensureSingleInstance must be called in Electron environment')
73+
if (onError) {
74+
onError(error)
75+
return false
76+
}
77+
throw error
78+
}
79+
80+
try {
81+
// 沙箱模式检查
82+
if (enableSandbox) {
83+
!silentMode && console.log('Sandbox mode enabled, skipping single instance check')
84+
onSuccess?.()
85+
return true
86+
}
87+
88+
// 请求单例锁
89+
const gotTheLock = app.requestSingleInstanceLock()
90+
91+
// 如果无法获取锁,说明已有实例在运行
92+
if (!gotTheLock) {
93+
!silentMode && console.log('Application instance already running, quitting...')
94+
app.quit()
95+
return false
96+
}
97+
98+
// 监听第二个实例的启动
99+
app.on('second-instance', (event, commandLine, workingDirectory) => {
100+
try {
101+
// 获取所有窗口
102+
const windows = BrowserWindow.getAllWindows()
103+
const mainWindow = windows.length ? windows[0] : null
104+
105+
// 处理窗口焦点
106+
onShowWindow?.(mainWindow, commandLine)
107+
108+
if (mainWindow) {
109+
if (mainWindow.isMinimized() || !mainWindow.isVisible()) {
110+
mainWindow.show()
111+
}
112+
if (forceFocus) {
113+
mainWindow.focus()
114+
}
115+
}
116+
117+
// 调用用户自定义的回调
118+
onSecondInstance?.(event, commandLine, workingDirectory, mainWindow)
119+
}
120+
catch (error) {
121+
!silentMode && console.error('Error handling second instance:', error)
122+
onError?.(error)
123+
}
124+
})
125+
126+
// 调用成功回调
127+
onSuccess?.()
128+
return true
129+
}
130+
catch (error) {
131+
!silentMode && console.error('Error in ensureSingleInstance:', error)
132+
onError?.(error)
133+
return false
134+
}
135+
}
136+
137+
/**
138+
* 检查当前是否为应用程序的主实例
139+
* @returns {boolean} 如果是主实例返回 true,否则返回 false
140+
*/
141+
function isMainInstance() {
142+
return app.requestSingleInstanceLock()
143+
}
144+
145+
/**
146+
* 释放单例锁,允许其他实例启动
147+
* @returns {void}
148+
*/
149+
function releaseSingleInstanceLock() {
150+
app.releaseSingleInstanceLock()
151+
}
152+
153+
export {
154+
ensureSingleInstance,
155+
isMainInstance,
156+
releaseSingleInstanceLock,
157+
}

electron/ipc/tray/index.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
1+
import { app, dialog, Menu, Tray } from 'electron'
12
import { trayPath } from '$electron/configs/index.js'
23
import { executeI18n } from '$electron/helpers/index.js'
34
import appStore from '$electron/helpers/store.js'
4-
import { app, dialog, Menu, Tray } from 'electron'
5+
import { eventEmitter } from '$electron/helpers/emitter.js'
56

67
export default (mainWindow) => {
78
const t = value => executeI18n(mainWindow, value)
89

910
let tray = null
1011

12+
eventEmitter.on('tray:destroy', () => {
13+
tray?.destroy?.()
14+
})
15+
1116
const showApp = () => {
1217
if (process.platform === 'darwin') {
1318
app.dock.show()

electron/main.js

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ import { loadPage } from './helpers/index.js'
2222

2323
import { Edger } from './helpers/edger/index.js'
2424

25+
import { ensureSingleInstance } from './helpers/single.js'
26+
import { eventEmitter } from './helpers/emitter.js'
27+
2528
const require = createRequire(import.meta.url)
2629
const __dirname = path.dirname(fileURLToPath(import.meta.url))
2730

@@ -122,27 +125,39 @@ function createWindow() {
122125
control(mainWindow)
123126
}
124127

125-
app.whenReady().then(() => {
126-
electronApp.setAppUserModelId('com.viarotel.escrcpy')
127-
128-
app.on('browser-window-created', (_, window) => {
129-
optimizer.watchWindowShortcuts(window)
128+
function onWhenReady() {
129+
app.whenReady().then(() => {
130+
electronApp.setAppUserModelId('com.viarotel.escrcpy')
131+
132+
app.on('browser-window-created', (_, window) => {
133+
optimizer.watchWindowShortcuts(window)
134+
})
135+
136+
createWindow()
137+
138+
// macOS 中应用被激活
139+
app.on('activate', () => {
140+
if (BrowserWindow.getAllWindows().length === 0) {
141+
createWindow()
142+
return
143+
}
144+
145+
app.dock.show()
146+
mainWindow.show()
147+
})
130148
})
149+
}
131150

132-
createWindow()
133-
134-
// macOS 中应用被激活
135-
app.on('activate', () => {
136-
if (BrowserWindow.getAllWindows().length === 0) {
137-
createWindow()
138-
return
139-
}
140-
141-
app.dock.show()
142-
mainWindow.show()
143-
})
151+
ensureSingleInstance({
152+
onSuccess() {
153+
onWhenReady()
154+
},
155+
onShowWindow() {
156+
eventEmitter.emit('tray:destroy')
157+
}
144158
})
145159

160+
146161
app.on('window-all-closed', () => {
147162
app.isQuiting = true
148163
app.quit()

0 commit comments

Comments
 (0)