MessagePorts in ElectronElectron中的消息端口
MessagePorts are a web feature that allow passing messages between different contexts. 是一种允许在不同上下文之间传递消息的web功能。It's like 这就像windowpostMessage,但在不同的频道上。window.postMessage
, but on different channels. The goal of this document is to describe how Electron extends the Channel Messaging model, and to give some examples of how you might use MessagePorts in your app.本文档的目标是描述Electron如何扩展信道消息传递模型,并给出一些示例,说明如何在应用程序中使用消息端口。
Here is a very brief example of what a MessagePort is and how it works:下面是一个非常简单的示例,说明MessagePort是什么以及它如何工作:
// MessagePorts are created in pairs. A connected pair of message ports is
// called a channel.
const channel = new MessageChannel()
// The only difference between port1 and port2 is in how you use them. Messages
// sent to port1 will be received by port2 and vice-versa.
const port1 = channel.port1
const port2 = channel.port2
// It's OK to send a message on the channel before the other end has registered
// a listener. Messages will be queued until a listener is registered.
port2.postMessage({ answer: 42 })
// Here we send the other end of the channel, port1, to the main process. It's
// also possible to send MessagePorts to other frames, or to Web Workers, etc.
ipcRenderer.postMessage('port', null, [port1])
// In the main process, we receive the port.
ipcMain.on('port', (event) => {
// When we receive a MessagePort in the main process, it becomes a
// MessagePortMain.
const port = event.ports[0]
// MessagePortMain uses the Node.js-style events API, rather than the
// web-style events API. So .on('message', ...) instead of .onmessage = ...
port.on('message', (event) => {
// data is { answer: 42 }
const data = event.data
})
// MessagePortMain queues messages until the .start() method has been called.
port.start()
})
The Channel Messaging API documentation is a great way to learn more about how MessagePorts work.通道消息传递API文档是了解更多消息端口工作方式的好方法。
MessagePorts in the main process主进程中的消息端口
In the renderer, the 在渲染器中,MessagePort
class behaves exactly as it does on the web. MessagePort
类的行为与它在web上的行为完全相同。The main process is not a web page, though—it has no Blink integration—and so it does not have the 主进程不是web页面,但它没有Blink集成,因此它没有MessagePort
or MessageChannel
classes. MessagePort
或MessageChannel
类。In order to handle and interact with MessagePorts in the main process, Electron adds two new classes: MessagePortMain and MessageChannelMain. 为了在主进程中处理和交互MessagePorts,Electron添加了两个新类:MessagePortMain和MessageChannelMain。These behave similarly to the analogous classes in the renderer.它们的行为类似于渲染器中的类似类。
MessagePort
objects can be created in either the renderer or the main process, and passed back and forth using the ipcRenderer.postMessage and WebContents.postMessage methods. MessagePort
对象可以在渲染器或主进程中创建,并使用ipcRenderer.postMessage和WebContents.postMessage方法来回传递。Note that the usual IPC methods like 请注意,通常的IPC方法(如send
and invoke
cannot be used to transfer MessagePort
s, only the postMessage
methods can transfer MessagePort
s.send
和invoke
)不能用于传输MessagePort
,只有postMessage
方法可以传输MessagePort
。
By passing 通过主进程传递MessagePort
s via the main process, you can connect two pages that might not otherwise be able to communicate (e.g. due to same-origin restrictions).MessagePort
,您可以连接两个可能无法通信的页面(例如,由于相同的源限制)。
Extension: close
event扩展:close
事件
close
eventElectron adds one feature to Electron为MessagePort
that isn't present on the web, in order to make MessagePorts more useful. MessagePort
添加了一个web上没有的功能,以使MessagePort
更有用。That is the 即close
event, which is emitted when the other end of the channel is closed. close
事件,当通道的另一端关闭时发出。Ports can also be implicitly closed by being garbage-collected.端口也可以通过垃圾收集隐式关闭。
In the renderer, you can listen for the 在渲染器中,您可以通过分配给close
event either by assigning to port.onclose
or by calling port.addEventListener('close', ...)
. port.onclose
或调用port.addEventListener('close', ...)
来侦听close
事件。In the main process, you can listen for the 在主进程中,您可以通过调用close
event by calling port.on('close', ...)
.port.on('close', ...)
来监听close
事件。
Example use cases示例用例
Setting up a MessageChannel between two renderers在两个渲染器之间设置消息通道
In this example, the main process sets up a MessageChannel, then sends each port to a different renderer. 在本例中,主进程设置MessageChannel
,然后将每个端口发送到不同的渲染器。This allows renderers to send messages to each other without needing to use the main process as an in-between.这允许渲染器相互发送消息,而不需要使用主进程作为中间进程。
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 = 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])
})
})
Then, in your preload scripts you receive the port through IPC and set up the listeners.然后,在预加载脚本中,通过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
}
})
In this example messagePort is bound to the 在本例中,window
object directly. messagePort
直接绑定到window
对象。It is better to use 最好使用contextIsolation
and set up specific contextBridge calls for each of your expected messages, but for the simplicity of this example we don't. contextIsolation
并为每个预期消息设置特定的contextBridge
调用,但为了简单起见,我们不这样做。You can find an example of context isolation further down this page at Communicating directly between the main process and the main world of a context-isolated page您可以在本页面的下一页找到上下文隔离的示例,即在上下文隔离页面的主进程和主世界之间直接通信。
That means window.messagePort is globally available and you can call 这意味着window.messagePort是全局可用的,您可以从应用程序中的任何位置调用它的postMessage
on it from anywhere in your app to send a message to the other renderer.postMessage
,将消息发送到其他渲染器。
// elsewhere in your code to send a message to the other renderers message handler
window.electronMessagePort.postmessage('ping')
Worker process
In this example, your app has a worker process implemented as a hidden window. 在本例中,您的应用程序有一个实现为隐藏窗口的工作进程。You want the app page to be able to communicate directly with the worker process, without the performance overhead of relaying via the main process.您希望应用程序页面能够直接与工作进程通信,而无需通过主进程进行中继的性能开销。
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')
// The main window will send work to the worker process and receive results
// over a MessagePort.
const mainWindow = new BrowserWindow({
webPreferences: { nodeIntegration: true }
})
mainWindow.loadFile('app.html')
// We can't use ipcMain.handle() here, because the reply needs to transfer a
// MessagePort.
ipcMain.on('request-worker-channel', (event) => {
// For security reasons, let's make sure only the frames we expect can
// access the worker.
if (event.senderFrame === mainWindow.webContents.mainFrame) {
// Create a new channel ...
const { port1, port2 } = new MessageChannelMain()
// ... send one end to the worker ...
worker.webContents.postMessage('new-client', null, [port1])
// ... and the other end to the main window.
event.senderFrame.postMessage('provide-worker-channel', null, [port2])
// Now the main window and the worker can communicate with each other
// without going through the main process!
}
})
})
<script>
const { ipcRenderer } = require('electron')
const doWork = (input) => {
// Something cpu-intensive.
return input * 2
}
// We might get multiple clients, for instance if there are multiple windows,
// or if the main window reloads.
ipcRenderer.on('new-client', (event) => {
const [ port ] = event.ports
port.onmessage = (event) => {
// The event data can be any serializable object (and the event could even
// carry other MessagePorts with it!)
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) => {
// Once we receive the reply, we can take the port...
const [ port ] = event.ports
// ... register a handler to receive results ...
port.onmessage = (event) => {
console.log('received result:', event.data)
}
// ... and start sending it work!
port.postMessage(21)
})
</script>
Reply streams回复流
Electron's built-in IPC methods only support two modes: fire-and-forget (e.g. Electron的内置IPC方法仅支持两种模式:触发和忘记(如send
), or request-response (e.g. invoke
). send
)或请求-响应(如invoke
)。Using MessageChannels, you can implement a "response stream", where a single request responds with a stream of data.使用MessageChannel
,您可以实现“响应流”,其中单个请求用数据流进行响应。
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:', event.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()
})
Communicating directly between the main process and the main world of a context-isolated page在上下文隔离页面的主进程和主世界之间直接通信
When context isolation is enabled, IPC messages from the main process to the renderer are delivered to the isolated world, rather than to the main world. 启用上下文隔离后,从主进程到渲染器的IPC消息将传递到隔离世界,而不是主世界。Sometimes you want to deliver messages to the main world directly, without having to step through the isolated world.有时候,你想直接向主世界传递信息,而不必穿过孤立的世界。
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()
// It's OK to send a message on the channel before the other end has
// registered a listener. Messages will be queued until a listener is
// registered.
port2.postMessage({ test: 21 })
// We can also receive messages from the main world of the renderer.
port2.on('message', (event) => {
console.log('from renderer main world:', event.data)
})
port2.start()
// The preload script will receive this IPC message and transfer the port
// over to the main world.
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. We create this promise in the preload so it's guaranteed
// to register the onload listener before the load event is fired.
const windowLoaded = new Promise(resolve => {
window.onload = resolve
})
ipcRenderer.on('main-world-port', async (event) => {
await windowLoaded
// We use regular window.postMessage to transfer the port from the isolated
// world to the main world.
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
// Once we have the port, we can communicate directly with the main
// process.
port.onmessage = (event) => {
console.log('from main process:', event.data)
port.postMessage(event.data * 2)
}
}
}
</script>