@easy-electron/core 1.0.1-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/index.cjs +799 -0
- package/dist/index.d.cts +528 -0
- package/dist/index.d.mts +528 -0
- package/dist/index.d.ts +528 -0
- package/dist/index.mjs +787 -0
- package/package.json +48 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
import { BrowserWindow, ipcMain, app } from 'electron';
|
|
2
|
+
import { Logger } from '@easy-electron/logger';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { EEUtil, ConfigUtil, ipcRegistry } from '@easy-electron/utils';
|
|
5
|
+
import { randomUUID } from 'node:crypto';
|
|
6
|
+
|
|
7
|
+
class EasyElectronError extends Error {
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "EasyElectronError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class IpcError extends EasyElectronError {
|
|
15
|
+
/** 错误码 */
|
|
16
|
+
code;
|
|
17
|
+
constructor(code, message) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = "IpcError";
|
|
20
|
+
this.code = code;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 将任意错误包装为 IpcError
|
|
24
|
+
* @param error 原始错误
|
|
25
|
+
* @returns IpcError 实例
|
|
26
|
+
*/
|
|
27
|
+
static from(error) {
|
|
28
|
+
if (error instanceof IpcError) return error;
|
|
29
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
30
|
+
return new IpcError("IPC_UNKNOWN" /* UNKNOWN */, message);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const WindowEventTypes = ["created", "closed", "focus", "blur", "ready"];
|
|
35
|
+
const DEFAULT_MAIN_WINDOW_ID = "main";
|
|
36
|
+
const DEFAULT_APP_CONFIG = {
|
|
37
|
+
app: {
|
|
38
|
+
name: "Electron App",
|
|
39
|
+
singleInstance: true
|
|
40
|
+
},
|
|
41
|
+
window: {
|
|
42
|
+
width: 800,
|
|
43
|
+
height: 600,
|
|
44
|
+
webPreferences: {
|
|
45
|
+
preload: EEUtil.getDefaultPreloadPath()
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
dev: {
|
|
49
|
+
openDevTools: "right"
|
|
50
|
+
},
|
|
51
|
+
prod: {
|
|
52
|
+
htmlPath: "dist/index.html"
|
|
53
|
+
},
|
|
54
|
+
plugins: []
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
class WindowManager {
|
|
58
|
+
windows;
|
|
59
|
+
defaultConfig;
|
|
60
|
+
eventListeners;
|
|
61
|
+
logger = Logger.scope("WindowManager");
|
|
62
|
+
/** 开发模式下的页面 URL(可选,替代默认的 VITE_DEV_SERVER_URL 检测) */
|
|
63
|
+
devUrl;
|
|
64
|
+
/** 生产模式下的 HTML 路径(可选) */
|
|
65
|
+
prodHtmlPath;
|
|
66
|
+
constructor(defaultConfig, devUrl, prodHtmlPath) {
|
|
67
|
+
this.windows = /* @__PURE__ */ new Map();
|
|
68
|
+
this.defaultConfig = defaultConfig || {};
|
|
69
|
+
this.eventListeners = /* @__PURE__ */ new Map();
|
|
70
|
+
this.devUrl = devUrl;
|
|
71
|
+
this.prodHtmlPath = prodHtmlPath;
|
|
72
|
+
WindowEventTypes.forEach((type) => {
|
|
73
|
+
this.eventListeners.set(type, /* @__PURE__ */ new Set());
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* 创建窗口
|
|
78
|
+
* @param config 窗口配置
|
|
79
|
+
* @returns 窗口实例
|
|
80
|
+
*/
|
|
81
|
+
async create(config) {
|
|
82
|
+
const mergedConfig = ConfigUtil.deepMergeConfig(this.defaultConfig, config || {});
|
|
83
|
+
const windowId = this.windows.size === 0 ? DEFAULT_MAIN_WINDOW_ID : mergedConfig.id || randomUUID();
|
|
84
|
+
if (this.windows.has(windowId)) {
|
|
85
|
+
const error = new EasyElectronError(`Window with id '${windowId}' already exists`);
|
|
86
|
+
this.logger.error(error.message);
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
this.logger.info(`Creating window with id '${windowId}'`);
|
|
90
|
+
const { id: _id, url: _url, htmlPath: _htmlPath, raw, ...nativeOptions } = mergedConfig;
|
|
91
|
+
const windowOptions = ConfigUtil.deepMergeConfig(
|
|
92
|
+
nativeOptions,
|
|
93
|
+
raw || {}
|
|
94
|
+
);
|
|
95
|
+
const browserWindow = new BrowserWindow(windowOptions);
|
|
96
|
+
const instance = {
|
|
97
|
+
id: windowId,
|
|
98
|
+
window: browserWindow,
|
|
99
|
+
config: mergedConfig
|
|
100
|
+
};
|
|
101
|
+
this.windows.set(windowId, instance);
|
|
102
|
+
this.setupWindowListeners(instance);
|
|
103
|
+
try {
|
|
104
|
+
await this.loadContent(browserWindow, mergedConfig);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
this.logger.error(`\u7A97\u53E3\u52A0\u8F7D\u5931\u8D25 [${windowId}]:`, error instanceof Error ? error.message : "");
|
|
107
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
108
|
+
browserWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(this.buildErrorPage(windowId, errMsg))}`);
|
|
109
|
+
}
|
|
110
|
+
this.emit("created", instance);
|
|
111
|
+
return instance;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* 加载窗口内容
|
|
115
|
+
*/
|
|
116
|
+
async loadContent(window, config) {
|
|
117
|
+
if (config.url) {
|
|
118
|
+
await window.loadURL(config.url);
|
|
119
|
+
} else if (config.htmlPath) {
|
|
120
|
+
await window.loadFile(config.htmlPath);
|
|
121
|
+
} else if (this.devUrl) {
|
|
122
|
+
await window.loadURL(this.devUrl);
|
|
123
|
+
} else {
|
|
124
|
+
const htmlPath = this.prodHtmlPath || path.join(EEUtil.getAppRoot(), "dist", "index.html");
|
|
125
|
+
await window.loadFile(htmlPath);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* 设置窗口事件监听器
|
|
130
|
+
*/
|
|
131
|
+
setupWindowListeners(instance) {
|
|
132
|
+
const { window } = instance;
|
|
133
|
+
window.once("ready-to-show", () => {
|
|
134
|
+
this.emit("ready", instance);
|
|
135
|
+
});
|
|
136
|
+
window.on("focus", () => {
|
|
137
|
+
this.emit("focus", instance);
|
|
138
|
+
});
|
|
139
|
+
window.on("blur", () => {
|
|
140
|
+
this.emit("blur", instance);
|
|
141
|
+
});
|
|
142
|
+
window.on("close", () => {
|
|
143
|
+
this.emit("closed", instance);
|
|
144
|
+
this.windows.delete(instance.id);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* 获取窗口实例
|
|
149
|
+
* @param id 窗口 ID
|
|
150
|
+
* @returns 窗口实例或 undefined
|
|
151
|
+
*/
|
|
152
|
+
get(id) {
|
|
153
|
+
return this.windows.get(id);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* 获取所有窗口实例
|
|
157
|
+
* @returns 窗口实例数组
|
|
158
|
+
*/
|
|
159
|
+
getAll() {
|
|
160
|
+
return Array.from(this.windows.values());
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* 检查窗口是否存在
|
|
164
|
+
* @param id 窗口 ID
|
|
165
|
+
* @returns 是否存在
|
|
166
|
+
*/
|
|
167
|
+
has(id) {
|
|
168
|
+
return this.windows.has(id);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* 获取窗口数量
|
|
172
|
+
* @returns 窗口数量
|
|
173
|
+
*/
|
|
174
|
+
count() {
|
|
175
|
+
return this.windows.size;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* 关闭窗口
|
|
179
|
+
* @param id 窗口 ID
|
|
180
|
+
*/
|
|
181
|
+
close(id) {
|
|
182
|
+
const instance = this.windows.get(id);
|
|
183
|
+
if (!instance) {
|
|
184
|
+
const error = new EasyElectronError(`Window with id '${id}' not found`);
|
|
185
|
+
this.logger.error(error.message);
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
this.logger.info(`Closing window with id '${id}'`);
|
|
189
|
+
if (!instance.window.isDestroyed()) {
|
|
190
|
+
instance.window.close();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* 关闭所有窗口
|
|
195
|
+
*/
|
|
196
|
+
closeAll() {
|
|
197
|
+
const windowIds = Array.from(this.windows.keys());
|
|
198
|
+
windowIds.forEach((id) => {
|
|
199
|
+
try {
|
|
200
|
+
this.close(id);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
console.error(`Error closing window '${id}':`, error);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* 注册窗口事件监听器
|
|
208
|
+
* @param event 事件类型
|
|
209
|
+
* @param listener 监听器函数
|
|
210
|
+
*/
|
|
211
|
+
on(event, listener) {
|
|
212
|
+
const listeners = this.eventListeners.get(event);
|
|
213
|
+
if (listeners) {
|
|
214
|
+
listeners.add(listener);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* 移除窗口事件监听器
|
|
219
|
+
* @param event 事件类型
|
|
220
|
+
* @param listener 监听器函数
|
|
221
|
+
*/
|
|
222
|
+
off(event, listener) {
|
|
223
|
+
const listeners = this.eventListeners.get(event);
|
|
224
|
+
if (listeners) {
|
|
225
|
+
listeners.delete(listener);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* 触发窗口事件
|
|
230
|
+
* @param event 事件类型
|
|
231
|
+
* @param instance 窗口实例
|
|
232
|
+
*/
|
|
233
|
+
emit(event, instance) {
|
|
234
|
+
const listeners = this.eventListeners.get(event);
|
|
235
|
+
if (listeners) {
|
|
236
|
+
listeners.forEach((listener) => {
|
|
237
|
+
try {
|
|
238
|
+
listener(instance);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
console.error(`Error in window event listener for '${event}':`, error);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* 获取默认配置
|
|
247
|
+
*/
|
|
248
|
+
getDefaultConfig() {
|
|
249
|
+
return ConfigUtil.deepMergeConfig({}, this.defaultConfig);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* 构建降级错误页面(当窗口内容加载失败时显示)
|
|
253
|
+
* @param windowId 窗口 ID
|
|
254
|
+
* @param errorMessage 错误信息
|
|
255
|
+
* @returns HTML 字符串
|
|
256
|
+
*/
|
|
257
|
+
buildErrorPage(windowId, errorMessage) {
|
|
258
|
+
return `<!DOCTYPE html>
|
|
259
|
+
<html lang="zh-CN">
|
|
260
|
+
<head>
|
|
261
|
+
<meta charset="UTF-8">
|
|
262
|
+
<style>
|
|
263
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
264
|
+
body {
|
|
265
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
266
|
+
display: flex; justify-content: center; align-items: center;
|
|
267
|
+
min-height: 100vh; background: #1a1a2e; color: #e0e0e0;
|
|
268
|
+
}
|
|
269
|
+
.card {
|
|
270
|
+
background: #16213e; border-radius: 12px; padding: 40px;
|
|
271
|
+
max-width: 520px; text-align: center;
|
|
272
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
|
|
273
|
+
}
|
|
274
|
+
h1 { font-size: 20px; color: #e94560; margin-bottom: 16px; }
|
|
275
|
+
.detail {
|
|
276
|
+
background: #0f3460; border-radius: 8px; padding: 16px;
|
|
277
|
+
font-size: 13px; color: #a0a0b0; text-align: left;
|
|
278
|
+
word-break: break-all; margin: 16px 0;
|
|
279
|
+
}
|
|
280
|
+
.hint { font-size: 12px; color: #666; }
|
|
281
|
+
.hint code { background: #0f3460; padding: 2px 6px; border-radius: 4px; }
|
|
282
|
+
</style>
|
|
283
|
+
</head>
|
|
284
|
+
<body>
|
|
285
|
+
<div class="card">
|
|
286
|
+
<h1>\u26A0 \u7A97\u53E3\u52A0\u8F7D\u5931\u8D25</h1>
|
|
287
|
+
<p>\u7A97\u53E3 <strong>${windowId}</strong> \u7684\u5185\u5BB9\u672A\u80FD\u6210\u529F\u52A0\u8F7D\u3002</p>
|
|
288
|
+
<div class="detail">${errorMessage}</div>
|
|
289
|
+
<p class="hint">
|
|
290
|
+
\u8BF7\u68C0\u67E5 <code>htmlPath</code> \u6216 <code>url</code> \u914D\u7F6E\uFF0C\u6216\u786E\u4FDD\u5F00\u53D1\u670D\u52A1\u5668\u5DF2\u542F\u52A8\u3002
|
|
291
|
+
</p>
|
|
292
|
+
</div>
|
|
293
|
+
</body>
|
|
294
|
+
</html>`;
|
|
295
|
+
}
|
|
296
|
+
clear() {
|
|
297
|
+
this.windows.clear();
|
|
298
|
+
this.eventListeners.clear();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const IPC_MODULE_KEY = Symbol.for("easy-electron:ipc-module");
|
|
303
|
+
class IpcManager {
|
|
304
|
+
handlers;
|
|
305
|
+
windowManager;
|
|
306
|
+
logger = Logger.scope("IpcManager");
|
|
307
|
+
constructor(windowManager) {
|
|
308
|
+
this.handlers = /* @__PURE__ */ new Map();
|
|
309
|
+
this.windowManager = windowManager;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* 注册 IPC 处理器
|
|
313
|
+
* @param channel IPC 通道名称
|
|
314
|
+
* @param handler 处理器函数
|
|
315
|
+
* @param options 处理器选项
|
|
316
|
+
*/
|
|
317
|
+
handle(channel, handler, options) {
|
|
318
|
+
if (this.handlers.has(channel)) {
|
|
319
|
+
const error = new EasyElectronError(`IPC handler for channel '${channel}' already exists`);
|
|
320
|
+
this.logger.error(error.message);
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
this.logger.info(`Registering IPC handler for channel '${channel}'`);
|
|
324
|
+
this.handlers.set(channel, handler);
|
|
325
|
+
if (options?.sync) {
|
|
326
|
+
ipcMain.on(channel, (event, ...args) => {
|
|
327
|
+
try {
|
|
328
|
+
const result = handler(event, ...args);
|
|
329
|
+
event.returnValue = result;
|
|
330
|
+
} catch (error) {
|
|
331
|
+
console.error(`Error in sync IPC handler for '${channel}':`, error);
|
|
332
|
+
event.returnValue = { error: error instanceof Error ? error.message : "Unknown error" };
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
} else {
|
|
336
|
+
ipcMain.handle(channel, async (event, ...args) => {
|
|
337
|
+
try {
|
|
338
|
+
return await handler(event, ...args);
|
|
339
|
+
} catch (error) {
|
|
340
|
+
console.error(`Error in async IPC handler for '${channel}':`, error);
|
|
341
|
+
throw error;
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* 批量注册 defineIpc 返回的处理器映射
|
|
348
|
+
*
|
|
349
|
+
* 自动识别函数形式(handle)和对象形式({type, handler}),
|
|
350
|
+
* 根据 moduleName 前缀拼接 IPC 通道名。
|
|
351
|
+
*
|
|
352
|
+
* @param handlers defineIpc 返回的处理器映射对象
|
|
353
|
+
* @param moduleName 可选的模块名称(用于拼接通道前缀,如 `module:method`)
|
|
354
|
+
*
|
|
355
|
+
* @example
|
|
356
|
+
* ```ts
|
|
357
|
+
* import ipcHandlers from './electron/ipc/handlers'
|
|
358
|
+
* app.ipc.registerHandlers(ipcHandlers)
|
|
359
|
+
* ```
|
|
360
|
+
*/
|
|
361
|
+
registerHandlers(handlers, moduleName) {
|
|
362
|
+
if (!moduleName) {
|
|
363
|
+
moduleName = handlers[IPC_MODULE_KEY];
|
|
364
|
+
}
|
|
365
|
+
for (const [name, handler] of Object.entries(handlers)) {
|
|
366
|
+
if (typeof name === "string" && name.startsWith("__")) continue;
|
|
367
|
+
const channel = moduleName ? `${moduleName}:${name}` : name;
|
|
368
|
+
if (typeof handler === "object" && handler !== null && "handler" in handler) {
|
|
369
|
+
const config = handler;
|
|
370
|
+
const type = config.type ?? "handle";
|
|
371
|
+
const handlerFn = config.handler;
|
|
372
|
+
if (type === "on" || type === "once") {
|
|
373
|
+
this.logger.info(`\u6CE8\u518C IPC \u5904\u7406\u5668(${type}): ${channel}`);
|
|
374
|
+
const method = type === "once" ? "once" : "on";
|
|
375
|
+
ipcMain[method](channel, (event, ...args) => {
|
|
376
|
+
try {
|
|
377
|
+
handlerFn(event, ...args);
|
|
378
|
+
} catch (error) {
|
|
379
|
+
console.error(`Error in IPC ${type} handler for '${channel}':`, error);
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
} else {
|
|
383
|
+
this.logger.info(`\u6CE8\u518C IPC \u5904\u7406\u5668(${type}): ${channel}`);
|
|
384
|
+
const method = type === "handleOnce" ? "handleOnce" : "handle";
|
|
385
|
+
ipcMain[method](channel, async (event, ...args) => {
|
|
386
|
+
try {
|
|
387
|
+
return await handlerFn(event, ...args);
|
|
388
|
+
} catch (error) {
|
|
389
|
+
console.error(`Error in IPC ${type} handler for '${channel}':`, error);
|
|
390
|
+
throw IpcError.from(error);
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
if (!this.handlers.has(channel)) {
|
|
395
|
+
this.handlers.set(channel, handlerFn);
|
|
396
|
+
}
|
|
397
|
+
} else if (typeof handler === "function") {
|
|
398
|
+
this.logger.info(`\u6CE8\u518C IPC \u5904\u7406\u5668(handle): ${channel}`);
|
|
399
|
+
ipcMain.handle(channel, async (event, ...args) => {
|
|
400
|
+
try {
|
|
401
|
+
return await handler(event, ...args);
|
|
402
|
+
} catch (error) {
|
|
403
|
+
console.error(`Error in IPC handler for '${channel}':`, error);
|
|
404
|
+
throw IpcError.from(error);
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
if (!this.handlers.has(channel)) {
|
|
408
|
+
this.handlers.set(channel, handler);
|
|
409
|
+
}
|
|
410
|
+
} else {
|
|
411
|
+
this.logger.warn(`\u8DF3\u8FC7\u65E0\u6CD5\u8BC6\u522B\u7684\u5904\u7406\u5668: ${channel}`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* 移除 IPC 处理器
|
|
417
|
+
* @param channel IPC 通道名称
|
|
418
|
+
*/
|
|
419
|
+
removeHandler(channel) {
|
|
420
|
+
if (!this.handlers.has(channel)) {
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
this.handlers.delete(channel);
|
|
424
|
+
ipcMain.removeHandler(channel);
|
|
425
|
+
ipcMain.removeAllListeners(channel);
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* 移除所有 IPC 处理器
|
|
429
|
+
*/
|
|
430
|
+
removeAllHandlers() {
|
|
431
|
+
const channels = Array.from(this.handlers.keys());
|
|
432
|
+
channels.forEach((channel) => {
|
|
433
|
+
this.removeHandler(channel);
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* 获取已注册的处理器数量
|
|
438
|
+
*/
|
|
439
|
+
count() {
|
|
440
|
+
return this.handlers.size;
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* 检查处理器是否已注册
|
|
444
|
+
*/
|
|
445
|
+
has(channel) {
|
|
446
|
+
return this.handlers.has(channel);
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* 向主窗口发送消息
|
|
450
|
+
* @param channel IPC 通道名称
|
|
451
|
+
* @param args 消息参数
|
|
452
|
+
*/
|
|
453
|
+
send(channel, ...args) {
|
|
454
|
+
const instance = this.windowManager.get(DEFAULT_MAIN_WINDOW_ID);
|
|
455
|
+
if (!instance) {
|
|
456
|
+
throw new EasyElectronError("Main window not found");
|
|
457
|
+
}
|
|
458
|
+
if (!instance.window.isDestroyed()) {
|
|
459
|
+
instance.window.webContents.send(channel, ...args);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* 向指定窗口发送消息
|
|
464
|
+
* @param windowId 窗口 ID
|
|
465
|
+
* @param channel IPC 通道名称
|
|
466
|
+
* @param args 消息参数
|
|
467
|
+
*/
|
|
468
|
+
sendTo(windowId, channel, ...args) {
|
|
469
|
+
const instance = this.windowManager.get(windowId);
|
|
470
|
+
if (!instance) {
|
|
471
|
+
throw new EasyElectronError(`Window with id '${windowId}' not found`);
|
|
472
|
+
}
|
|
473
|
+
if (!instance.window.isDestroyed()) {
|
|
474
|
+
instance.window.webContents.send(channel, ...args);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* 向所有窗口广播消息
|
|
479
|
+
* @param channel IPC 通道名称
|
|
480
|
+
* @param args 消息参数
|
|
481
|
+
*/
|
|
482
|
+
broadcast(channel, ...args) {
|
|
483
|
+
const instances = this.windowManager.getAll();
|
|
484
|
+
instances.forEach((instance) => {
|
|
485
|
+
if (!instance.window.isDestroyed()) {
|
|
486
|
+
try {
|
|
487
|
+
instance.window.webContents.send(channel, ...args);
|
|
488
|
+
} catch (error) {
|
|
489
|
+
console.error(`Error broadcasting to window '${instance.id}':`, error);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
class LifecycleManager {
|
|
497
|
+
app;
|
|
498
|
+
windowManager;
|
|
499
|
+
readyHooks;
|
|
500
|
+
activateHooks;
|
|
501
|
+
beforeQuitHooks;
|
|
502
|
+
constructor(app, windowManager) {
|
|
503
|
+
this.app = app;
|
|
504
|
+
this.windowManager = windowManager;
|
|
505
|
+
this.readyHooks = /* @__PURE__ */ new Set();
|
|
506
|
+
this.activateHooks = /* @__PURE__ */ new Set();
|
|
507
|
+
this.beforeQuitHooks = /* @__PURE__ */ new Set();
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* 注册 ready 钩子
|
|
511
|
+
* @param hook 钩子函数
|
|
512
|
+
*/
|
|
513
|
+
onReady(hook) {
|
|
514
|
+
this.readyHooks.add(hook);
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* 注册 activate 钩子(macOS)
|
|
518
|
+
* @param hook 钩子函数
|
|
519
|
+
*/
|
|
520
|
+
onActivate(hook) {
|
|
521
|
+
this.activateHooks.add(hook);
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* 注册退出前的清理钩子
|
|
525
|
+
* @param hook 钩子函数
|
|
526
|
+
*/
|
|
527
|
+
onBeforeQuit(hook) {
|
|
528
|
+
this.beforeQuitHooks.add(hook);
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* 初始化生命周期监听
|
|
532
|
+
*/
|
|
533
|
+
init() {
|
|
534
|
+
this.app.whenReady().then(async () => {
|
|
535
|
+
await this.executeHooks(this.readyHooks);
|
|
536
|
+
});
|
|
537
|
+
this.app.on("activate", async () => {
|
|
538
|
+
await this.executeHooks(this.activateHooks);
|
|
539
|
+
});
|
|
540
|
+
this.app.on("will-quit", async (event) => {
|
|
541
|
+
if (this.beforeQuitHooks.size > 0) {
|
|
542
|
+
event.preventDefault();
|
|
543
|
+
try {
|
|
544
|
+
await this.executeHooks(this.beforeQuitHooks);
|
|
545
|
+
} catch (error) {
|
|
546
|
+
console.error("\u9000\u51FA\u524D\u6E05\u7406\u5931\u8D25:", error);
|
|
547
|
+
}
|
|
548
|
+
this.app.exit(0);
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
this.app.on("window-all-closed", () => {
|
|
552
|
+
if (process.platform !== "darwin") {
|
|
553
|
+
this.app.quit();
|
|
554
|
+
this.windowManager.clear();
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* 执行钩子集合
|
|
560
|
+
* @param hooks 钩子函数集合
|
|
561
|
+
*/
|
|
562
|
+
async executeHooks(hooks) {
|
|
563
|
+
for (const hook of hooks) {
|
|
564
|
+
try {
|
|
565
|
+
await hook();
|
|
566
|
+
} catch (error) {
|
|
567
|
+
console.error("\u751F\u547D\u5468\u671F\u94A9\u5B50\u6267\u884C\u5931\u8D25:", error);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
class PluginManager {
|
|
574
|
+
plugins;
|
|
575
|
+
context;
|
|
576
|
+
constructor(context) {
|
|
577
|
+
this.plugins = /* @__PURE__ */ new Map();
|
|
578
|
+
this.context = context;
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* 注册插件
|
|
582
|
+
* @param plugin 插件对象
|
|
583
|
+
*/
|
|
584
|
+
register(plugin) {
|
|
585
|
+
if (!plugin.name) {
|
|
586
|
+
throw new EasyElectronError("Plugin must have a name");
|
|
587
|
+
}
|
|
588
|
+
if (this.plugins.has(plugin.name)) {
|
|
589
|
+
throw new EasyElectronError(`Plugin '${plugin.name}' is already registered`);
|
|
590
|
+
}
|
|
591
|
+
this.plugins.set(plugin.name, plugin);
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* 获取插件
|
|
595
|
+
* @param name 插件名称
|
|
596
|
+
* @returns 插件对象或 undefined
|
|
597
|
+
*/
|
|
598
|
+
get(name) {
|
|
599
|
+
return this.plugins.get(name);
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* 检查插件是否已注册
|
|
603
|
+
* @param name 插件名称
|
|
604
|
+
* @returns 是否已注册
|
|
605
|
+
*/
|
|
606
|
+
has(name) {
|
|
607
|
+
return this.plugins.has(name);
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* 调用插件钩子
|
|
611
|
+
* @param hookName 钩子名称
|
|
612
|
+
*/
|
|
613
|
+
async callHook(hookName) {
|
|
614
|
+
for (const plugin of this.plugins.values()) {
|
|
615
|
+
const hook = plugin[hookName];
|
|
616
|
+
if (typeof hook === "function") {
|
|
617
|
+
try {
|
|
618
|
+
await hook.call(plugin, this.context);
|
|
619
|
+
} catch (error) {
|
|
620
|
+
console.error(`Error in plugin '${plugin.name}' hook '${String(hookName)}':`, error);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* 获取已注册的插件数量
|
|
627
|
+
*/
|
|
628
|
+
count() {
|
|
629
|
+
return this.plugins.size;
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* 获取所有插件
|
|
633
|
+
*/
|
|
634
|
+
getAll() {
|
|
635
|
+
return Array.from(this.plugins.values());
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
class EasyElectron {
|
|
640
|
+
windowManager;
|
|
641
|
+
ipcManager;
|
|
642
|
+
lifecycleManager;
|
|
643
|
+
pluginManager;
|
|
644
|
+
config;
|
|
645
|
+
constructor(config) {
|
|
646
|
+
this.config = ConfigUtil.deepMergeConfig(DEFAULT_APP_CONFIG, config || {});
|
|
647
|
+
this.windowManager = new WindowManager(
|
|
648
|
+
this.config.window,
|
|
649
|
+
this.config.dev?.serverUrl,
|
|
650
|
+
this.config.prod?.htmlPath
|
|
651
|
+
);
|
|
652
|
+
this.ipcManager = new IpcManager(this.windowManager);
|
|
653
|
+
this.lifecycleManager = new LifecycleManager(app, this.windowManager);
|
|
654
|
+
const pluginContext = {
|
|
655
|
+
windows: this.windowManager,
|
|
656
|
+
ipc: this.ipcManager,
|
|
657
|
+
app,
|
|
658
|
+
config: this.config
|
|
659
|
+
};
|
|
660
|
+
this.pluginManager = new PluginManager(pluginContext);
|
|
661
|
+
if (this.config.plugins) {
|
|
662
|
+
this.config.plugins.forEach((plugin) => {
|
|
663
|
+
this.pluginManager.register(plugin);
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* 获取窗口管理器
|
|
669
|
+
*/
|
|
670
|
+
get windows() {
|
|
671
|
+
return this.windowManager;
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* 获取 IPC 管理器
|
|
675
|
+
*/
|
|
676
|
+
get ipc() {
|
|
677
|
+
return this.ipcManager;
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* 初始化应用程序
|
|
681
|
+
*/
|
|
682
|
+
async init() {
|
|
683
|
+
if (this.config.app?.singleInstance) {
|
|
684
|
+
const gotLock = app.requestSingleInstanceLock();
|
|
685
|
+
if (!gotLock) {
|
|
686
|
+
app.quit();
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
app.on("second-instance", () => {
|
|
690
|
+
const mainWindow = this.windowManager.get("main");
|
|
691
|
+
if (mainWindow && !mainWindow.window.isDestroyed()) {
|
|
692
|
+
if (mainWindow.window.isMinimized()) mainWindow.window.restore();
|
|
693
|
+
mainWindow.window.focus();
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
await this.pluginManager.callHook("install");
|
|
698
|
+
const entries = ipcRegistry.drain();
|
|
699
|
+
for (const entry of entries) {
|
|
700
|
+
this.ipcManager.registerHandlers(entry.handlers, entry.moduleName);
|
|
701
|
+
}
|
|
702
|
+
this.lifecycleManager.onActivate(async () => {
|
|
703
|
+
if (this.windowManager.count() === 0) {
|
|
704
|
+
await this.windowManager.create();
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
this.lifecycleManager.onReady(async () => {
|
|
708
|
+
await this.pluginManager.callHook("onReady");
|
|
709
|
+
const instance = await this.windowManager.create();
|
|
710
|
+
if (EEUtil.isDev && this.config.dev?.openDevTools) {
|
|
711
|
+
instance.window.webContents.openDevTools({ mode: this.config.dev?.openDevTools || "right" });
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
this.lifecycleManager.onBeforeQuit(async () => {
|
|
715
|
+
await this.pluginManager.callHook("onBeforeQuit");
|
|
716
|
+
this.ipcManager.removeAllHandlers();
|
|
717
|
+
});
|
|
718
|
+
this.lifecycleManager.init();
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* 注册插件
|
|
722
|
+
*/
|
|
723
|
+
use(plugin) {
|
|
724
|
+
this.pluginManager.register(plugin);
|
|
725
|
+
return this;
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* 注册 ready 钩子
|
|
729
|
+
*/
|
|
730
|
+
onReady(callback) {
|
|
731
|
+
this.lifecycleManager.onReady(callback);
|
|
732
|
+
return this;
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* 注册 activate 钩子
|
|
736
|
+
*/
|
|
737
|
+
onActivate(callback) {
|
|
738
|
+
this.lifecycleManager.onActivate(callback);
|
|
739
|
+
return this;
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* 注册 beforeQuit 钩子
|
|
743
|
+
*/
|
|
744
|
+
onBeforeQuit(callback) {
|
|
745
|
+
this.lifecycleManager.onBeforeQuit(callback);
|
|
746
|
+
return this;
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* 设置 IPC 处理器
|
|
750
|
+
* @param setup IPC 处理器设置函数
|
|
751
|
+
*/
|
|
752
|
+
setupIpc(setup) {
|
|
753
|
+
setup(this.ipcManager);
|
|
754
|
+
return this;
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* 批量注册 IPC 处理器
|
|
758
|
+
*
|
|
759
|
+
* 接收 defineIpc 返回的定义对象,自动识别模块名并注册所有处理器。
|
|
760
|
+
*
|
|
761
|
+
* @param handlers defineIpc 返回的处理器映射
|
|
762
|
+
*
|
|
763
|
+
* @example
|
|
764
|
+
* ```ts
|
|
765
|
+
* import ipcHandlers from './electron/ipc/handlers'
|
|
766
|
+
* app.registerIpc(ipcHandlers)
|
|
767
|
+
* ```
|
|
768
|
+
*/
|
|
769
|
+
registerIpc(handlers) {
|
|
770
|
+
this.ipcManager.registerHandlers(handlers);
|
|
771
|
+
return this;
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* 修改日志实现
|
|
775
|
+
* @param logger 日志实现
|
|
776
|
+
*/
|
|
777
|
+
setLogger(logger) {
|
|
778
|
+
Logger.setImpl(logger);
|
|
779
|
+
return this;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function createElectronApp(config) {
|
|
784
|
+
return new EasyElectron(config);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
export { EasyElectron, EasyElectronError, IpcManager, LifecycleManager, PluginManager, WindowManager, createElectronApp };
|