进程间通信
5737字约19分钟
2024-12-03
进程间通信 (IPC)
进程间通信 (IPC) 是在 Electron 中构建功能丰富的桌面应用程序的关键部分之一。 由于主进程和渲染器进程在 Electron 的进程模型具有不同的职责,因此 IPC 是执行许多常见任务的唯一方法,例如从 UI 调用原生 API 或从原生菜单触发 Web 内容的更改。
IPC 通道
在 Electron 中,进程使用 ipcMain
和 ipcRenderer
模块,通过开发人员定义的“通道”传递消息来进行通信。 这些通道是任意(您可以随意命名它们)和双向(您可以在两个模块中使用相同的通道名称)的。
渲染器进程到主进程(单向)
要将单向 IPC 消息从渲染器进程发送到主进程,您可以使用 ipcRenderer.send
API 发送消息,然后使用 ipcMain.on
API 接收。
- ipcRenderer.send: 渲染器进程发送消息
- ipcMain.on:主进程监听消息
const { app, BrowserWindow, ipcMain } = require('electron/main')
const path = require('node:path')
function handleSetTitle (event, title) {
const webContents = event.sender
const win = BrowserWindow.fromWebContents(webContents)
win.setTitle(title)
}
function createWindow () {
const mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
mainWindow.loadFile('index.html')
}
app.whenReady().then(() => {
// 主进程监听消息
ipcMain.on('set-title', handleSetTitle)
createWindow()
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
const { contextBridge, ipcRenderer } = require('electron/renderer')
contextBridge.exposeInMainWorld('electronAPI', {
// 预加载脚本暴露发送消息的API
setTitle: (title) => ipcRenderer.send('set-title', title)
})
<!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>
const setButton = document.getElementById('btn')
const titleInput = document.getElementById('title')
setButton.addEventListener('click', () => {
const title = titleInput.value
// 调用预加载脚本暴露的API
window.electronAPI.setTitle(title)
})
上面的 handleSetTitle
回调函数有两个参数:一个 IpcMainEvent
结构和一个 title
字符串。
IpcMainEvent:
processId
Integer - 发送该消息的渲染进程内部的IDframeId
Integer - 发送该消息的渲染进程框架的ID(可能是iframe)returnValue
any - 如果对此赋值,则该值会在同步消息中返回sender
WebContent - 返回发送消息的 webContentssenderFrame
WebFrameMain | null Readonly - 发送这个消息的框架,访问后为null
的情况,框架要么还没进入导航或已进入销毁。ports
MessagePortMain[] - 带有此消息传递的 MessagePort 列表reply
Function - 将 IPC 消息发送到渲染器框架的函数,该渲染器框架发送当前正在处理的原始消息。 您应该使用“reply”方法回复发送的消息,以确保回复将转到正确的进程和框架。channel
string...args
any[]
渲染器进程到主进程(双向)
双向 IPC 的一个常见应用是从渲染器进程代码调用主进程模块并等待结果。 这可以通过将 ipcRenderer.invoke
与 ipcMain.handle
搭配使用来完成。
- ipcMain.handle: 主进程监听消息, 并返回结果
- ipcRenderer.invoke: 渲染器进程调用主进程模块, 并等待结果
在下面的示例中,我们将从渲染器进程打开一个原生的文件对话框,并返回所选文件的路径:
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()
})
const { contextBridge, ipcRenderer } = require('electron/renderer')
contextBridge.exposeInMainWorld('electronAPI', {
// 预加载脚本暴露调用API
openFile: () => ipcRenderer.invoke('dialog:openFile')
})
<!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>
const btn = document.getElementById('btn')
const filePathElement = document.getElementById('filePath')
btn.addEventListener('click', async () => {
// 调用预加载脚本暴露的API, 并等待结果
const filePath = await window.electronAPI.openFile()
filePathElement.innerText = filePath
})
在主进程中,我们将创建一个 handleFileOpen()
函数,它调用 dialog.showOpenDialog
并返回用户选择的文件路径值。 每当渲染器进程通过 dialog:openFile
通道发送 ipcRender.invoke
消息时,此函数被用作一个回调。 然后,返回值将作为一个 Promise
返回到最初的 invoke
调用。
关于错误处理
在主进程中通过 handle
引发的错误是不透明的,因为它们被序列化了,并且只有原始错误的 message
属性会提供给渲染器进程。 详情请参阅 #24427
版本注意
ipcRenderer.invoke API 是在 Electron 7 中添加的,作为处理渲染器进程中双向 IPC 的一种开发人员友好的方式。
主进程到渲染器进程(单向)
将消息从主进程发送到渲染器进程时,需要指定是哪一个渲染器接收消息。 消息需要通过其 WebContents
实例发送到渲染器进程。 此 WebContents
实例包含一个 send
方法,其使用方式与 ipcRenderer.send
相同。
- webContents.send 主进程发送消息
- ipcRenderer.on 渲染进程接收消息
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()
})
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)
})
<!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>
const counter = document.getElementById('counter')
window.electronAPI.onUpdateCounter((value) => {
const oldValue = Number(counter.innerText)
const newValue = oldValue + value
counter.innerText = newValue.toString()
window.electronAPI.counterValue(newValue)
})
渲染器进程到渲染器进程
没有直接的方法可以使用 ipcMain 和 ipcRenderer 模块在 Electron 中的渲染器进程之间发送消息。 为此,您有两种选择:
- 将主进程作为渲染器之间的消息代理。 这需要将消息从一个渲染器发送到主进程,然后主进程将消息转发到另一个渲染器。
- 从主进程将一个
MessagePort
传递到两个渲染器。 这将允许在初始设置后渲染器之间直接进行通信。
主进程作为消息代理
示例如下:
const { app, BrowserWindow, ipcMain } = require('electron/main')
const path = require('node:path')
// 创建IPC 通道
/**
* @params {BrowserWindow} toWindow 目标窗口
* @params {string} formChannel 接收消息的通道
* @params {string} toChannel 目标渲染进程接收消息的通道
*/
function createIPC(toWindow, formChannel, toChannel) {
// 主进程监听渲染进程发送的消息
ipcMain.on(formChannel, (event, message) => {
// 转发消息给另一个渲染进程
toWindow.webContents.send(toChannel, message)
})
}
function createWindow(fileName, formChannel, toChannel) {
const mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
mainWindow.loadFile(fileName)
// Open the DevTools.
mainWindow.webContents.openDevTools()
return mainWindow;
}
app.whenReady().then(() => {
const firstChannel = 'firstChannel'
const secondChannel = 'secondChannel'
const firstWin = createWindow('index.html')
const secondWin = createWindow('index2.html')
createIPC(secondWin, firstChannel, secondChannel)
createIPC(firstWin, secondChannel, firstChannel)
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
const { contextBridge, ipcRenderer } = require('electron/renderer')
contextBridge.exposeInMainWorld('electronAPI', {
// 渲染器进程发送消息
send: (channel, message) => ipcRenderer.send(channel, message),
// 渲染器进程监听消息
accept: (channel, callback) => ipcRenderer.on(channel, callback)
})
const inputEl = document.getElementById('input')
const sendEl = document.getElementById('send')
const messageEl = document.getElementById('message')
sendEl.addEventListener('click', () => {
window.electronAPI.send(sendEl.dataset.key, inputEl.value)
})
const channel = messageEl.dataset.key
function acceptMessage(event, value) {
console.log('accept', channel)
messageEl.innerHTML = value
}
window.electronAPI.accept(channel, acceptMessage)
<!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>firstChannel</title>
</head>
<body>
<input type="text" id="input" />
<button id="send" data-key="firstChannel">send</button>
<div>内容: <strong id="message" data-key="firstChannel"></strong></div>
<script src="./renderer.js"></script>
</body>
</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>secondChannel</title>
</head>
<body>
<input type="text" id="input" />
<button id="send" data-key="secondChannel">send</button>
<div>内容: <strong id="message" data-key="secondChannel"></strong></div>
<script src="./renderer.js"></script>
</body>
</html>
提示
在 Electron 中,当两个渲染进程使用同一个 preload.js
、renderer.js
文件时,它们的 JavaScript 环境在某种程度上是相似的,但它们是相互独立的。
消息端口 MessagePort
MessagePort
是一个允许在不同上下文之间传递消息的 Web 功能,类似于 window.postMessage
,但在不同的通道上。在 Electron 中,它可以用于在不同的渲染器进程之间进行通信,而无需通过主进程作为消息代理。
以下是 MessagePort 的一些关键特点和使用场景:
- 直接通信:使用
MessagePort
可以实现渲染器进程之间的直接通信,避免了将主进程作为消息代理的复杂性。 - 跨上下文通信:它可以在不同的执行上下文(如不同的
iframe
或Web Worker
)之间传递消息。
以下是一个简单的示例,展示了如何在主进程和渲染器进程之间使用 MessagePort:
// 在主进程中,我们接收端口对象。
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()
})
// 消息端口是成对创建的。 连接的一对消息端口
// 被称为通道。
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])
主进程中的 MessagePorts
在渲染器中, MessagePort
类的行为与它在 web
上的行为完全一样。 但是,主进程不是网页(它没有 Blink
[开源的浏览器渲染引擎] 集成),因此它没有 MessagePort
或 MessageChannel
类。 为了在主进程中处理 MessagePorts
并与之交互,Electron 添加了两个新类: MessagePortMain
和 MessageChannelMain
。 这些行为 类似于渲染器中 analogous
类。
MessagePort 对象可以在渲染器或主 进程中创建,并使用 ipcRenderer.postMessage
和 WebContents.postMessage
方法互相传递。 请注意,通常的 IPC 方法,例如 send
和 invoke
不能用来传输 MessagePort
, 只有 postMessage
方法可以传输 MessagePort
。
通过主进程传递 MessagePort
,就可以连接两个可能无法通信的页面 (例如,由于同源限制) 。
扩展: close 事件
Electron 在 MessagePort
添加了一个在 Web 上本不存在的功能,以使 MessagePort
更加好用。 这个功能就是 close
事件, 在通道的另一端关闭时会触发该事件。 端口也可以通过垃圾回收而隐式关闭。
在渲染进程中,你可以通过将事件分配给 port.onclose
或调用 port.addEventListener('close', ...)
来监听 close
事件。 在主进程中,你可以通过调用 port.on('close', ...)
来监听 close
事件。
在两个渲染进程之间建立 MessageChannel
在这个示例中,主进程设置了一个 MessageChannel
,然后将每个端口发送给不同的渲染进程。 这样可以让渲染进程彼此之间发送消息,而无需使用主进程作为中转。
创建时分配 MessageChannel
const { app, BrowserWindow, ipcMain, MessageChannelMain } = require('electron/main')
const path = require('node:path')
/**
* 传递 MessageChannelMain 通道
* @params toWindow 渲染进程
* @params toChannel 传递channel
* @parms port 通道
*/
function sendPort(toWindow, toChannel, port) {
toWindow.webContents.postMessage(toChannel, null, [port])
}
/**
* 创建渲染进程
* @params fileName 页面文件路径
* @params toChannel 传递channel
* @params port 通道
*/
function createWindow(filePath, toChannel, port) {
const mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
// 在加载页面时,渲染进程第一次完成绘制时,如果窗口还没有被显示,渲染进程会发出 ready-to-show 事件
mainWindow.once('ready-to-show', () => {
sendPort(mainWindow, toChannel, port)
})
mainWindow.loadFile(filePath)
// Open the DevTools.
mainWindow.webContents.openDevTools()
return mainWindow;
}
app.whenReady().then(() => {
// 创建 MessageChannelMain 通道
const { port1, port2 } = new MessageChannelMain()
// 创建窗口1
createWindow('index.html', 'port', port1)
// 创建窗口2
createWindow('second.html', 'port', port2)
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
const { contextBridge, ipcRenderer } = require('electron/renderer')
// 发送信息
function send(port) {
return function (message) {
port.postMessage(message)
}
}
// 接收信息
function accept(port) {
return function (callback) {
// 因为上下文隔离的原因,如果 port.onmessage = callback 会导致返回到 callback 的结果 messageEvent 会经过序列化,结果中 messageEvent.data 是 undefined
port.onmessage = (messageEvent) => {
callback(messageEvent.data)
}
}
}
contextBridge.exposeInMainWorld('electronAPI', {
loadPort: () => {
// ipcRenderer.on 是一个异步方法,如果放在最外层,会导致 ipcRenderer.on 的回调尚未完成,从而导致 contextBridge.exposeInMainWorld 设置的 API 还未完全准备好,进而出现访问问题
return new Promise(resolve => {
ipcRenderer.on('port', (e) => {
const port = e.ports[0]
resolve({ send: send(port), accept: accept(port) })
})
})
}
})
const inputEl = document.getElementById('input')
const sendEl = document.getElementById('send')
const messageEl = document.getElementById('message')
window.electronAPI.loadPort(acceptMessage).then(({ send, accept }) => {
// 设置接收信息回调
accept((message) => {
messageEl.innerHTML = message
})
// 发送信息
sendEl.addEventListener('click', () => {
send(inputEl.value)
})
})
<!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>index</title>
</head>
<body>
<input type="text" id="input" />
<button id="send">send</button>
<div>内容: <strong id="message"></strong></div>
<script src="./renderer.js"></script>
</body>
</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>index2</title>
</head>
<body>
<input type="text" id="input" />
<button id="send">send</button>
<div>内容: <strong id="message"></strong></div>
<script src="./renderer.js"></script>
</body>
</html>
在上面的示例中,需要注意的是:
ipcRenderer.on
是一个异步方法,在preload.js
中使用它时,其回调函数的执行时机不确定。如果contextBridge.exposeInMainWorld
暴露的 API 依赖于ipcRenderer.on
的回调函数的执行结果,可能会出现renderer.js
尝试访问这些 API 时,ipcRenderer.on
的回调尚未完成,从而导致contextBridge.exposeInMainWorld
设置的 API 还未完全准备好,进而出现访问问题。- 由于上下文隔离机制,当使用
port.onmessage = callback
时,传递给callback
的messageEvent
对象会被序列化,这可能导致messageEvent.data
的值为undefined
。
在上述示例中,MessageChannelMain
通道是在创建渲染进程的同时进行分配,并传递给渲染进程的。然而,若要在渲染进程中手动创建 MessageChannelMain
通道,可采用 [mainWindow].webContents.mainFrame.ipc.on
方法监听调用后,将通道传递给渲染进程。具体示例如下:
手动连接分配 MessageChannelMain
const { app, BrowserWindow, MessageChannelMain } = require('electron/main')
const path = require('node:path')
let isConnect = false;
/**
* 创建手动连接
*/
function createConnect(mainWindow, toWindow) {
mainWindow.webContents.mainFrame.ipc.on('connect', (event) => {
if (!isConnect) {
const { port1, port2 } = new MessageChannelMain()
isConnect = true
toWindow.webContents.postMessage('port', null, [port2])
event.sender.postMessage('port', null, [port1])
}
})
}
function createWindow(fileName) {
const mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
mainWindow.loadFile(fileName)
// Open the DevTools.
mainWindow.webContents.openDevTools()
return mainWindow;
}
app.whenReady().then(() => {
const index = createWindow('index.html')
const second = createWindow('index2.html')
createConnect(index, second)
createConnect(second, index)
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
const { contextBridge, ipcRenderer } = require('electron/renderer')
// 发送信息
function send(port) {
return function (message) {
port.postMessage(message)
}
}
// 接收信息
function accept(port) {
return function (callback) {
port.onmessage = (messageEvent) => {
callback(messageEvent.data)
}
}
}
contextBridge.exposeInMainWorld('electronAPI', {
// 手动连接
connect: () => ipcRenderer.send('connect'),
loadPort: () => {
return new Promise(resolve => {
ipcRenderer.once('port', (e) => {
const port = e.ports[0]
resolve({ send: send(port), accept: accept(port) })
})
})
}
})
const inputEl = document.getElementById('input')
const sendEl = document.getElementById('send')
const messageEl = document.getElementById('message')
const connectEl = document.getElementById('connect')
const statusEl = document.getElementById('status')
// 手动连接
connectEl.addEventListener('click', () => {
window.electronAPI.connect()
})
function acceptMessage(event) {
messageEl.innerHTML = event
}
window.electronAPI.loadPort(acceptMessage).then(({ send, accept }) => {
statusEl.innerHTML = '已连接!!!';
// 设置接收信息回调
accept(acceptMessage)
sendEl.addEventListener('click', () => {
send(inputEl.value)
})
})
<!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>index</title>
</head>
<body>
<input type="text" id="input" />
<button id="send">send</button>
<button id="connect">connect</button>
<div>连接状态: <strong id="status">未连接</strong></div>
<div>内容: <strong id="message"></strong></div>
<script src="./renderer.js"></script>
</body>
</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>index2</title>
</head>
<body>
<input type="text" id="input" />
<button id="send">send</button>
<button id="connect">connect</button>
<div>连接状态: <strong id="status">未连接</strong></div>
<div>内容: <strong id="message"></strong></div>
<script src="./renderer.js"></script>
</body>
</html>
Worker进程
在 Electron 应用程序中,Worker 进程可以被看作是一种特殊的进程,它类似于隐藏的窗口的渲染进程。通常情况下,Worker 进程用于执行一些后台任务,以避免阻塞主线程或主渲染进程。
特点:
- 后台执行:Worker 进程可以在后台执行一些计算密集型或长时间运行的任务,而不会影响用户界面的响应性。
- 独立执行:它可以独立于主渲染进程运行,拥有自己的执行环境和资源。
在这个示例中,你的应用程序有一个作为隐藏窗口存在的 Worker 进程。 你希望应用程序页面能够直接与 Worker 进程通信,而不需要通过主进程进行中继,以避免性能开销。
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, contextIsolation: false }
})
await worker.loadFile('worker.html')
// main window 将发送内容给 worker process 同时通过 MessagePort 接收返回值
const mainWindow = new BrowserWindow({
webPreferences: { nodeIntegration: true, contextIsolation: false }
})
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])
// 现在主窗口和工作进程可以直接相互通信,无需经过主进程!
})
})
<script>
const { ipcRenderer } = require('electron')
const doWork = (input) => {
// 一些对CPU要求较高的任务
return input * 2
}
// 我们可能会得到多个 clients, 比如有多个 windows,
// 或者假如 main window 重新加载了.
ipcRenderer.on('new-client', (event) => {
const [ port ] = event.ports
port.onmessage = (event) => {
// 事件数据可以是任何可序列化的对象 (事件甚至可以
// 携带其他 MessagePorts 对象!)
const result = doWork(event.data)
port.postMessage(result)
}
})
</script>
<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>
Worker 进程与效率进程的简单区别:
Worker 进程可以看作是一种特殊的效率进程,主要区别在于:
- 使用场景:
- Worker 进程:通常用于在 Web 或 Electron 等环境中,将计算密集型任务从主线程分离,避免阻塞用户界面,确保界面的流畅性。例如,在浏览器中处理复杂的数学计算、图像处理等。
- 效率进程(一般):涵盖范围更广,可处理多种任务,包括网络请求、文件操作、数据库操作等,旨在提高系统整体性能,不局限于计算密集型任务。
- 通信方式:
- Worker 进程:在 Web 或 Electron 中,主要通过 postMessage 进行通信,遵循特定的 Web 标准。
- 效率进程(一般):通信方式多样,如在 Node.js 中使用 child_process 模块时,可通过 stdout、stdin 或自定义的 IPC 机制进行通信。
总之,Worker 进程是一种专门为解决界面阻塞问题而设计的效率进程,在特定环境下使用特定通信方式;而一般的效率进程可处理更多类型的任务,通信方式更灵活。
回复流
Electron 的内置 IPC 方法只支持两种模式:即发即弃(例如, send),或请求-响应(例如, invoke)。 使用 MessageChannels
,你可以实现一个“响应流”,其中单个请求可以返回一串数据。
const { BrowserWindow, app, ipcMain } = require('electron')
const path = require('node:path')
app.whenReady().then(async () => {
const mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
mainWindow.loadFile('index.html')
// 回复
ipcMain.on('give-me-a-stream', (event, data)=>{
const [port] = event.ports
const { message, count } = data
for(let i = 0; i < count; i++) {
port.postMessage(message)
}
// 回复完毕,关闭通道
port.close()
})
mainWindow.webContents.openDevTools()
})
const { contextBridge, ipcRenderer } = require('electron/renderer')
// 发送请求
function makeStreamingRequest(message, count){
const { port1, port2 } = new MessageChannel()
ipcRenderer.postMessage('give-me-a-stream', { message, count }, [port2])
port1.onmessage = (event) => {
console.log('get message:', event)
}
port1.onclose = () => {
console.log('stream ended')
}
}
contextBridge.exposeInMainWorld('electronAPI', {
makeStreamingRequest: (message, count) => makeStreamingRequest(message, count),
})
const sendEl = document.getElementById('send')
sendEl.addEventListener('click', () => {
window.electronAPI.makeStreamingRequest('got response data:', 10)
})
<!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>request stream</title>
</head>
<body>
<button id="send">send</button>
<script src="./renderer.js"></script>
</body>
</html>
直接在上下文隔离页面的主进程和主世界之间进行通信
主世界(Main World)是指渲染进程中页面的 JavaScript 环境。在 Electron 中,为了保证安全性,默认情况下,渲染进程中的页面 JavaScript 无法直接访问 Node.js 和 Electron 的 API,因为存在上下文隔离机制。
在之前的示例中,我们都是通过 contextBridge
模块来在渲染进程和主进程之间进行通信。 但是,在某些情况下,您可能需要在主世界和主进程之间进行通信。 例如,您可能需要从主进程中获取一些全局配置,然后在渲染进程中使用这些配置。
下面的示例演示了如何在主世界和主进程之间进行通信:
const { BrowserWindow, app, MessageChannelMain } = require('electron')
const path = require('node:path')
app.whenReady().then(async () => {
const bw = new BrowserWindow({
webPreferences: {
// 开启浏览器调试
openDevTools: true,
// 开启上下文隔离
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
}
})
bw.loadFile('index.html')
// 通过通道连接主世界和主进程
const { port1, port2 } = new MessageChannelMain()
// 允许在另一端还没有注册监听器的情况下就通过通道向其发送消息 消息将排队等待,直到有一个监听器注册为止。
port2.postMessage({ test: 21 })
// 我们也可以接收来自渲染器主进程的消息。
port2.on('message', (event) => {
console.log('from renderer main world:', event.data)
})
port2.start()
// 预加载脚本将接收此 IPC 消息并将端口
// 传输到主进程。
bw.webContents.postMessage('main-world-port', null, [port1])
bw.webContents.openDevTools()
})
const { ipcRenderer } = require('electron/renderer')
// 在发送端口之前,我们需要等待主窗口准备好接收消息 我们在预加载时创建此 promise ,以此保证
// 在触发 load 事件之前注册 onload 侦听器。
const windowLoaded = new Promise(resolve => {
window.onload = resolve
})
ipcRenderer.on('main-world-port', async (event) => {
await windowLoaded
// 我们使用 window.postMessage 将端口
// 发送到主进程
window.postMessage('main-world-port', '*', event.ports)
})
window.onmessage = (event) => {
// event.source === window 意味着消息来自预加载脚本
// 而不是来自iframe或其他来源
if (event.source === window && event.data === 'main-world-port') {
const [ port ] = event.ports
// 一旦我们有了这个端口,我们就可以直接与主进程通信
port.onmessage = (event) => {
console.log('from main process:', event.data)
port.postMessage(event.data.test * 2)
}
}
}
<!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>request stream</title>
</head>
<body>
<script src="./renderer.js"></script>
</body>
</html>