@lynker-desktop/electron-ipc
Version:
electron-ipc
383 lines (380 loc) • 15.8 kB
JavaScript
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