在这里插入图片描述
个人主页:ujainu

引言

在 Electron 应用架构中,主进程(Main Process) 负责创建窗口、调用系统 API、管理生命周期;而 渲染进程(Renderer Process) 则运行 Web 页面(HTML/CSS/JS),处理用户交互。由于安全与稳定性考虑,两者运行在完全隔离的上下文中,无法直接共享内存或变量。

因此,进程间通信(IPC, Inter-Process Communication) 成为连接二者的核心桥梁。本文将深入剖析如何通过 ipcRenderer.send()ipcMain.on() 实现“页面点击按钮 → 发送消息给主进程 → 主进程回复‘收到’ → 页面显示结果”的完整通信链路,并结合实际代码逐行讲解其工作原理、设计考量与潜在陷阱。

💡 本方案已在 HarmonyOS PC 的 Linux 兼容层成功验证,适用于鸿蒙生态下的桌面应用开发。


一、IPC 通信基础模型

Electron 的 IPC 基于 Chromium 的 Message Channel 机制,采用频道(Channel)命名+事件监听模式:

角色 方法 作用
渲染进程 ipcRenderer.send(channel, data) 向主进程发送异步消息
主进程 ipcMain.on(channel, (event, data) => {...}) 监听指定频道的消息
主进程 event.reply(channel, response) 回复原始发送者(最常用)
主进程 webContents.send(channel, data) 向特定窗口发送消息

关键特性

  • 支持传递任意可序列化 JavaScript 对象(包括嵌套对象、数组、Date 等)
  • 消息传递是异步的,不会阻塞 UI
  • event.reply() 自动绑定到源窗口,避免广播混乱

二、渲染进程代码详解(前端)

2.1 引入 ipcRenderer

const { ipcRenderer } = require('electron');
  • 此行依赖 nodeIntegration: truecontextIsolation: false
  • 在更安全的 preload.js 模式中,应通过 contextBridge.exposeInMainWorld 暴露有限方法。

2.2 发送消息:ipcRenderer.send()

function sendMessage() {
    const message = {
        type: 'hello',
        content: '你好,主进程!',
        count: ++messageCount,
        timestamp: new Date().toLocaleTimeString()
    };
    ipcRenderer.send('renderer-to-main', message);
}

解析

  • 频道名 'renderer-to-main':自定义字符串,需与主进程监听一致。
  • 消息结构设计
    • type:用于主进程路由不同逻辑(类似 RESTful 的 endpoint)
    • count / timestamp:便于调试与追踪
  • 发送时机:通常由用户交互(如按钮点击)触发。

2.3 监听主进程回复:ipcRenderer.on()

ipcRenderer.on('main-to-renderer', (event, response) => {
    document.getElementById('responseText').textContent = response.message;
    document.getElementById('statusText').textContent = '状态:' + response.status;
});

关键点

  • 监听器持久化:一旦注册,将持续接收该频道所有消息,直到页面销毁。
  • 参数解构
    • event:包含发送者信息(如 event.senderFrame
    • response:主进程通过 event.reply() 传回的数据对象
  • UI 更新:直接操作 DOM,体现“响应式反馈”。

⚠️ 注意:不要在循环或高频函数中重复调用 ipcRenderer.on(),否则会注册多个监听器,导致重复执行!


三、主进程代码详解(后端)

3.1 监听消息:ipcMain.on()

ipcMain.on('renderer-to-main', (event, message) => {
    console.log('主进程收到:', message);
    // 处理逻辑...
});

参数说明

  • event 对象包含:
    • event.sender:发送消息的 WebContents 实例
    • event.sender.id:唯一窗口 ID(可用于多窗口管理)
  • message:即渲染进程传入的第二个参数,已自动反序列化。

3.2 消息路由与处理

let responseMessage = '';
if (message.type === 'hello') {
    responseMessage = `收到!你好,渲染进程!(第${message.count}次)`;
} else if (message.type === 'custom') {
    responseMessage = `收到自定义消息!用户ID: ${message.data.userId}`;
}

设计优势

  • 通过 type 字段实现单一入口、多逻辑分支,避免为每种消息创建独立频道。
  • 类似后端 API 的控制器(Controller)模式,易于扩展。

3.3 回复渲染进程:event.reply()

event.reply('main-to-renderer', {
    message: responseMessage,
    status: '已成功接收',
    timestamp: new Date().toLocaleTimeString()
});

为什么推荐 event.reply()

方法 优点 缺点
event.reply(channel, data) 自动定向回源窗口,简洁安全 仅能回复发送者
BrowserWindow.fromId(...).webContents.send(...) 可定向任意窗口 需手动管理窗口 ID,易出错
webContents.sendToAll(...) 广播所有窗口 可能造成信息泄露或性能浪费

最佳实践:除非需要跨窗口通信,否则一律使用 event.reply()


四、通信流程图解

主进程 (Main) 渲染进程 (Renderer) 主进程 (Main) 渲染进程 (Renderer) ipcRenderer.send('renderer-to-main', {type: 'hello'}) 根据 message.type 路由处理 event.reply('main-to-renderer', {message: '收到!'}) 更新 DOM 显示回复内容

该流程清晰展示了请求-响应模型的闭环,符合典型客户端-服务端交互范式。


五、安全与性能考量

5.1 安全风险

当前配置:

webPreferences: {
    nodeIntegration: true,
    contextIsolation: false
}

虽便于开发,但存在隐患:

风险 说明
XSS 攻击 若页面加载外部 HTML/JS,攻击者可调用 require('fs') 删除文件
原型污染 可篡改全局对象,影响主进程

解决方案:迁移到 preload 脚本模式(未来演进方向)。

5.2 性能建议

  • 避免高频发送:如鼠标移动事件,应使用节流(throttle)或防抖(debounce)。
  • 减少大数据传输:IPC 序列化有开销,大文件建议通过 path 传递路径而非内容。
  • 及时清理监听器:页面卸载时调用 ipcRenderer.removeAllListeners()(若动态注册)。

六、HarmonyOS PC 适配要点

项目 建议
运行环境 确保 Node.js ≥ 18,Electron ≥ 25
硬件加速 必须调用 app.disableHardwareAcceleration()
中文字体 CSS 中明确指定 PingFang SC, Microsoft YaHei
窗口控制 使用 ipc 而非 -webkit-app-region: no-drag 实现最小化/关闭

七、扩展:多类型消息处理对比表

消息类型 渲染进程发送内容 主进程处理逻辑 回复示例
hello {type: 'hello', count: 1} 拼接问候语 + 计数 “收到!你好,渲染进程!(第1次)”
custom {type: 'custom', data: {userId: 1001}} 提取结构化数据 “收到自定义消息!用户ID: 1001”
close-window 无数据 调用 mainWindow.close() 无回复(单向命令)

📌 命令 vs 查询

  • 命令类消息(如关闭窗口):无需回复,主进程直接执行操作。
  • 查询类消息(如获取系统信息):必须回复,形成完整交互。

完整代码

const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');

let mainWindow;

// 1. 在 Electron 层面禁用硬件加速
app.disableHardwareAcceleration();

function createWindow() {
    // 获取屏幕尺寸
    const { width: screenWidth, height: screenHeight } = screen.getPrimaryDisplay().workAreaSize;
    
    // 窗口尺寸
    const windowWidth = 600;
    const windowHeight = 500;
    
    // 计算居中位置
    const x = (screenWidth - windowWidth) / 2;
    const y = (screenHeight - windowHeight) / 2;

    mainWindow = new BrowserWindow({
        width: windowWidth,
        height: windowHeight,
        x: x,
        y: y,
        frame: false,
        transparent: true,
        resizable: false,
        movable: true,
        minimizable: true,
        closable: true,
        skipTaskbar: false,
        roundedCorners: true,
        hasShadow: true,
        webPreferences: {
            nodeIntegration: true,
            contextIsolation: false,
        },
    });

    // 创建 HTML 内容(包含 IPC 通信示例)
    const htmlContent = `
        <!DOCTYPE html>
        <html>
        <head>
            <meta charset="UTF-8">
            <style>
                * {
                    margin: 0;
                    padding: 0;
                    box-sizing: border-box;
                    user-select: none;
                }
                
                body {
                    font-family: 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Arial, sans-serif;
                    overflow: hidden;
                    background: transparent;
                }

                .window-container {
                    width: 100%;
                    height: 100vh;
                    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
                    border-radius: 16px;
                    overflow: hidden;
                    display: flex;
                    flex-direction: column;
                    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
                }

                .title-bar {
                    height: 44px;
                    background: linear-gradient(to bottom, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0.05));
                    backdrop-filter: blur(20px);
                    -webkit-backdrop-filter: blur(20px);
                    display: flex;
                    justify-content: space-between;
                    align-items: center;
                    padding: 0 20px;
                    -webkit-app-region: drag;
                    border-bottom: 1px solid rgba(255, 255, 255, 0.1);
                }

                .title-text {
                    color: white;
                    font-size: 15px;
                    font-weight: 600;
                    letter-spacing: 0.5px;
                    text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
                    font-family: 'SF Pro Display', 'Segoe UI', 'PingFang SC', sans-serif;
                }

                .window-controls {
                    display: flex;
                    gap: 10px;
                    -webkit-app-region: no-drag;
                }

                .control-btn {
                    width: 14px;
                    height: 14px;
                    border-radius: 50%;
                    border: none;
                    cursor: pointer;
                    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
                    position: relative;
                    outline: none;
                }

                .control-btn::before {
                    content: '';
                    position: absolute;
                    top: 50%;
                    left: 50%;
                    transform: translate(-50%, -50%);
                    width: 100%;
                    height: 100%;
                    border-radius: 50%;
                    background: radial-gradient(circle at 30% 30%, rgba(255, 255, 255, 0.3), transparent);
                }

                .control-btn:hover {
                    transform: scale(1.1);
                }

                .control-btn:active {
                    transform: scale(0.95);
                }

                .close-btn {
                    background: linear-gradient(135deg, #ff6b6b, #ee5a5a);
                    box-shadow: 0 2px 4px rgba(255, 107, 107, 0.4);
                }

                .close-btn:hover {
                    background: linear-gradient(135deg, #ff5252, #e04545);
                    box-shadow: 0 3px 8px rgba(255, 107, 107, 0.6);
                }

                .minimize-btn {
                    background: linear-gradient(135deg, #ffd93d, #ffcd38);
                    box-shadow: 0 2px 4px rgba(255, 217, 61, 0.4);
                }

                .minimize-btn:hover {
                    background: linear-gradient(135deg, #ffc929, #ffbd2e);
                    box-shadow: 0 3px 8px rgba(255, 217, 61, 0.6);
                }

                .maximize-btn {
                    background: linear-gradient(135deg, #6bcf7f, #4fd66a);
                    box-shadow: 0 2px 4px rgba(107, 207, 127, 0.4);
                }

                .maximize-btn:hover {
                    background: linear-gradient(135deg, #5dd66f, #45c75f);
                    box-shadow: 0 3px 8px rgba(107, 207, 127, 0.6);
                }

                .content {
                    flex: 1;
                    display: flex;
                    flex-direction: column;
                    justify-content: center;
                    align-items: center;
                    color: white;
                    padding: 30px;
                }

                h1 {
                    font-size: 28px;
                    font-weight: 700;
                    background: linear-gradient(135deg, #ffffff 0%, #f0f0f0 100%);
                    -webkit-background-clip: text;
                    -webkit-text-fill-color: transparent;
                    background-clip: text;
                    text-shadow: none;
                    letter-spacing: 1px;
                    margin-bottom: 30px;
                    font-family: 'SF Pro Display', 'PingFang SC', 'Microsoft YaHei', sans-serif;
                }

                .message-area {
                    width: 100%;
                    max-width: 400px;
                    background: rgba(255, 255, 255, 0.15);
                    backdrop-filter: blur(10px);
                    -webkit-backdrop-filter: blur(10px);
                    border-radius: 12px;
                    padding: 25px;
                    border: 1px solid rgba(255, 255, 255, 0.2);
                }

                .message-title {
                    font-size: 16px;
                    font-weight: 600;
                    margin-bottom: 15px;
                    color: rgba(255, 255, 255, 0.95);
                    font-family: 'SF Pro Text', 'PingFang SC', sans-serif;
                }

                .button-group {
                    display: flex;
                    gap: 12px;
                    margin-bottom: 20px;
                }

                .action-btn {
                    flex: 1;
                    padding: 12px 20px;
                    background: linear-gradient(135deg, #ffffff, #f0f0f0);
                    border: none;
                    border-radius: 8px;
                    color: #667eea;
                    font-size: 14px;
                    font-weight: 600;
                    cursor: pointer;
                    transition: all 0.3s ease;
                    font-family: 'SF Pro Text', 'PingFang SC', sans-serif;
                    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
                }

                .action-btn:hover {
                    transform: translateY(-2px);
                    box-shadow: 0 6px 12px rgba(0, 0, 0, 0.2);
                }

                .action-btn:active {
                    transform: translateY(0);
                }

                .response-display {
                    background: rgba(0, 0, 0, 0.3);
                    border-radius: 8px;
                    padding: 15px;
                    min-height: 80px;
                    display: flex;
                    flex-direction: column;
                    justify-content: center;
                }

                .response-label {
                    font-size: 12px;
                    color: rgba(255, 255, 255, 0.7);
                    margin-bottom: 8px;
                    font-family: 'SF Pro Text', sans-serif;
                }

                .response-text {
                    font-size: 18px;
                    color: #4fd66a;
                    font-weight: 600;
                    font-family: 'SF Pro Display', 'PingFang SC', sans-serif;
                    word-break: break-all;
                }

                .status-indicator {
                    display: inline-block;
                    width: 8px;
                    height: 8px;
                    border-radius: 50%;
                    background: #ffd93d;
                    margin-right: 8px;
                    animation: pulse 2s infinite;
                }

                @keyframes pulse {
                    0%, 100% {
                        opacity: 1;
                    }
                    50% {
                        opacity: 0.5;
                    }
                }

                .status-text {
                    font-size: 13px;
                    color: rgba(255, 255, 255, 0.8);
                    margin-top: 10px;
                    font-family: 'SF Pro Text', sans-serif;
                }
            </style>
        </head>
        <body>
            <div class="window-container">
                <div class="title-bar">
                    <div class="title-text">IPC 通信示例</div>
                    <div class="window-controls">
                        <button class="control-btn minimize-btn" onclick="minimizeWindow()" title="最小化"></button>
                        <button class="control-btn maximize-btn" onclick="maximizeWindow()" title="最大化"></button>
                        <button class="control-btn close-btn" onclick="closeWindow()" title="关闭"></button>
                    </div>
                </div>
                <div class="content">
                    <h1>主进程 ↔ 页面通信</h1>
                    
                    <div class="message-area">
                        <div class="message-title">
                            <span class="status-indicator"></span>
                            IPC 消息传输演示
                        </div>
                        
                        <div class="button-group">
                            <button class="action-btn" onclick="sendMessage()">发送消息</button>
                            <button class="action-btn" onclick="sendCustomMessage()">发送自定义消息</button>
                        </div>
                        
                        <div class="response-display">
                            <div class="response-label">主进程回复:</div>
                            <div class="response-text" id="responseText">等待消息...</div>
                        </div>
                        
                        <div class="status-text" id="statusText">状态:就绪</div>
                    </div>
                </div>
            </div>

            <script>
                const { ipcRenderer } = require('electron');
                
                let messageCount = 0;

                // 窗口控制函数
                function closeWindow() {
                    ipcRenderer.send('close-window');
                }

                function minimizeWindow() {
                    ipcRenderer.send('minimize-window');
                }

                function maximizeWindow() {
                    ipcRenderer.send('maximize-window');
                }

                // 发送消息到主进程
                function sendMessage() {
                    messageCount++;
                    const message = {
                        type: 'hello',
                        content: '你好,主进程!',
                        timestamp: new Date().toLocaleTimeString(),
                        count: messageCount
                    };
                    
                    // 更新状态
                    document.getElementById('statusText').textContent = '状态:发送中...';
                    document.getElementById('responseText').textContent = '发送消息:' + message.content + ' (第' + messageCount + '次)';
                    document.getElementById('responseText').style.color = '#ffd93d';
                    
                    // 使用 ipcRenderer.send 发送消息给主进程
                    ipcRenderer.send('renderer-to-main', message);
                }

                // 发送自定义消息
                function sendCustomMessage() {
                    messageCount++;
                    const customMessage = {
                        type: 'custom',
                        content: '这是自定义消息 #' + messageCount,
                        data: {
                            userId: 1001,
                            action: 'test',
                            priority: 'high'
                        },
                        timestamp: new Date().toLocaleTimeString()
                    };
                    
                    document.getElementById('statusText').textContent = '状态:发送自定义消息...';
                    document.getElementById('responseText').textContent = '发送:' + JSON.stringify(customMessage.data);
                    document.getElementById('responseText').style.color = '#ffd93d';
                    
                    ipcRenderer.send('renderer-to-main', customMessage);
                }

                // 监听来自主进程的回复
                // 使用 ipcRenderer.on 接收主进程的消息
                ipcRenderer.on('main-to-renderer', (event, response) => {
                    console.log('收到主进程回复:', response);
                    
                    // 显示回复内容
                    document.getElementById('responseText').textContent = response.message;
                    document.getElementById('responseText').style.color = '#4fd66a';
                    document.getElementById('statusText').textContent = '状态:' + response.status;
                });

                // 也可以监听特定频道的消息
                ipcRenderer.on('acknowledge', (event, data) => {
                    console.log('收到确认:', data);
                });
            </script>
        </body>
        </html>
    `;

    mainWindow.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent(htmlContent));

    // ========== 主进程 IPC 处理 ==========
    
    // 使用 ipcMain.on 监听渲染进程发来的消息
    ipcMain.on('renderer-to-main', (event, message) => {
        console.log('主进程收到消息:', message);
        
        // 处理不同类型的消息
        let responseMessage = '';
        if (message.type === 'hello') {
            responseMessage = '收到!你好,渲染进程!(第' + message.count + '次)';
        } else if (message.type === 'custom') {
            responseMessage = '收到自定义消息!用户 ID: ' + message.data.userId + ', 优先级:' + message.data.priority;
        } else {
            responseMessage = '收到消息:' + message.content;
        }
        
        // 构造回复对象
        const response = {
            message: responseMessage,
            status: '已成功接收',
            timestamp: new Date().toLocaleTimeString(),
            receivedData: message
        };
        
        // 使用 event.reply 回复给渲染进程
        // 这会发送回原来的渲染进程
        event.reply('main-to-renderer', response);
        
        // 也可以使用 send 方法(需要知道目标窗口的 ID)
        // BrowserWindow.fromId(event.sender.id).webContents.send('main-to-renderer', response);
    });

    // 监听窗口控制事件
    ipcMain.on('close-window', () => {
        mainWindow.close();
    });

    ipcMain.on('minimize-window', () => {
        mainWindow.minimize();
    });

    ipcMain.on('maximize-window', () => {
        if (mainWindow.isMaximized()) {
            mainWindow.unmaximize();
        } else {
            mainWindow.maximize();
        }
    });
}

// 引入 screen 模块
const { screen } = require('electron');

app.whenReady().then(createWindow);

// 处理 macOS 上的所有窗口关闭事件
app.on('window-all-closed', () => {
    app.quit();
});

运行界面:
在这里插入图片描述
在这里插入图片描述

八、总结与最佳实践

  1. IPC 是 Electron 应用的“神经系统”,掌握 send / on / reply 三件套是基本功。
  2. 消息设计要结构化:使用 type 字段实现逻辑路由,提升可维护性。
  3. 优先使用 event.reply():安全、简洁、不易出错。
  4. 生产环境务必启用 contextIsolation:通过 preload.js 暴露受控 API。
  5. 在 HarmonyOS PC 上:禁用硬件加速 + 指定中文字体 + 使用 IPC 控制窗口,是稳定运行的关键。

通过本文的深入解析,你不仅掌握了基础通信,更理解了其背后的架构思想与工程实践。这将为你在鸿蒙生态中构建高性能、高安全性的桌面应用打下坚实基础。


欢迎加入开源鸿蒙PC社区:https://harmonypc.csdn.net/

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐