Electron 中的消息端口

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

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

renderer.js (Renderer Process)

  1. // MessagePorts are created in pairs. 连接的一对消息端口
  2. // 被称为通道。
  3. const channel = new MessageChannel()
  4. // port1 和 port2 之间唯一的不同是你如何使用它们。 消息
  5. // 发送到port1 将被port2 接收,反之亦然。
  6. const port1 = channel.port1
  7. const port2 = channel.port2
  8. // 允许在另一端还没有注册监听器的情况下就通过通道向其发送消息
  9. // 消息将排队等待,直到一个监听器注册为止。
  10. port2.postMessage({ answer: 42 })
  11. // 这次我们通过 ipc 向主进程发送 port1 对象。 类似的,
  12. // 我们也可以发送 MessagePorts 到其他 frames, 或发送到 Web Workers, 等.
  13. ipcRenderer.postMessage('port', null, [port1])

main.js (Main Process)

  1. // In the main process, we receive the port.
  2. ipcMain.on('port', (event) => {
  3. // 当我们在主进程中接收到 MessagePort 对象, 它就成为了
  4. // MessagePortMain.
  5. const port = event.ports[0]
  6. // MessagePortMain 使用了 Node.js 风格的事件 API, 而不是
  7. // web 风格的事件 API. 因此使用 .on('message', ...) 而不是 .onmessage = ...
  8. port.on('message', (event) => {
  9. // 收到的数据是: { answer: 42 }
  10. const data = event.data
  11. })
  12. // MessagePortMain 阻塞消息直到 .start() 方法被调用
  13. port.start()
  14. })

关于 channel 消息接口的使用文档详见 Channel Messaging API

主进程中的 MessagePorts

在渲染器中, MessagePort 类的行为与它在 web 上的行为完全一样。 但是,主进程不是网页(它没有 Blink 集成),因此它没有 MessagePortMessageChannel 类。 为了在主进程中处理 MessagePorts 并与之交互,Electron 添加了两个新类: MessagePortMainMessageChannelMain。 这些行为 类似于渲染器中 analogous 类。

MessagePort 对象可以在渲染器或主 进程中创建,并使用 ipcRenderer.postMessageWebContents.postMessage 方法互相传递。 请注意,通常的 IPC 方法,例如 sendinvoke 不能用来传输 MessagePort, 只有 postMessage 方法可以传输 MessagePort

通过主进程传递 MessagePort,就可以连接两个可能无法通信的页面 (例如,由于同源限制) 。

扩展: close 事件

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', ...).

实例使用

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. This allows renderers to send messages to each other without needing to use the main process as an in-between.

main.js (Main Process)

  1. const { BrowserWindow, app, MessageChannelMain } = require('electron')
  2. app.whenReady().then(async () => {
  3. // create the windows.
  4. const mainWindow = new BrowserWindow({
  5. show: false,
  6. webPreferences: {
  7. contextIsolation: false,
  8. preload: 'preloadMain.js'
  9. }
  10. })
  11. const secondaryWindow = new BrowserWindow({
  12. show: false,
  13. webPreferences: {
  14. contextIsolation: false,
  15. preload: 'preloadSecondary.js'
  16. }
  17. })
  18. // set up the channel.
  19. const { port1, port2 } = new MessageChannelMain()
  20. // once the webContents are ready, send a port to each webContents with postMessage.
  21. mainWindow.once('ready-to-show', () => {
  22. mainWindow.webContents.postMessage('port', null, [port1])
  23. })
  24. secondaryWindow.once('ready-to-show', () => {
  25. secondaryWindow.webContents.postMessage('port', null, [port2])
  26. })
  27. })

Then, in your preload scripts you receive the port through IPC and set up the listeners.

preloadMain.js and preloadSecondary.js (Preload scripts)

  1. const { ipcRenderer } = require('electron')
  2. ipcRenderer.on('port', e => {
  3. // port received, make it globally available.
  4. window.electronMessagePort = e.ports[0]
  5. window.electronMessagePort.onmessage = messageEvent => {
  6. // handle message
  7. }
  8. })

In this example messagePort is bound to the window object directly. 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. 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.electronMessagePort is globally available and you can call postMessage on it from anywhere in your app to send a message to the other renderer.

renderer.js (Renderer Process)

  1. // elsewhere in your code to send a message to the other renderers message handler
  2. window.electronMessagePort.postmessage('ping')

Worker进程

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.

main.js (Main Process)

  1. const { BrowserWindow, app, ipcMain, MessageChannelMain } = require('electron')
  2. app.whenReady().then(async () => {
  3. // The worker process is a hidden BrowserWindow, so that it will have access
  4. // to a full Blink context (including e.g. <canvas>, audio, fetch(), etc.)
  5. const worker = new BrowserWindow({
  6. show: false,
  7. webPreferences: { nodeIntegration: true }
  8. })
  9. await worker.loadFile('worker.html')
  10. // main window 将发送内容给 worker process 同时通过 MessagePort 接收返回值
  11. const mainWindow = new BrowserWindow({
  12. webPreferences: { nodeIntegration: true }
  13. })
  14. mainWindow.loadFile('app.html')
  15. // 在这里我们不能使用 ipcMain.handle() , 因为回复需要传输
  16. // MessagePort.
  17. // Listen for message sent from the top-level frame
  18. mainWindow.webContents.mainFrame.on('request-worker-channel', (event) => {
  19. // Create a new channel ...
  20. const { port1, port2 } = new MessageChannelMain()
  21. // ... send one end to the worker ...
  22. worker.webContents.postMessage('new-client', null, [port1])
  23. // ... and the other end to the main window.
  24. event.senderFrame.postMessage('provide-worker-channel', null, [port2])
  25. // Now the main window and the worker can communicate with each other
  26. // without going through the main process!
  27. })
  28. })

worker.html

  1. <script>
  2. const { ipcRenderer } = require('electron')
  3. const doWork = (input) => {
  4. // Something cpu-intensive.
  5. return input * 2
  6. }
  7. // 我们可能会得到多个 clients, 比如有多个 windows,
  8. // 或者假如 main window 重新加载了.
  9. ipcRenderer.on('new-client', (event) => {
  10. const [ port ] = event.ports
  11. port.onmessage = (event) => {
  12. // 事件数据可以是任何可序列化的对象 (事件甚至可以
  13. // 携带其他 MessagePorts 对象!)
  14. const result = doWork(event.data)
  15. port.postMessage(result)
  16. }
  17. })
  18. </script>

app.html

  1. <script>
  2. const { ipcRenderer } = require('electron')
  3. // We request that the main process sends us a channel we can use to
  4. // communicate with the worker.
  5. ipcRenderer.send('request-worker-channel')
  6. ipcRenderer.once('provide-worker-channel', (event) => {
  7. // 一旦收到回复, 我们可以这样做...
  8. const [ port ] = event.ports
  9. // ... 注册一个接收结果处理器 ...
  10. port.onmessage = (event) => {
  11. console.log('received result:', event.data)
  12. }
  13. // ... 并开始发送消息给 work!
  14. port.postMessage(21)
  15. })
  16. </script>

回复流

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.

renderer.js (Renderer Process)

  1. const makeStreamingRequest = (element, callback) => {
  2. // MessageChannels are lightweight--it's cheap to create a new one for each
  3. // request.
  4. const { port1, port2 } = new MessageChannel()
  5. // We send one end of the port to the main process ...
  6. ipcRenderer.postMessage(
  7. 'give-me-a-stream',
  8. { element, count: 10 },
  9. [port2]
  10. )
  11. // ... and we hang on to the other end. The main process will send messages
  12. // to its end of the port, and close it when it's finished.
  13. port1.onmessage = (event) => {
  14. callback(event.data)
  15. }
  16. port1.onclose = () => {
  17. console.log('stream ended')
  18. }
  19. }
  20. makeStreamingRequest(42, (data) => {
  21. console.log('got response data:', data)
  22. })
  23. // We will see "got response data: 42" 10 times.

main.js (Main Process)

  1. ipcMain.on('give-me-a-stream', (event, msg) => {
  2. // The renderer has sent us a MessagePort that it wants us to send our
  3. // response over.
  4. const [replyPort] = event.ports
  5. // Here we send the messages synchronously, but we could just as easily store
  6. // the port somewhere and send messages asynchronously.
  7. for (let i = 0; i < msg.count; i++) {
  8. replyPort.postMessage(msg.element)
  9. }
  10. // We close the port when we're done to indicate to the other end that we
  11. // won't be sending any more messages. This isn't strictly necessary--if we
  12. // didn't explicitly close the port, it would eventually be garbage
  13. // collected, which would also trigger the 'close' event in the renderer.
  14. replyPort.close()
  15. })

直接在上下文隔离页面的主进程和主世界之间进行通信

当 [context isolation][] 已启用。 IPC 消息从主进程发送到渲染器是发送到隔离的世界,而不是发送到主世界。 有时候你希望不通过隔离的世界,直接向主世界发送消息。

main.js (Main Process)

  1. const { BrowserWindow, app, MessageChannelMain } = require('electron')
  2. const path = require('path')
  3. app.whenReady().then(async () => {
  4. // Create a BrowserWindow with contextIsolation enabled.
  5. const bw = new BrowserWindow({
  6. webPreferences: {
  7. contextIsolation: true,
  8. preload: path.join(__dirname, 'preload.js')
  9. }
  10. })
  11. bw.loadURL('index.html')
  12. // We'll be sending one end of this channel to the main world of the
  13. // context-isolated page.
  14. const { port1, port2 } = new MessageChannelMain()
  15. // 允许在另一端还没有注册监听器的情况下就通过通道向其发送消息 消息将排队等待,直到有一个监听器注册为止。
  16. port2.postMessage({ test: 21 })
  17. // 我们也可以接收来自渲染器主进程的消息。
  18. port2.on('message', (event) => {
  19. console.log('from renderer main world:', event.data)
  20. })
  21. port2.start()
  22. // 预加载脚本将接收此 IPC 消息并将端口
  23. // 传输到主进程。
  24. bw.webContents.postMessage('main-world-port', null, [port1])
  25. })

preload.js (Preload Script)

  1. const { ipcRenderer } = require('electron')
  2. // We need to wait until the main world is ready to receive the message before
  3. // sending the port. 我们在预加载时创建此 promise ,以此保证
  4. // 在触发 load 事件之前注册 onload 侦听器。
  5. const windowLoaded = new Promise(resolve => {
  6. window.onload = resolve
  7. })
  8. ipcRenderer.on('main-world-port', async (event) => {
  9. await windowLoaded
  10. // 我们使用 window.postMessage 将端口
  11. // 发送到主进程
  12. window.postMessage('main-world-port', '*', event.ports)
  13. })

index.html

  1. <script>
  2. window.onmessage = (event) => {
  3. // event.source === window means the message is coming from the preload
  4. // script, as opposed to from an <iframe> or other source.
  5. if (event.source === window && event.data === 'main-world-port') {
  6. const [ port ] = event.ports
  7. // 一旦我们有了这个端口,我们就可以直接与主进程通信
  8. port.onmessage = (event) => {
  9. console.log('from main process:', event.data)
  10. port.postMessage(event.data * 2)
  11. }
  12. }
  13. }
  14. </script>

[context isolation]: latest/tutorial/context-isolation. md