electron多进程通信

ops/2025/3/4 8:32:27/

进程间通信 | Electron

进程间通信 (IPC) 是在 Electron 中构建功能丰富的桌面应用程序的关键部分之一。 由于主进程和渲染器进程在 Electron 的进程模型具有不同的职责,因此 IPC 是执行许多常见任务的唯一方法,例如从 UI 调用原生 API 或从原生菜单触发 Web 内容的更改。

IPC 通道​


        在 Electron 中,进程通过使用 ipcMain 和 ipcRenderer 模块通过开发人员定义的“通道”传递消息来进行通信。

        这些通道是 任意 (您可以随意命名它们)和 双向 (您可以在两个模块中使用相同的通道名称)的。

        在本指南中,我们将介绍一些基本的 IPC 模式,并提供具体的示例。您可以将这些示例作为您应用程序代码的参考。

了解上下文隔离进程

        在继续实施细节之前,您应该熟悉使用预加载脚本在上下文隔离的渲染器进程中导入 Node.js 和 Electron 模块的想法。

        有关 Electron 进程模型的完整概述,您可以阅读进程模型文档。
        有关使用 contextBridge 模块从预加载脚本公开 API 的入门知识,请查看上下文隔离教程。

 上下文隔离 | Electron

上下文隔离是什么?​

        上下文隔离功能将确保您的 预加载脚本 和 Electron的内部逻辑 运行在所加载的 webcontent网页 之外的另一个独立的上下文环境里。 这对安全性很重要,因为它有助于阻止网站访问 Electron 的内部组件 您的预加载脚本可访问的高等级权限的API

        这意味着,实际上,您的预加载脚本访问的 window 对象并不是网站所能访问的对象。 例如,如果您在预加载脚本中设置 window.hello = 'wave' 并且启用了上下文隔离,当网站尝试访问window.hello对象时将返回 undefined。

        自 Electron 12 以来,默认情况下已启用上下文隔离,并且它是 _所有应用程序_推荐的安全设置。

启用上下文隔离​

        Electron 提供一种专门的模块来无阻地帮助您完成这项工作。
        contextBridge 模块可用于安全地将 API 从预加载脚本的隔离上下文公开到网站正在运行的上下文中

preload.js

// 在上下文隔离启用的情况下使用预加载
const { contextBridge } = require('electron')contextBridge.exposeInMainWorld('myAPI', {doAThing: () => {}
})

renderer.js

// 在渲染器进程使用导出的 API
window.myAPI.doAThing()

请阅读 contextBridge 的文档,以全面了解其限制。 例如,您不能在 contextBridge 中暴露原型或者 Symbol。

安全事项​

        单单开启和使用 contextIsolation 并不直接意味着您所做的一切都是安全的。 例如,此代码是 不安全的

preload.js

// ❌ 错误使用
contextBridge.exposeInMainWorld('myAPI', {send: ipcRenderer.send
})

        它直接暴露了一个没有任何参数过滤的高等级权限 API 。 这将允许任何网站发送任意的 IPC 消息,这不会是你希望发生的。 相反,暴露进程间通信相关 API 的正确方法是为每一种通信消息提供一种实现方法。

preload.js

// ✅ 正确使用
contextBridge.exposeInMainWorld('myAPI', {loadPreferences: () => ipcRenderer.invoke('load-prefs')
})

与Typescript一同使用​

如果您正在使用 TypeScript 构建 Electron 应用程序,您需要给通过 context bridge 暴露的 API 添加类型。 渲染进程的 window 对象将不会包含正确扩展类型,除非给其添加了 类型声明。

例如,在这个 preload.ts 脚本中:

preload.ts

contextBridge.exposeInMainWorld('electronAPI', {loadPreferences: () => ipcRenderer.invoke('load-prefs')
})

您可以创建一个 interface.d.ts 类型声明文件,并且全局增强 Window 接口。

interface.d.ts

export interface IElectronAPI {loadPreferences: () => Promise<void>,
}declare global {interface Window {electronAPI: IElectronAPI}
}

以上所做皆是为了确保在您编写渲染进程的脚本时, TypeScript 编译器将会知晓electronAPI合适地在您的全局window对象中

renderer.ts

window.electronAPI.loadPreferences()

模式1:render process 到 main process (单向)

        

        ipcRenderer.send   渲染进程发送消息    ipcMain.on 主进程接收消息

        要从渲染器进程向主进程发送单向 IPC 消息,可以使用 ipcRenderer.send API 发送一条消息,然后由 ipcMain.on API 接收该消息。     

        通常使用此模式从 Web 内容调用主进程 API。 我们将通过创建一个简单的应用来演示此模式,可以通过编程方式更改它的窗口标题。

        对于此演示,您需要将代码添加到主进程、渲染器进程和预加载脚本。 完整代码如下,我们将在后续章节中对每个文件进行单独解释。

        https://github.com/electron/electron-quick-start   可以通过这个项目创建一个简单的electron demo,然后在其中增删代码;更新代码:

main.js

const { app, BrowserWindow, ipcMain } = require('electron/main')
const path = require('node:path')function createWindow () {const mainWindow = new BrowserWindow({webPreferences: {preload: path.join(__dirname, 'preload.js')}})ipcMain.on('set-title', (event, title) => {const webContents = event.senderconst win = BrowserWindow.fromWebContents(webContents)win.setTitle(title)})mainWindow.loadFile('index.html')
}app.whenReady().then(() => {createWindow()app.on('activate', function () {if (BrowserWindow.getAllWindows().length === 0) createWindow()})
})app.on('window-all-closed', function () {if (process.platform !== 'darwin') app.quit()
})

​preload.js

const { contextBridge, ipcRenderer } = require('electron/renderer')contextBridge.exposeInMainWorld('electronAPI', {setTitle: (title) => ipcRenderer.send('set-title', title)
})

index.html

<!DOCTYPE html>
<html><head><meta charset="UTF-8"><!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --><meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'"><title>Hello World!</title></head><body>Title: <input id="title"/><button id="btn" type="button">Set</button><script src="./renderer.js"></script></body>
</html>

renderer.js

const setButton = document.getElementById('btn')
const titleInput = document.getElementById('title')
setButton.addEventListener('click', () => {const title = titleInput.valuewindow.electronAPI.setTitle(title)
})

1. 通过 ipcMain.on 监听事件

在主进程中,使用 ipcMain.on API 在 set-title 通道上设置 IPC 监听器:

        上面的handleSetTitle回调有两个参数:一个IpcMainEvent结构和一个标题字符串。

         每当消息通过 set-title 通道传入时,此函数找到附加到消息发送方的 BrowserWindow 实例,并在该实例上使用 win.setTitle API。

2. 通过预加载脚本暴露 ipcRenderer.send

要将消息发送到上面创建的监听器,您可以使用 ipcRenderer.send API。 默认情况下,渲染器进程没有权限访问 Node.js 和 Electron 模块。 作为应用开发者,您需要使用 contextBridge API 来选择要从预加载脚本中暴露哪些 API。

在您的预加载脚本中添加以下代码,向渲染器进程暴露一个全局的 window.electronAPI 变量。

3. 构建渲染器进程 UI

在 BrowserWindow 加载的我们的 HTML 文件中,添加一个由文本输入框和按钮组成的基本用户界面:index.html

        为了使这些元素具有交互性,我们将在导入的 renderer.js 文件中添加几行代码,以利用从预加载脚本中暴露的 window.electronAPI 功能:

 

模式 2:渲染器进程到主进程(双向)

        渲染进程调用  ipcRenderer.invoke 接口调用主进程模块,主进程通过  ipcMain.handle. 处理并且返回结果;​

        双向 IPC 的一个常见应用是从渲染器进程代码调用主进程模块并等待结果。 This can be done by using ipcRenderer.invoke paired with ipcMain.handle.

在下面的示例中,我们将从渲染器进程打开一个原生的文件对话框,并返回所选文件的路径。

        对于此演示,您需要将代码添加到主进程、渲染器进程和预加载脚本。 完整代码如下,我们将在后续章节中对每个文件进行单独解释。

main.js

const { app, BrowserWindow, ipcMain, dialog } = require('electron/main')
const path = require('node:path')async function handleFileOpen () {const { canceled, filePaths } = await dialog.showOpenDialog()if (!canceled) {return filePaths[0]}
}function createWindow () {const mainWindow = new BrowserWindow({webPreferences: {preload: path.join(__dirname, 'preload.js')}})mainWindow.loadFile('index.html')
}app.whenReady().then(() => {ipcMain.handle('dialog:openFile', handleFileOpen)createWindow()app.on('activate', function () {if (BrowserWindow.getAllWindows().length === 0) createWindow()})
})app.on('window-all-closed', function () {if (process.platform !== 'darwin') app.quit()
})

preload.js

const { contextBridge, ipcRenderer } = require('electron/renderer')contextBridge.exposeInMainWorld('electronAPI', {openFile: () => ipcRenderer.invoke('dialog:openFile')
})

index.html

<!DOCTYPE html>
<html><head><meta charset="UTF-8"><!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --><meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'"><title>Dialog</title></head><body><button type="button" id="btn">Open a File</button>File path: <strong id="filePath"></strong><script src='./renderer.js'></script></body>
</html>

renderer.js

const btn = document.getElementById('btn')
const filePathElement = document.getElementById('filePath')btn.addEventListener('click', async () => {const filePath = await window.electronAPI.openFile()filePathElement.innerText = filePath
})

不建议使用的方法:

注意:对于旧方法​

ipcRenderer.invoke API 是在 Electron 7 中添加的,作为处理渲染器进程中双向 IPC 的一种开发人员友好的方式。 However, a couple of alternative approaches to this IPC pattern exist.

如果可能,请避免使用旧方法

我们建议尽可能使用 ipcRenderer.invoke 。 出于保留历史的目地,记录了下面双向地渲染器到主进程模式。

info

对于以下示例,我们将直接从预加载脚本调用 ipcRenderer,以保持代码示例短小。

使用 ipcRenderer.send

我们用于单向通信的 ipcRenderer.send API 也可用于双向通信。 这是在 Electron 7 之前通过 IPC 进行异步双向通信的推荐方式。

preload.js (Preload Script)

// 您也可以使用 `contextBridge` API
// 将这段代码暴露给渲染器进程
const { ipcRenderer } = require('electron')ipcRenderer.on('asynchronous-reply', (_event, arg) => {console.log(arg) // 在 DevTools 控制台中打印“pong”
})
ipcRenderer.send('asynchronous-message', 'ping')

main.js (Main Process)

ipcMain.on('asynchronous-message', (event, arg) => {console.log(arg) // 在 Node 控制台中打印“ping”// 作用如同 `send`,但返回一个消息// 到发送原始消息的渲染器event.reply('asynchronous-reply', 'pong')
})

这种方法有几个缺点:

  • 您需要设置第二个 ipcRenderer.on 监听器来处理渲染器进程中的响应。 使用 invoke,您将获得作为 Promise 返回到原始 API 调用的响应值。
  • 没有显而易见的方法可以将 asynchronous-reply 消息与原始的 asynchronous-message 消息配对。 如果您通过这些通道非常频繁地来回传递消息,则需要添加其他应用代码来单独跟踪每个调用和响应。
使用 ipcRenderer.sendSync

ipcRenderer.sendSync API 向主进程发送消息,并 同步 等待响应。

main.js (Main Process)

const { ipcMain } = require('electron')
ipcMain.on('synchronous-message', (event, arg) => {console.log(arg) // 在 Node 控制台中打印“ping”event.returnValue = 'pong'
})

preload.js (Preload Script)

// 您也可以使用 `contextBridge` API
// 将这段代码暴露给渲染器进程
const { ipcRenderer } = require('electron')const result = ipcRenderer.sendSync('synchronous-message', 'ping')
console.log(result) // 在 DevTools 控制台中打印“pong”

这份代码的结构与 invoke 模型非常相似,但出于性能原因,我们建议避免使用此 API。 它的同步特性意味着它将阻塞渲染器进程,直到收到回复为止。

模式 3:主进程到渲染器进程

        将消息从主进程发送到渲染器进程时,需要指定是哪一个渲染器接收消息;

        需要通过 WebContents 实例将消息发送给渲染器进程。此 WebContents 实例包含一个 send 方法,其使用方式与 ipcRenderer.send 相同。

        为了演示此模式,我们将构建一个由原生操作系统菜单控制的数字计数器。

        对于此演示,您需要将代码添加到主进程、渲染器进程和预加载脚本。 完整代码如下,我们将在后续章节中对每个文件进行单独解释。

main.js

const { app, BrowserWindow, Menu, ipcMain } = require('electron/main')
const path = require('node:path')function createWindow () {const mainWindow = new BrowserWindow({webPreferences: {preload: path.join(__dirname, 'preload.js')}})const menu = Menu.buildFromTemplate([{label: app.name,submenu: [{click: () => mainWindow.webContents.send('update-counter', 1),label: 'Increment'},{click: () => mainWindow.webContents.send('update-counter', -1),label: 'Decrement'}]}])Menu.setApplicationMenu(menu)mainWindow.loadFile('index.html')// Open the DevTools.mainWindow.webContents.openDevTools()
}app.whenReady().then(() => {ipcMain.on('counter-value', (_event, value) => {console.log(value) // will print value to Node console})createWindow()app.on('activate', function () {if (BrowserWindow.getAllWindows().length === 0) createWindow()})
})app.on('window-all-closed', function () {if (process.platform !== 'darwin') app.quit()
})

preload.js

const { contextBridge, ipcRenderer } = require('electron/renderer')contextBridge.exposeInMainWorld('electronAPI', {onUpdateCounter: (callback) => ipcRenderer.on('update-counter', (_event, value) => callback(value)),counterValue: (value) => ipcRenderer.send('counter-value', value)
})

index.html

<!DOCTYPE html>
<html><head><meta charset="UTF-8"><!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --><meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'"><title>Menu Counter</title></head><body>Current value: <strong id="counter">0</strong><script src="./renderer.js"></script></body>
</html>

renderer.js

const counter = document.getElementById('counter')window.electronAPI.onUpdateCounter((value) => {const oldValue = Number(counter.innerText)const newValue = oldValue + valuecounter.innerText = newValue.toString()window.electronAPI.counterValue(newValue)
})

1. 使用 webContents 模块发送消息​

对于此演示,我们需要首先使用 Electron 的 Menu 模块在主进程中构建一个自定义菜单,该模块使用 webContents.send API 将 IPC 消息从主进程发送到目标渲染器。

出于本教程的目的,请务必注意, click 处理函数通过 update-counter 通道向渲染器进程发送消息(1 或 -1)。

click: () => mainWindow.webContents.send('update-counter', -1)

info

请确保您为以下步骤加载了 index.html 和 preload.js 入口点!

2. 通过预加载脚本暴露 ipcRenderer.on

与前面的渲染器到主进程的示例一样,我们使用预加载脚本中的 contextBridge 和 ipcRenderer 模块向渲染器进程暴露 IPC 功能:

3. 构建渲染器进程 UI​

为了将它们联系在一起,我们将在加载的 HTML 文件中创建一个接口,其中包含一个 #counter 元素,我们将使用该元素来显示值:

模式 4:点对点通信,不通过主进程中转​

没有直接的方法可以使用 ipcMain 和 ipcRenderer 模块在 Electron 中的渲染器进程之间发送消息。 为此,您有两种选择:

  • 将主进程作为渲染器之间的消息代理。 这需要将消息从一个渲染器发送到主进程,然后主进程将消息转发到另一个渲染器。
  • Pass a MessagePort from the main process to both renderers. 这将允许在初始设置后渲染器之间直接进行通信。

Electron 中的消息端口 | Electron

MessageChannelMain和MessageChannel的主要区别在于它们的应用场景、实现方式以及消息传递机制。

应用场景和实现方式

  • MessageChannel‌:这是一种基于DOM的通信方式,主要用于在不同的浏览器窗口或iframe之间建立通信管道。它通过两端的端口(port1和port2)发送消息,以DOM Event的形式进行异步通信‌1。
  • MessageChannelMain‌:这是Electron框架中的一个组件,用于主进程与渲染进程之间的通信。它创建一对已连接的MessagePortMain对象,允许主进程和渲染进程之间高效地传递消息‌2。

消息传递机制

  • MessageChannel‌:消息传递是通过port1和port2的onmessage事件监听器来实现的。需要在port1和port2上分别设置监听函数,并通过postMessage方法发送消息。需要注意的是,初始时消息传递是暂停的,需要调用start()方法来启动消息传递‌1。
  • MessageChannelMain‌:在Electron中,MessageChannelMain提供了类似于DOMMessageChannel的功能,但它在Electron的Node.js环境中实现。它使用Node.js的EventEmitter系统来处理事件,而不是DOM的EventTarget系统。这意味着在Electron中监听消息时,应使用port.on('message', ...)而不是port.onmessage = ...或port.addEventListener('message', ...)‌

MessageChannelMain

        

        MessageChannelMain 可以理解为一个独立的消息队列,提供的两个 port 之间互为对方的管道;下面的操作实现了 render进程和utility进程实现点对点的通信

main.js

const { app, BrowserWindow,ipcMain,utilityProcess, MessageChannelMain } = require('electron/main')
const path = require('node:path')let mainWindow;function createWindow () {mainWindow = new BrowserWindow({webPreferences: {preload: path.join(__dirname, 'preload.js')}})mainWindow.loadFile('index.html')// Open the DevTools.mainWindow.webContents.openDevTools()
}function createUtiltyProcess () {const {port1, port2} = new MessageChannelMain()const child = utilityProcess.fork(path.join(__dirname, "utility.js"))child.on("spawn", () => {child.postMessage({message: 'hello'}, [port1])})mainWindow.webContents.postMessage('channel-port','test', [port2])
}app.whenReady().then(() => {ipcMain.on('createProcess', () => {createUtiltyProcess();})createWindow()app.on('activate', function () {if (BrowserWindow.getAllWindows().length === 0) createWindow()})
})app.on('window-all-closed', function () {if (process.platform !== 'darwin') app.quit()
})

utility.js

console.log('Listening for messages...');let cnt = 1;process.parentPort.on('message', (e) => {const port = e.ports[0];process.parentPort.postMessage({data: "Ready"});console.log("I m coming,do you find me?")port.on("message", (e) => {console.log("utility receive:", e.data)setTimeout(() => {port.postMessage(`I receive your message:${cnt++}`)}, 2000)});port.start();port.postMessage({data: "Ready"});
});

preload.js

const { contextBridge, ipcRenderer } = require('electron/renderer')contextBridge.exposeInMainWorld('electronAPI', {createProcess: () => ipcRenderer.send('createProcess'),
})ipcRenderer.on('channel-port', (e) => {// e.ports is a list of ports sent along with this messagee.ports[0].onmessage = (messageEvent) => {console.log(messageEvent.data);e.ports[0].postMessage("renderer send");}e.ports[0].start();
})

renderer.js


const btn_createPorcess = document.getElementById('btn_createPorcess')btn_createPorcess.addEventListener('click', () => {window.electronAPI.createProcess();
})

index.html

<!DOCTYPE html>
<html><head><meta charset="UTF-8"><!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --><meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'"><title>Menu Counter</title></head><body><button id="btn_createPorcess" type="button">Set</button><script src="./renderer.js"></script></body>
</html>

效果:


http://www.ppmy.cn/ops/162660.html

相关文章

taoCMS v3.0.2 任意文件读取漏洞(CVE-2022-23316)

漏洞简介&#xff1a; taoCMS v3.0.2 存在任意文件读取漏洞 漏洞环境&#xff1a; 春秋云镜中的漏洞靶标&#xff0c;CVE编号为CVE-2022-23316 漏洞复现 漏洞的位置在 \taocms\include\Model\File.php 中的第 55 行&#xff0c;我们可以看到 path 参数直接传递给file_get_…

wav格式的音频压缩,WAV 转 MP3 VBR 体积缩减比为 13.5%、多个 MP3 格式音频合并为一个、文件夹存在则删除重建,不存在则直接建立

&#x1f947; 版权: 本文由【墨理学AI】原创首发、各位读者大大、敬请查阅、感谢三连 &#x1f389; 声明: 作为全网 AI 领域 干货最多的博主之一&#xff0c;❤️ 不负光阴不负卿 ❤️ 文章目录 问题一&#xff1a;wav格式的音频压缩为哪些格式&#xff0c;网络传输给用户播放…

校园订餐微信小程序(全套)

网络技术的快速发展给各行各业带来了很大的突破&#xff0c;也给各行各业提供了一种新的管理模块和校园订餐模块&#xff0c;对于校园订餐小程序将是又一个传统管理到智能化信息管理的改革&#xff0c;对于传统的校园订餐管理&#xff0c;所包括的信息内容比较多&#xff0c;对…

【Jenkins】一种灵活定义多个执行label节点的jenkinsfile写法

确定执行机器和自定义工作目录&#xff08;忽略节点的workspace&#xff09; pipeline{agent {node {label "XXXXX"customWorkspace "E:/workspace/"}}parameters {}options {}stages {}post {} }仅确定执行机器 pipeline{agent { label "XXXXX&quo…

Scala 字符串插值的简单介绍

在 Scala 中&#xff0c;字符串插值是通过在字符串前添加特定前缀实现的&#xff0c;主要有三种标准插值器&#xff1a;s、f 和 raw。它们的核心用法如下&#xff1a; 1. s 插值器&#xff08;简单插值&#xff09; 作用&#xff1a;将变量或表达式直接嵌入字符串。语法&#…

基于PHP+MySQL校园新闻管理系统设计与实现

摘要 信息技术飞速发展&#xff0c;校园新闻管理走向数字化、信息化成为必然。一个高效的校园新闻管理系统对学校信息及时发布与传播极为关键。由此提出基于 PHPMySQL 的校园新闻管理系统。此系统前端运用 HTML、CSS 和 JavaScript 搭建用户界面&#xff0c;给予用户良好交互体…

php中使用laravel9项目 使用FFMpeg视频剪辑功能

1&#xff1a;需要现在系统中安装FFMpeg软件 2&#xff1a;在对应laravel项目中 按照扩展 composer require pbmedia/laravel-ffmpeg 2.1 发布配置文件 php artisan vendor:publish --provider"ProtoneMedia\LaravelFFMpeg\Support\ServiceProvider" 这会在 con…

神经网络|(十一)|神经元和神经网络

【1】引言 前序已经了解了基本的神经元知识&#xff0c;相关文章链接为&#xff1a; 神经网络|(一)加权平均法&#xff0c;感知机和神经元-CSDN博客 神经网络|(二)sigmoid神经元函数_sigmoid函数绘制-CSDN博客 神经网络|(三)线性回归基础知识-CSDN博客 把不同的神经元通过…