Electron Electron 中的消息端口
MessagePort
是一个允许在不同上下文之间传递消息的Web功能。 就像 window.postMessage
, 但是在不同的通道上。 此文档的目标是描述 Electron 如何扩展 Channel Messaging model ,并举例说明如何在应用中使用 MessagePorts
下面是 MessagePort 是什么和如何工作的一个非常简短的例子:
// MessagePorts are created in pairs. 连接的一对消息端口
// 被称为通道。
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])
// In the main process, we receive the port.
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()
})
关于 channel 消息接口的使用文档详见 Channel Messaging API
主进程中的 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 上不存在的功能,以使 MessagePorts 更有用。那就是关闭事件,当通道的另一端关闭时发出。端口也可以通过垃圾收集隐式关闭。在渲染器中,您可以通过分配给 port.onclose 或调用 port.addEventListener('close', ...) 来监听关闭事件。在主进程中,您可以通过调用 port.on('close', ...) 来监听关闭事件。
实例使用
在两个渲染器之间设置 MessageChannel
在此示例中,主进程设置了一个 MessageChannel,然后将每个端口发送到不同的渲染器。这允许渲染器相互发送消息,而无需将主进程用作中间进程。
const { BrowserWindow, app, MessageChannelMain } = require('electron')
app.whenReady().then(async () => {
// create the windows.
const mainWindow = new BrowserWindow({
show: false,
webPreferences: {
contextIsolation: false,
preload: 'preloadMain.js'
}
})
const secondaryWindow = new BrowserWindow({
show: false,
webPreferences: {
contextIsolation: false,
preload: 'preloadSecondary.js'
}
})
// set up the channel.
const { port1, port2 } = new MessageChannelMain()
// once the webContents are ready, send a port to each webContents with postMessage.
mainWindow.once('ready-to-show', () => {
mainWindow.webContents.postMessage('port', null, [port1])
})
secondaryWindow.once('ready-to-show', () => {
secondaryWindow.webContents.postMessage('port', null, [port2])
})
})
然后,在您的预加载脚本中,您通过 IPC 接收端口并设置侦听器。
const { ipcRenderer } = require('electron')
ipcRenderer.on('port', e => {
// port received, make it globally available.
window.electronMessagePort = e.ports[0]
window.electronMessagePort.onmessage = messageEvent => {
// handle message
}
})
在此示例中,messagePort 直接绑定到 window 对象。最好使用 contextIsolation 并为每个预期消息设置特定的 contextBridge 调用,但为了本示例的简单性,我们不这样做。
这意味着 window.electronMessagePort 是全局可用的,您可以从应用程序的任何位置调用它的 postMessage 以向其他渲染器发送消息。
// elsewhere in your code to send a message to the other renderers message handler
window.electronMessagePort.postmessage('ping')
Worker进程
在此示例中,您的应用程序有一个作为隐藏窗口实现的工作进程。您希望应用程序页面能够直接与工作进程通信,而没有通过主进程进行中继的性能开销。
const { BrowserWindow, app, ipcMain, MessageChannelMain } = require('electron')
app.whenReady().then(async () => {
// The worker process is a hidden BrowserWindow, so that it will have access
// to a full Blink context (including e.g. <canvas>, audio, fetch(), etc.)
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.
ipcMain.on('request-worker-channel', (event) => {
// 出于安全考虑, 让我们确保只要我们期望的 frames
// 可以访问 worker.
if (event.senderFrame === mainWindow.webContents.mainFrame) {
// 创建新的通道 ...
const { port1, port2 } = new MessageChannelMain()
// ... 把一端送到 worker ...
worker.webContents.postMessage('new-client', null, [port1])
// ... 同时把另一端送到 main window.
event.senderFrame.postMessage('provide-worker-channel', null, [port2])
// 现在 main window 和 worker 可以相互通信
// 且不需要通过 main process 了!
}
})
})
<script>
const { ipcRenderer } = require('electron')
const doWork = (input) => {
// Something cpu-intensive.
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')
// We request that the main process sends us a channel we can use to
// communicate with the 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 方法仅支持两种模式:即发即弃(例如发送)或请求-响应(例如调用)。使用 MessageChannels,您可以实现“响应流”,其中单个请求以数据流响应。
const makeStreamingRequest = (element, callback) => {
// MessageChannels are lightweight--it's cheap to create a new one for each
// request.
const { port1, port2 } = new MessageChannel()
// We send one end of the port to the main process ...
ipcRenderer.postMessage(
'give-me-a-stream',
{ element, count: 10 },
[port2]
)
// ... and we hang on to the other end. The main process will send messages
// to its end of the port, and close it when it's finished.
port1.onmessage = (event) => {
callback(event.data)
}
port1.onclose = () => {
console.log('stream ended')
}
}
makeStreamingRequest(42, (data) => {
console.log('got response data:', data)
})
// We will see "got response data: 42" 10 times.
ipcMain.on('give-me-a-stream', (event, msg) => {
// The renderer has sent us a MessagePort that it wants us to send our
// response over.
const [replyPort] = event.ports
// Here we send the messages synchronously, but we could just as easily store
// the port somewhere and send messages asynchronously.
for (let i = 0; i < msg.count; i++) {
replyPort.postMessage(msg.element)
}
// We close the port when we're done to indicate to the other end that we
// won't be sending any more messages. This isn't strictly necessary--if we
// didn't explicitly close the port, it would eventually be garbage
// collected, which would also trigger the 'close' event in the renderer.
replyPort.close()
})
直接在上下文隔离页面的主进程和主世界之间进行通信
当 context isolation 已启用。 IPC 消息从主进程发送到渲染器是发送到隔离的世界,而不是发送到主世界。 有时候你希望不通过隔离的世界,直接向主世界发送消息。
const { BrowserWindow, app, MessageChannelMain } = require('electron')
const path = require('path')
app.whenReady().then(async () => {
// Create a BrowserWindow with contextIsolation enabled.
const bw = new BrowserWindow({
webPreferences: {
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
}
})
bw.loadURL('index.html')
// We'll be sending one end of this channel to the main world of the
// context-isolated page.
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])
})
const { ipcRenderer } = require('electron')
// We need to wait until the main world is ready to receive the message before
// sending the port. 我们在预加载时创建此 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)
})
<script>
window.onmessage = (event) => {
// event.source === window means the message is coming from the preload
// script, as opposed to from an <iframe> or other source.
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 * 2)
}
}
}
</script>