Electron IPC 通信与接口设计

使用 preload、invoke 和 handle,让 Vue 页面通过清晰的接口安全调用主进程能力。

🎯 引言

渲染进程负责页面,但它不能直接调用主进程中的 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 处理版本请求:

main.js
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

preload.js
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

index.html
<!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 调用预加载脚本提供的方法,再把返回值显示到页面中:

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.jsonversion 字段。


⚡ invoke 和 handle 如何配合

本例只使用一组 IPC API:invokehandle

// 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 中:

preload.js
const { contextBridge, ipcRenderer } = require('electron/renderer');

electronAPI 是 undefined

先检查 BrowserWindow 是否正确指定了预加载脚本:

main.js
webPreferences: {
    preload: path.join(__dirname, 'preload.js'),
},

还要确认 preload.jsmain.js 位于同一目录。

页面一直显示“正在读取版本...”

检查两边的频道名是否完全一致:

// preload.js
ipcRenderer.invoke('app:get-version');

// main.js
ipcMain.handle('app:get-version', () => {});

一个字符不同,主进程就接收不到对应请求。


🧾 小节总结

  • 渲染进程不能直接调用主进程中的 app.getVersion()
  • 预加载脚本通过 contextBridge 给页面提供明确的方法。
  • ipcRenderer.invoke 从渲染侧发起请求,并返回 Promise。
  • ipcMain.handle 在主进程接收请求并返回结果。
  • invokehandle 的频道名必须完全一致。
  • 主进程、预加载脚本和渲染进程代码要放在对应文件中。

❓ 知识问答

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 频道,让页面读取当前系统名称。

main.js
ipcMain.handle('app:get-platform', () => {
    // 请在这里编写代码:返回 process.platform
});
preload.js
contextBridge.exposeInMainWorld('electronAPI', {
    getAppVersion: () => ipcRenderer.invoke('app:get-version'),
    // 请在这里编写代码:提供 getPlatform 方法
});

练习二:在页面中增加 #platform 元素,并把系统名称显示出来:

renderer.js
async function showPlatform() {
    // 请在这里编写代码
}

🎉 恭喜你已经完成一次完整的 Electron 进程通信啦!接下来可以使用相同方式,让页面请求其他主进程能力。