Electron 中的消息端口

MessagePort是一个允许在不同上下文之间传递消息的Web功能。 就像 window.postMessage, 但是在不同的通道上。 此文档的目标是描述 Electron 如何扩展 Channel Messaging model ,并举例说明如何在应用中使用 MessagePorts

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

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

The Channel Messaging API documentation is a great way to learn more about how MessagePorts work.

MessagePorts in the main process

In the renderer, the MessagePort class behaves exactly as it does on the web. The main process is not a web page, though—it has no Blink integration—and so it does not have the MessagePort or MessageChannel classes. In order to handle and interact with MessagePorts in the main process, Electron adds two new classes: MessagePortMain and 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. Note that the usual IPC methods like send and invoke cannot be used to transfer MessagePorts, only the postMessage methods can transfer MessagePorts.

By passing MessagePorts via the main process, you can connect two pages that might not otherwise be able to communicate (e.g. due to same-origin restrictions).

Extension: close event

Electron adds one feature to MessagePort that isn’t present on the web, in order to make MessagePorts more useful. That is the close event, which is emitted when the other end of the channel is closed. 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', ...). In the main process, you can listen for the close event by calling port.on('close', ...).

Example use cases

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.

  1. // main.js ///////////////////////////////////////////////////////////////////
  2. const { BrowserWindow, app, ipcMain, MessageChannelMain } = require('electron')
  3. app.whenReady().then(async () => {
  4. // The worker process is a hidden BrowserWindow, so that it will have access
  5. // to a full Blink context (including e.g. <canvas>, audio, fetch(), etc.)
  6. const worker = new BrowserWindow({
  7. show: false,
  8. webPreferences: { nodeIntegration: true }
  9. })
  10. await worker.loadFile('worker.html')
  11. // The main window will send work to the worker process and receive results
  12. // over a MessagePort.
  13. const mainWindow = new BrowserWindow({
  14. webPreferences: { nodeIntegration: true }
  15. })
  16. mainWindow.loadFile('app.html')
  17. // We can't use ipcMain.handle() here, because the reply needs to transfer a
  18. // MessagePort.
  19. ipcMain.on('request-worker-channel', (event) => {
  20. // For security reasons, let's make sure only the frames we expect can
  21. // access the worker.
  22. if (event.senderFrame === mainWindow.webContents.mainFrame) {
  23. // Create a new channel ...
  24. const { port1, port2 } = new MessageChannelMain()
  25. // ... send one end to the worker ...
  26. worker.webContents.postMessage('new-client', null, [port1])
  27. // ... and the other end to the main window.
  28. event.senderFrame.postMessage('provide-worker-channel', null, [port2])
  29. // Now the main window and the worker can communicate with each other
  30. // without going through the main process!
  31. }
  32. })
  33. })
  1. <!-- worker.html ------------------------------------------------------------>
  2. <script>
  3. const { ipcRenderer } = require('electron')
  4. function doWork(input) {
  5. // Something cpu-intensive.
  6. return input * 2
  7. }
  8. // We might get multiple clients, for instance if there are multiple windows,
  9. // or if the main window reloads.
  10. ipcRenderer.on('new-client', (event) => {
  11. const [ port ] = event.ports
  12. port.onmessage = (event) => {
  13. // The event data can be any serializable object (and the event could even
  14. // carry other MessagePorts with it!)
  15. const result = doWork(event.data)
  16. port.postMessage(result)
  17. }
  18. })
  19. </script>
  1. <!-- app.html --------------------------------------------------------------->
  2. <script>
  3. const { ipcRenderer } = require('electron')
  4. // We request that the main process sends us a channel we can use to
  5. // communicate with the worker.
  6. ipcRenderer.send('request-worker-channel')
  7. ipcRenderer.once('provide-worker-channel', (event) => {
  8. // Once we receive the reply, we can take the port...
  9. const [ port ] = event.ports
  10. // ... register a handler to receive results ...
  11. port.onmessage = (event) => {
  12. console.log('received result:', event.data)
  13. }
  14. // ... and start sending it work!
  15. port.postMessage(21)
  16. })
  17. </script>

Reply streams

Electron’s built-in IPC methods only support two modes: fire-and-forget (e.g. send), or request-response (e.g. invoke). Using MessageChannels, you can implement a “response stream”, where a single request responds with a stream of data.

  1. // renderer.js ///////////////////////////////////////////////////////////////
  2. function makeStreamingRequest (element, callback) {
  3. // MessageChannels are lightweight--it's cheap to create a new one for each
  4. // request.
  5. const { port1, port2 } = new MessageChannel()
  6. // We send one end of the port to the main process ...
  7. ipcRenderer.postMessage(
  8. 'give-me-a-stream',
  9. { element, count: 10 },
  10. [port2]
  11. )
  12. // ... and we hang on to the other end. The main process will send messages
  13. // to its end of the port, and close it when it's finished.
  14. port1.onmessage = (event) => {
  15. callback(event.data)
  16. }
  17. port1.onclose = () => {
  18. console.log('stream ended')
  19. }
  20. }
  21. makeStreamingRequest(42, (data) => {
  22. console.log('got response data:', event.data)
  23. })
  24. // We will see "got response data: 42" 10 times.
  1. // main.js ///////////////////////////////////////////////////////////////////
  2. ipcMain.on('give-me-a-stream', (event, msg) => {
  3. // The renderer has sent us a MessagePort that it wants us to send our
  4. // response over.
  5. const [replyPort] = event.ports
  6. // Here we send the messages synchronously, but we could just as easily store
  7. // the port somewhere and send messages asynchronously.
  8. for (let i = 0; i < msg.count; i++) {
  9. replyPort.postMessage(msg.element)
  10. }
  11. // We close the port when we're done to indicate to the other end that we
  12. // won't be sending any more messages. This isn't strictly necessary--if we
  13. // didn't explicitly close the port, it would eventually be garbage
  14. // collected, which would also trigger the 'close' event in the renderer.
  15. replyPort.close()
  16. })

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. Sometimes you want to deliver messages to the main world directly, without having to step through the isolated world.

  1. // main.js ///////////////////////////////////////////////////////////////////
  2. const { BrowserWindow, app, MessageChannelMain } = require('electron')
  3. const path = require('path')
  4. app.whenReady().then(async () => {
  5. // Create a BrowserWindow with contextIsolation enabled.
  6. const bw = new BrowserWindow({
  7. webPreferences: {
  8. contextIsolation: true,
  9. preload: path.join(__dirname, 'preload.js')
  10. }
  11. })
  12. bw.loadURL('index.html')
  13. // We'll be sending one end of this channel to the main world of the
  14. // context-isolated page.
  15. const { port1, port2 } = new MessageChannelMain()
  16. // It's OK to send a message on the channel before the other end has
  17. // registered a listener. Messages will be queued until a listener is
  18. // registered.
  19. port2.postMessage({ test: 21 })
  20. // We can also receive messages from the main world of the renderer.
  21. port2.on('message', (event) => {
  22. console.log('from renderer main world:', event.data)
  23. })
  24. port2.start()
  25. // The preload script will receive this IPC message and transfer the port
  26. // over to the main world.
  27. bw.webContents.postMessage('main-world-port', null, [port1])
  28. })
  1. // preload.js ////////////////////////////////////////////////////////////////
  2. const { ipcRenderer } = require('electron')
  3. // We need to wait until the main world is ready to receive the message before
  4. // sending the port. We create this promise in the preload so it's guaranteed
  5. // to register the onload listener before the load event is fired.
  6. const windowLoaded = new Promise(resolve => {
  7. window.onload = resolve
  8. })
  9. ipcRenderer.on('main-world-port', async (event) => {
  10. await windowLoaded
  11. // We use regular window.postMessage to transfer the port from the isolated
  12. // world to the main world.
  13. window.postMessage('main-world-port', '*', event.ports)
  14. })
  1. <!-- index.html ------------------------------------------------------------->
  2. <script>
  3. window.onmessage = (event) => {
  4. // event.source === window means the message is coming from the preload
  5. // script, as opposed to from an <iframe> or other source.
  6. if (event.source === window && event.data === 'main-world-port') {
  7. const [ port ] = event.ports
  8. // Once we have the port, we can communicate directly with the main
  9. // process.
  10. port.onmessage = (event) => {
  11. console.log('from main process:', event.data)
  12. port.postMessage(event.data * 2)
  13. }
  14. }
  15. }
  16. </script>