Electron IPC 通信与接口设计
🎯 引言
渲染进程负责页面,但它不能直接调用主进程中的 app.getVersion()。如果页面想读取应用版本,就需要把请求发送给主进程,再等待主进程返回结果。
本篇只完成这一件事:在页面中显示 Electron 应用的版本号。
学完本篇,你将能够:
- 理解预加载脚本在通信链路中的位置。
- 使用
ipcRenderer.invoke从页面发起请求。 - 使用
ipcMain.handle在主进程处理请求。 - 把主进程返回的数据显示到页面中。
🧱 为什么需要 IPC
应用版本来自 app.getVersion(),而 app 只能在主进程中使用。渲染进程不能直接调用主进程函数,所以需要 IPC(进程间通信) 传递请求和结果。
这次通信会经过三个位置:
| 位置 | 本例中的任务 |
|---|---|
| 渲染进程 | 请求应用版本,并把结果显示到页面 |
| 预加载脚本 | 给页面提供 getAppVersion() 方法并转发请求 |
| 主进程 | 调用 app.getVersion(),把版本号返回给页面 |
完整流程是:
renderer.js
→ window.electronAPI.getAppVersion()
preload.js
→ ipcRenderer.invoke('app:get-version')
main.js
→ ipcMain.handle('app:get-version')
→ 返回版本号
app:get-version 是这次通信使用的频道名。发送方和接收方使用相同频道名,消息才能正确对应。
🛠 完成版本号读取 Demo
项目需要四个文件:
electron-course/
├── main.js
├── preload.js
├── index.html
└── renderer.js
main.js 中。1. 主进程处理版本请求
main.js 创建窗口,同时使用 ipcMain.handle 处理版本请求:
const { app, BrowserWindow, ipcMain } = require('electron/main');
const path = require('node:path');
ipcMain.handle('app:get-version', () => {
return app.getVersion();
});
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
},
});
win.loadFile(path.join(__dirname, 'index.html'));
}
app.whenReady().then(() => {
createWindow();
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
ipcMain.handle 的回调运行在主进程,因此可以调用 app.getVersion()。
2. 预加载脚本提供页面方法
preload.js 使用 contextBridge,在页面的 window 上提供 electronAPI:
const { contextBridge, ipcRenderer } = require('electron/renderer');
contextBridge.exposeInMainWorld('electronAPI', {
getAppVersion: () => ipcRenderer.invoke('app:get-version'),
});
页面最终可以调用的方法是:
window.electronAPI.getAppVersion();
这个方法内部使用 ipcRenderer.invoke 向主进程发送请求。页面只关心“读取版本”,不需要知道 Electron 模块的其他内容。
3. 页面准备显示位置
在 index.html 中准备一个段落,并引入 renderer.js:
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'"
/>
<title>Electron IPC</title>
</head>
<body>
<h1>应用信息</h1>
<p id="version">正在读取版本...</p>
<script src="./renderer.js"></script>
</body>
</html>
4. 渲染进程请求版本
renderer.js 调用预加载脚本提供的方法,再把返回值显示到页面中:
const versionElement = document.querySelector('#version');
async function showVersion() {
const version = await window.electronAPI.getAppVersion();
versionElement.textContent = `应用版本:${version}`;
}
showVersion();
运行项目:
npm start
页面中的“正在读取版本...”会变成类似“应用版本:1.0.0”。这个版本来自 package.json 的 version 字段。
⚡ invoke 和 handle 如何配合
本例只使用一组 IPC API:invoke 和 handle。
// preload.js:发送请求,并等待返回值
ipcRenderer.invoke('app:get-version');
// main.js:处理相同频道的请求
ipcMain.handle('app:get-version', () => {
return app.getVersion();
});
两者的对应关系如下:
| 代码 | 作用 |
|---|---|
ipcRenderer.invoke(...) | 发起请求,返回一个 Promise |
ipcMain.handle(...) | 接收请求并执行处理函数 |
处理函数中的 return | 把结果返回给 invoke |
await | 等待主进程返回结果 |
只要记住一条主线:渲染侧 invoke,主进程 handle,频道名保持一致。
🪤 常见错误
contextBridge 是 undefined
原因是把 contextBridge 写进了 main.js。它应该写在 preload.js 中:
const { contextBridge, ipcRenderer } = require('electron/renderer');
electronAPI 是 undefined
先检查 BrowserWindow 是否正确指定了预加载脚本:
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
},
还要确认 preload.js 与 main.js 位于同一目录。
页面一直显示“正在读取版本...”
检查两边的频道名是否完全一致:
// preload.js
ipcRenderer.invoke('app:get-version');
// main.js
ipcMain.handle('app:get-version', () => {});
一个字符不同,主进程就接收不到对应请求。
🧾 小节总结
- 渲染进程不能直接调用主进程中的
app.getVersion()。 - 预加载脚本通过
contextBridge给页面提供明确的方法。 ipcRenderer.invoke从渲染侧发起请求,并返回 Promise。ipcMain.handle在主进程接收请求并返回结果。invoke和handle的频道名必须完全一致。- 主进程、预加载脚本和渲染进程代码要放在对应文件中。
❓ 知识问答
Q1:为什么 renderer.js 不能直接使用 app.getVersion()?
A:app 属于主进程模块,renderer.js 运行在渲染进程,两个进程不能直接调用彼此的函数。
Q2:为什么还要多一个 preload.js?
A:它负责给页面提供一个明确的调用入口。页面调用 getAppVersion(),预加载脚本再负责把请求发送给主进程。
Q3:invoke 为什么需要 await?
A:请求需要发送到另一个进程并等待处理结果,所以 invoke 返回 Promise,要通过 await 取得最终值。
Q4:app:get-version 是 Electron 固定的名称吗?
A:不是,它是我们自己定义的频道名。可以使用其他名称,但发送方和接收方必须保持一致。
🧪 小练习
练习一:增加一个 app:get-platform 频道,让页面读取当前系统名称。
ipcMain.handle('app:get-platform', () => {
// 请在这里编写代码:返回 process.platform
});
contextBridge.exposeInMainWorld('electronAPI', {
getAppVersion: () => ipcRenderer.invoke('app:get-version'),
// 请在这里编写代码:提供 getPlatform 方法
});
练习二:在页面中增加 #platform 元素,并把系统名称显示出来:
async function showPlatform() {
// 请在这里编写代码
}
🎉 恭喜你已经完成一次完整的 Electron 进程通信啦!接下来可以使用相同方式,让页面请求其他主进程能力。
