Electron 主进程与渲染进程通信详解:HarmonyOS PC基于 `ipcRenderer.send` 与 `ipcMain.on` 的双向数据传输
本文详细介绍了Electron中主进程与渲染进程间的通信机制。通过ipcRenderer.send()和ipcMain.on()方法实现双向异步通信,并推荐使用event.reply()进行定向回复。文章剖析了消息路由、数据结构设计、安全风险及性能优化策略,特别针对HarmonyOS PC环境提供了适配建议。同时通过流程图和对比表,展示了不同类型消息的处理方式,为开发者构建健壮的跨进程通信提供了完

个人主页: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: true和contextIsolation: 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()。
四、通信流程图解
该流程清晰展示了请求-响应模型的闭环,符合典型客户端-服务端交互范式。
五、安全与性能考量
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();
});
运行界面:

八、总结与最佳实践
- IPC 是 Electron 应用的“神经系统”,掌握
send/on/reply三件套是基本功。 - 消息设计要结构化:使用
type字段实现逻辑路由,提升可维护性。 - 优先使用
event.reply():安全、简洁、不易出错。 - 生产环境务必启用
contextIsolation:通过preload.js暴露受控 API。 - 在 HarmonyOS PC 上:禁用硬件加速 + 指定中文字体 + 使用 IPC 控制窗口,是稳定运行的关键。
通过本文的深入解析,你不仅掌握了基础通信,更理解了其背后的架构思想与工程实践。这将为你在鸿蒙生态中构建高性能、高安全性的桌面应用打下坚实基础。
欢迎加入开源鸿蒙PC社区:https://harmonypc.csdn.net/
更多推荐



所有评论(0)