当前位置: 首页 > news >正文

Electron通信流程

前言       

 今天讲Electron框架的通信流程,首先我们需要知道为什么需要通信。这得益于Electron的多进程模型,它主要模仿chrome的多进程模型如下图:

chrome多进程模型

作为应用开发者,我们将控制两种类型的进程:主进程和渲染器进程 。

主进程、渲染进程和Preload脚本

主进程

每个 Electron 应用都有一个单一的主进程,作为应用程序的入口点。 主进程在 Node.js 环境中运行,这意味着它具有 require 模块和使用所有 Node.js API 的能力。

除此之外,主进程还可通过BrowserWindow类的每个实例创建一个应用程序窗口,且在单独的渲染器进程中加载一个网页。

const { BrowserWindow } = require('electron')const win = new BrowserWindow({ width: 800, height: 1500 })
win.loadURL('https://github.com')const contents = win.webContents
console.log(contents)

渲染进程

每个 Electron 应用都会为每个打开的 BrowserWindow ( 与每个网页嵌入 ) 生成一个单独的渲染器进程。 洽如其名,渲染器负责 渲染 网页内容。 所以实际上,运行于渲染器进程中的代码是须遵照网页标准的 (至少就目前使用的 Chromium 而言是如此) 。

因此,一个浏览器窗口中的所有的用户界面和应用功能,都应与您在网页开发上使用相同的工具和规范来进行攥写。

此外,这也意味着渲染器无权直接访问 require 或其他 Node.js API。 为了在渲染器中直接包含 NPM 模块,您必须使用与在 web 开发时相同的打包工具 (例如 webpack 或 parcel)

既然如此,那渲染器进程用户界面怎样才能与 Node.js 和 Electron 的原生桌面功能进行交互呢?这用到了Preload脚本

Preload脚本

预加载(preload)脚本包含了那些执行于渲染器进程中,且先于网页内容开始加载的代码 。 这些脚本虽运行于渲染器的环境中,却因能访问 Node.js API 而拥有了更多的权限。

预加载脚本可以在 BrowserWindow 构造方法中的 webPreferences 选项里被附加到主进程。

const { BrowserWindow } = require('electron')
// ...
const win = new BrowserWindow({webPreferences: {preload: 'path/to/preload.js'}
})
// ...

 因为预加载脚本与浏览器共享同一个全局 Window 接口,并且可以访问 Node.js API,所以它通过在全局 window 中暴露任意 API 来增强渲染器,以便你的网页内容使用。

// preload.js
window.myAPI = {desktop: true
}// renderer.js
console.log(window.myAPI)
// => undefined

语境隔离(Context Isolation)意味着预加载脚本与渲染器的主要运行环境是隔离开来的,以避免泄漏任何具特权的 API 到您的网页内容代码中。我们可以使用contextBridge方法安全的暴露api

// preload.js 
const { contextBridge } = require('electron')contextBridge.exposeInMainWorld('myAPI', {desktop: true
})// renderer.js
console.log(window.myAPI)
// => { desktop: true }

IPC通道

通过ipcMain和ipcRenderer模块可以创建任意 (您可以随意命名它们)和 双向 (您可以在两个模块中使用相同的通道名称)的通道。

模式 1:渲染器进程到主进程(单向)​

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

对于此演示,您需要将代码添加到主进程、渲染器进程和预加载脚本。 完整代码如下:

// 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监听事件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')//通过预加载脚本暴露ipcRenderer.send
contextBridge.exposeInMainWorld('electronAPI', {setTitle: (title) => ipcRenderer.send('set-title', title)
})// renderer.js
const setButton = document.getElementById('btn')
const titleInput = document.getElementById('title')
setButton.addEventListener('click', () => {const title = titleInput.valuewindow.electronAPI.setTitle(title)
})
// index.html
// 构建渲染器进程UI
<!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>

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

双向 IPC 的一个常见应用是从渲染器进程代码调用主进程模块并等待结果。 可以通过 ipcRenderer.invoke和  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监听事件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')
// 预加载脚本暴露ipcRenderer.invoke
contextBridge.exposeInMainWorld('electronAPI', {openFile: () => ipcRenderer.invoke('dialog:openFile')
})// 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
})
// index.html
// 构建渲染器进程UI
<!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>

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

将消息从主进程发送到渲染器进程时,需要指定是哪一个渲染器接收消息。主要通过WebContents实例和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: [{// 使用webContents模块发送信息click: () => mainWindow.webContents.send('update-counter', 1),label: 'Increment'},{// 使用webContents模块发送信息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')
// 通过预加载脚本暴露ipcRenderer.on
contextBridge.exposeInMainWorld('electronAPI', {onUpdateCounter: (callback) => ipcRenderer.on('update-counter', (_event, value) => callback(value)),counterValue: (value) => ipcRenderer.send('counter-value', value)
})// 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)
})
// index.html
// 构建渲染器UI
<!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>

模式 4:渲染器进程到渲染器进程​

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

  • 将主进程作为渲染器之间的消息代理。 这需要将消息从一个渲染器发送到主进程,然后主进程将消息转发到另一个渲染器。
  • 通过MessagePort构建通信通道, 这将允许在初始设置后渲染器之间直接进行通信。

消息端口

简介

MessagePort是一个允许在不同上下文之间传递消息的Web功能。 就像 window.postMessage, 但是在不同的通道上。

下面是 MessagePort 是什么和如何工作的一个非常简短的例子:

// renderer.js
// 消息端口是成对创建的。 连接的一对消息端口
// 被称为通道。
const channel = new MessageChannel()// port1 和 port2 之间唯一的不同是你如何使用它们。 消息
// 发送到port1 将被port2 接收,反之亦然。
const port1 = channel.port1
const port2 = channel.port2// 允许在另一端还没有注册监听器的情况下就通过通道向其发送消息
// 消息将排队等待,直到一个监听器注册为止。
port2.postMessage({ answer: 42 })// 这次我们通过 ipc 向主进程发送 port1 对象。 类似的,
// 我们也可以发送 MessagePorts 到其他 frames, 或发送到 Web Workers, 等.
ipcRenderer.postMessage('port', null, [port1])
// main.js
// 在主进程中,我们接收端口对象。
ipcMain.on('port', (event) => {// 当我们在主进程中接收到 MessagePort 对象, 它就成为了// MessagePortMain.const port = event.ports[0]// MessagePortMain 使用了 Node.js 风格的事件 API, 而不是// web 风格的事件 API. 因此使用 .on('message', ...) 而不是 .onmessage = ...port.on('message', (event) => {// 收到的数据是: { answer: 42 }const data = event.data})// MessagePortMain 阻塞消息直到 .start() 方法被调用port.start()
})

实例使用

在两个渲染进程之间建立 MessageChannel​

在这个示例中,主进程设置了一个MessageChannel,然后将每个端口发送给不同的渲染进程。 这样可以让渲染进程彼此之间发送消息,而无需使用主进程作为中转。

// main.js 
const { BrowserWindow, app, MessageChannelMain } = require('electron')app.whenReady().then(async () => {// 创建窗口const mainWindow = new BrowserWindow({show: false,webPreferences: {contextIsolation: false,preload: 'preloadMain.js'}})const secondaryWindow = new BrowserWindow({show: false,webPreferences: {contextIsolation: false,preload: 'preloadSecondary.js'}})// 建立通道const { port1, port2 } = new MessageChannelMain()// webContents准备就绪后,使用postMessage向每个webContents发送一个端口。mainWindow.once('ready-to-show', () => {mainWindow.webContents.postMessage('port', null, [port1])})secondaryWindow.once('ready-to-show', () => {secondaryWindow.webContents.postMessage('port', null, [port2])})
})

接下来,在你的预加载脚本中通过IPC接收端口,并设置相应的监听器

// preloadMain.js 和 preloadSecondary.js
const { ipcRenderer } = require('electron')ipcRenderer.on('port', e => {// 接收到端口,使其全局可用。window.electronMessagePort = e.ports[0]window.electronMessagePort.onmessage = messageEvent => {// 处理消息}
})

这意味着 window.electronMessagePort 在全局范围内可用,你可以在应用程序的任何地方调用postMessage 方法,以便向另一个渲染进程发送消息。

// renderer.js
// elsewhere in your code to send a message to the other renderers message handler
window.electronMessagePort.postMessage('ping')
Worker进程​

在这个示例中,你的应用程序有一个作为隐藏窗口存在的 Worker 进程。 你希望应用程序页面能够直接与 Worker 进程通信,而不需要通过主进程进行中继,以避免性能开销。

 

// main.js
const { BrowserWindow, app, ipcMain, MessageChannelMain } = require('electron')app.whenReady().then(async () => {// Worker 进程是一个隐藏的 BrowserWindow// 它具有访问完整的Blink上下文(包括例如 canvas、音频、fetch()等)的权限const worker = new BrowserWindow({show: false,webPreferences: { nodeIntegration: true }})await worker.loadFile('worker.html')// main window 将发送内容给 worker process 同时通过 MessagePort 接收返回值const mainWindow = new BrowserWindow({webPreferences: { nodeIntegration: true }})mainWindow.loadFile('app.html')// 在这里我们不能使用 ipcMain.handle() , 因为回复需要传输// MessagePort.// 监听从顶级 frame 发来的消息mainWindow.webContents.mainFrame.ipc.on('request-worker-channel', (event) => {// 建立新通道  ...const { port1, port2 } = new MessageChannelMain()// ... 将其中一个端口发送给 Worker ...worker.webContents.postMessage('new-client', null, [port1])// ... 将另一个端口发送给主窗口event.senderFrame.postMessage('provide-worker-channel', null, [port2])// 现在主窗口和工作进程可以直接相互通信,无需经过主进程!})
})
// worker.html
<script>
const { ipcRenderer } = require('electron')const doWork = (input) => {// 一些对CPU要求较高的任务return input * 2
}// 我们可能会得到多个 clients, 比如有多个 windows,
// 或者假如 main window 重新加载了.
ipcRenderer.on('new-client', (event) => {const [ port ] = event.portsport.onmessage = (event) => {// 事件数据可以是任何可序列化的对象 (事件甚至可以// 携带其他 MessagePorts 对象!)const result = doWork(event.data)port.postMessage(result)}
})
</script>
// app.html
<script>
const { ipcRenderer } = require('electron')// 我们请求主进程向我们发送一个通道
// 以便我们可以用它与 Worker 进程建立通信
ipcRenderer.send('request-worker-channel')ipcRenderer.once('provide-worker-channel', (event) => {// 一旦收到回复, 我们可以这样做...const [ port ] = event.ports// ... 注册一个接收结果处理器 ...port.onmessage = (event) => {console.log('received result:', event.data)}// ... 并开始发送消息给 work!port.postMessage(21)
})
</script>
 回复流

Electron的内置IPC方法只支持两种模式:即发即弃(例如, send),或请求-响应(例如, invoke)。 使用MessageChannels,你可以实现一个“响应流”,其中单个请求可以返回一串数据。

// renderer.js
const makeStreamingRequest = (element, callback) => {// MessageChannels 是轻量的// 为每个请求创建一个新的 MessageChannel 带来的开销并不大const { port1, port2 } = new MessageChannel()// 我们将端口的一端发送给主进程 ...ipcRenderer.postMessage('give-me-a-stream',{ element, count: 10 },[port2])// ... 保留另一端。 主进程将向其端口发送消息// 并在完成后关闭它port1.onmessage = (event) => {callback(event.data)}port1.onclose = () => {console.log('stream ended')}
}makeStreamingRequest(42, (data) => {console.log('got response data:', data)
})
// 我们会看到 "got response data: 42" 出现了10次
// main.js
ipcMain.on('give-me-a-stream', (event, msg) => {// 渲染进程向我们发送了一个 MessagePort// 并期望得到响应const [replyPort] = event.ports// 在这里,我们同步发送消息// 我们也可以将端口存储在某个地方,异步发送消息for (let i = 0; i < msg.count; i++) {replyPort.postMessage(msg.element)}// 当我们处理完成后,关闭端口以通知另一端// 我们不会再发送任何消息 这并不是严格要求的// 如果我们没有显式地关闭端口,它最终会被垃圾回收// 这也会触发渲染进程中的'close'事件replyPort.close()
})

总结:

  • 主进程和渲染器进程之间存在语境隔离
  • 主进程和渲染器进程可通过electron内置IPC通道进行通信
  • 渲染器之间可通过IPC通道,主进程为中间人互相通信;也可通过信息端口构建信道直接进行通信

相关文章:

  • 如何优化React Native应用以适配HarmonyOS5?
  • CppCon 2015 学习:Memory and C++ debugging at Electronic Arts
  • 例说局部性原理给程序带来的提升
  • 【PyCharm必会基础】正确移除解释器及虚拟环境(以 Poetry 为例 )
  • 【每日一题 | 2025年6.2 ~ 6.8】第16届蓝桥杯部分偏简单题
  • 3.机器学习-分类模型-线性模型
  • Go语言多线程问题
  • 数据库学习(三)——MySQL锁
  • Ubuntu20.04中MySQL的安装和配置
  • 基于React 的 AntD 库进行前端开发过程中的问题汇总
  • 使用 C/C++的OpenCV 实时播放火柴人爱心舞蹈动画
  • 机器人/智能车纯视觉巡线经典策略—滑动窗口+直方图法
  • 神经网络-Day48
  • 【CBAP50技术手册】#39 Roles and Permissions Matrix(角色与权限矩阵):业务分析师的“秩序守护器”
  • AU音频软件|Audition 2025网盘下载与安装教程指南
  • 华为开源自研AI框架昇思MindSpore应用案例:ICT实现图像修复
  • 算法:位运算
  • Linux 内存管理调试分析:ftrace、perf、crash 的系统化使用
  • 从零开始的云计算生活——番外,实战脚本。
  • OpenEuler服务器警告邮件自动化发送:原理、配置与安全实践
  • wordpress 过于肿肿/唐山百度seo公司
  • wordpress中文文章排版插件/深圳网站优化培训
  • 有哪些做投行网站/做网页设计一个月能挣多少
  • 不同用户入口的网站样板/宁波seo外包方案
  • 做php网站教程视频教程/沧州搜索引擎优化
  • 精通网站建设 百度云/百度热搜关键词