UNPKG

@lynker-desktop/electron-ipc

Version:

electron-ipc

383 lines (380 loc) 15.8 kB
import { EventEmitter } from 'events'; import { webContents, ipcMain } from 'electron'; import { getRandomUUID } from '../common/index.js'; let isInitialized = false; const TIMEOUT = 1000 * 60 * 30; // 30分钟超时 /** * 主进程 IPC 通信类 * 负责处理主进程与渲染进程之间的消息通信 * 使用单例模式确保全局唯一实例 * * 修复说明: * - 为每个请求生成唯一的 requestId,解决并发请求数据错乱问题 * - 确保请求和响应能够正确匹配,避免多个并发请求互相干扰 */ class MainIPC { constructor() { this.eventEmitter = new EventEmitter(); if (!MainIPC.instance) { this.eventEmitter = new EventEmitter(); MainIPC.instance = this; } return MainIPC.instance; } /** * 发送给主进程消息 * 使用唯一的 requestId 确保并发请求不会互相干扰 * * @param channel 消息通道名称 * @param args 传递给处理器的参数 * @returns Promise<any> 返回处理结果 * * 修复说明: * - 为每个请求生成唯一的 requestId * - 监听 `${channel}-reply-${requestId}` 事件,确保只接收对应请求的回复 * - 发送请求时包含 requestId,让处理器知道如何回复 */ async invokeMain(channel, ...args) { return new Promise((resolve, reject) => { const requestId = getRandomUUID(); const replyChannel = `${channel}-reply-${requestId}`; let isResolved = false; const replyHandler = (result) => { if (!isResolved) { isResolved = true; this.eventEmitter.removeListener(replyChannel, replyHandler); clearTimeout(timeoutId); resolve(result); } }; this.eventEmitter.once(replyChannel, replyHandler); // 添加超时机制,确保监听器最终会被清理 const timeoutId = setTimeout(() => { if (!isResolved) { isResolved = true; this.eventEmitter.removeListener(replyChannel, replyHandler); reject(new Error(`IPC request timeout: ${channel} (30s)`)); } }, TIMEOUT); // 30秒超时 this.eventEmitter.emit(channel, requestId, ...args); }); } /** * 处理主进程发送过来的消息 * 持续监听指定通道的消息 * * @param channel 消息通道名称 * @param handler 处理函数,接收除 requestId 外的所有参数 * * 修复说明: * - 接收 requestId 作为第一个参数 * - 使用 `${channel}-reply-${requestId}` 发送回复,确保回复给正确的请求 * - 支持多个并发请求,每个请求都有独立的回复通道 */ handleMain(channel, handler) { this.eventEmitter.on(channel, async (requestId, ...args) => { const result = await handler(...args); this.eventEmitter.emit(`${channel}-reply-${requestId}`, result); }); return { cancel: () => { this.eventEmitter.removeAllListeners(channel); } }; } /** * 发送给渲染进程消息 * 使用唯一的 requestId 确保并发请求不会互相干扰 * * @param webContents 目标渲染进程的 WebContents 对象 * @param channel 消息通道名称 * @param args 传递给渲染进程的参数 * @returns Promise<any> 返回渲染进程的处理结果 * * 修复说明: * - 为每个请求生成唯一的 requestId * - 监听 `${channel}-reply-${requestId}` 事件,确保只接收对应请求的回复 * - 发送请求时包含 requestId,让渲染进程知道如何回复 * - 等待渲染进程加载完成后再发送消息,确保消息能够被正确接收 */ invokeRenderer(webContents, channel, ...args) { return new Promise((resolve, reject) => { const requestId = getRandomUUID(); const replyChannel = `${channel}-reply-${requestId}`; let isResolved = false; const sendMessage = () => { // 检查 webContents 是否已销毁 if (webContents.isDestroyed()) { reject(new Error('WebContents has been destroyed')); return; } const replyHandler = (_event, result) => { if (!isResolved) { isResolved = true; ipcMain.removeListener(replyChannel, replyHandler); clearTimeout(timeoutId); resolve(result); } }; ipcMain.once(replyChannel, replyHandler); // 添加超时机制,确保监听器最终会被清理 const timeoutId = setTimeout(() => { if (!isResolved) { isResolved = true; ipcMain.removeListener(replyChannel, replyHandler); reject(new Error(`IPC request timeout: ${channel} (30s)`)); } }, TIMEOUT); // 30秒超时 webContents.send(channel, requestId, ...args); }; // 等待渲染进程加载完成后再推送,否则会导致渲染进程收不到消息 if (webContents.isLoading()) { webContents.once('did-finish-load', () => { setTimeout(() => sendMessage()); }); } else { sendMessage(); } }); } /** * 发送给所有渲染进程消息 * 向所有渲染进程发送消息并收集所有响应 * * @param channel 消息通道名称 * @param args 传递给所有渲染进程的参数 * @returns Promise<any[]> 返回所有渲染进程的处理结果数组 * * 修复说明: * - 为每个请求生成唯一的 requestId * - 收集所有渲染进程的响应,当所有响应都收到时才 resolve * - 使用 `${channel}-reply-${requestId}` 确保只接收对应请求的回复 * - 如果没有渲染进程,直接返回空数组 * - 等待每个渲染进程加载完成后再发送消息 */ invokeAllRenderer(channel, ...args) { return new Promise(resolve => { const requestId = getRandomUUID(); let responseCount = 0; const totalWebContents = webContents.getAllWebContents().length; if (totalWebContents === 0) { resolve([]); return; } const responses = []; const replyChannel = `${channel}-reply-${requestId}`; // 只注册一个监听器,避免内存泄漏 // 使用 on 而不是 once,因为需要接收多个响应 const replyHandler = (_event, result) => { responses.push(result); responseCount++; if (responseCount === totalWebContents) { // 清理监听器 ipcMain.removeListener(replyChannel, replyHandler); clearTimeout(timeoutId); resolve(responses); } }; ipcMain.on(replyChannel, replyHandler); // 添加超时机制,确保监听器最终会被清理 const timeoutId = setTimeout(() => { ipcMain.removeListener(replyChannel, replyHandler); if (responseCount < totalWebContents) { // 超时后返回已收到的响应 resolve(responses); } }, TIMEOUT); // 30秒超时 webContents.getAllWebContents().forEach(webContent => { const sendMessage = () => { webContent.send(channel, requestId, ...args); }; // 等待渲染进程加载完成后再推送,否则会导致渲染进程收不到消息 if (webContent.isLoading()) { webContent.once('did-finish-load', () => { setTimeout(() => sendMessage()); }); } else { sendMessage(); } }); }); } /** * 处理渲染进程发送过来的消息 * 持续监听指定通道的消息,支持超时处理 * * @param channel 消息通道名称 * @param handler 处理函数,接收除 requestId 外的所有参数 * * 修复说明: * - 接收 requestId 作为第一个参数 * - 使用 ipcMain.handle 替代 ipcMain.on,提供更好的错误处理 * - 支持 8 秒超时机制,避免长时间等待 * - 支持并发请求,每个请求都有独立的处理流程 * - 提供详细的错误日志记录 */ handleRenderer(channel, handler) { try { ipcMain.handle(channel, async (event, ...args) => { try { const timeout = () => { return new Promise((resolve, reject) => { setTimeout(() => { reject(new Error('请求超时')); }, TIMEOUT); }); }; const processing = () => { return new Promise(async (resolve, reject) => { try { const data = await handler(...args); resolve(data); } catch (error) { reject(error); } }); }; // 这里是实际的异步操作 const result = await Promise.race([processing(), timeout()]); // console.error(`[${channel}] handleRenderer result: `, result) return result; // 返回正常结果 } catch (error) { log('error', 'SDK handleRenderer error: ', `channel: ${channel}`, error); // @ts-ignore return error?.message; // 返回超时错误信息 } }); } catch (error) { log('error', 'SDK handleRenderer error: ', error); } return { cancel: () => { ipcMain.removeHandler(channel); } }; } /** * 初始化消息中继功能 * 设置消息转发和回复机制,支持跨渲染进程通信 * * 功能说明: * - relay-message: 转发消息到指定的渲染进程或所有渲染进程 * - relay-reply: 处理回复消息,广播给所有渲染进程 * - __GetCurrentWebContentId__: 获取当前 WebContent ID * - __OpenCurrentWebContentDevTools__: 打开当前 WebContent 的开发者工具 */ relayMessage() { // 防止重复注册监听器:先移除再添加 const relayMessageHandler = (_event, { targetWebContentId, channel, requestId, args }) => { log('log', 'relay-message', { targetWebContentId, channel, requestId, args }); if (targetWebContentId) { // 转发到指定的渲染进程 const targetWebContent = webContents.fromId(targetWebContentId); if (targetWebContent) { targetWebContent.send(channel, requestId, ...args); } } else { // 广播到所有渲染进程 webContents.getAllWebContents().forEach(webContent => { webContent.send(channel, requestId, ...args); }); } }; ipcMain.removeListener('relay-message', relayMessageHandler); ipcMain.on('relay-message', relayMessageHandler); // 处理回复消息广播 const relayReplyHandler = (_event, { originalChannel, requestId, result }) => { log('log', 'relay-reply', { originalChannel, requestId, result }); // 使用 requestId 确保回复发送给正确的请求 // 所有渲染进程都会接收 ${originalChannel}-reply-${requestId} 事件 // 只有对应的请求才会处理该回复,避免数据错乱 webContents.getAllWebContents().forEach(webContent => { webContent.send(`${originalChannel}-reply-${requestId}`, result); }); }; ipcMain.removeListener('relay-reply', relayReplyHandler); ipcMain.on('relay-reply', relayReplyHandler); // 获取当前 WebContent ID 的通道 const getCurrentWebContentIdChannel = '__GetCurrentWebContentId__'; const getCurrentWebContentIdHandler = (_event) => { try { _event.frameId; const webContentId = _event?.sender?.id; _event.sender.send(`${getCurrentWebContentIdChannel}-reply`, webContentId); } catch (error) { log('error', 'getCurrentWebContentId error:', error); } }; ipcMain.removeListener(getCurrentWebContentIdChannel, getCurrentWebContentIdHandler); ipcMain.on(getCurrentWebContentIdChannel, getCurrentWebContentIdHandler); // 打开当前 WebContent 开发者工具的通道 const openCurrentWebContentDevToolsChannel = '__OpenCurrentWebContentDevTools__'; const openCurrentWebContentDevToolsHandler = (_event) => { try { _event.frameId; const webContentId = _event?.sender?.id; webContents.fromId(webContentId)?.openDevTools(); _event.sender.send(`${openCurrentWebContentDevToolsChannel}-reply`, webContentId); } catch (error) { log('error', 'openCurrentWebContentDevTools error:', error); } }; ipcMain.removeListener(openCurrentWebContentDevToolsChannel, openCurrentWebContentDevToolsHandler); ipcMain.on(openCurrentWebContentDevToolsChannel, openCurrentWebContentDevToolsHandler); } } /** * 全局 MainIPC 实例 * 使用全局变量确保单例模式,避免重复创建实例 */ // @ts-ignore const mainIPC = global['__ELECTRON_IPC__'] ? global['__ELECTRON_IPC__'] : (global['__ELECTRON_IPC__'] = new MainIPC()); /** * 初始化 IPC 通信系统 * 设置消息中继功能,确保跨进程通信正常工作 * * @returns MainIPC 实例 * * 功能说明: * - 检查是否已经初始化,避免重复初始化 * - 设置消息中继功能,支持跨渲染进程通信 * - 返回全局 MainIPC 实例 */ const initialize = () => { // @ts-ignore if (isInitialized && global['__ELECTRON_IPC__']) { // @ts-ignore return global['__ELECTRON_IPC__']; } isInitialized = true; // @ts-ignore global['__ELECTRON_IPC__'].relayMessage(); // @ts-ignore return global['__ELECTRON_IPC__']; }; /** * 日志记录工具函数 * 提供统一的日志格式和错误处理 * * @param type 日志类型:'log' 或 'error' * @param data 要记录的数据 */ const log = (type, ...data) => { const key = `[electron-ipc]: `; try { console[type](key, ...data); } catch (error) { console.error(key, error); } }; export { initialize, isInitialized, mainIPC }; //# sourceMappingURL=index.js.map