跳转至

Electron 中的 MessagePorts

[MessagePort][] 是一种 Web 功能,允许在不同上下文之间传递消息。它类似于 window.postMessage,但运行在不同的通道上。本文档的目标是描述 Electron 如何扩展通道消息模型,并给出一些在应用中使用 MessagePorts 的示例。

这里是一个非常简短的示例,说明 MessagePort 是什么以及它如何工作:

renderer.js (Renderer Process)
// 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])
main.js (Main Process)
// 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()
})

Channel Messaging API 文档是进一步了解 MessagePorts 如何工作的绝佳方式。

主进程中的 MessagePorts

在渲染进程中,MessagePort 类的行为与在 Web 上完全一致。不过,主进程并不是网页——它没有 Blink 集成——因此它没有 MessagePort 或 MessageChannel 类。为了在主进程中处理 MessagePorts 并与它们交互,Electron 添加了两个新类:[MessagePortMain][] 和 [MessageChannelMain][]。它们的行为与渲染进程中对应的类类似。

MessagePort 对象可以在渲染进程或主进程中创建,并使用 [ipcRenderer.postMessage][] 和 [WebContents.postMessage][] 方法来回传递。请注意,常规的 IPC 方法(如 send 和 invoke)不能用于传输 MessagePort,只有 postMessage 方法可以传输 MessagePort。

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

扩展:close 事件

Electron 为 MessagePort 添加了一个 Web 上不存在的功能,以使 MessagePorts 更有用。那就是 close 事件,当通道的另一端被关闭时触发。端口也可能因被垃圾回收而隐式关闭。

在渲染进程中,你可以通过给 port.onclose 赋值,或调用 port.addEventListener('close', ...) 来监听 close 事件。在主进程中,你可以通过调用 port.on('close', ...) 来监听 close 事件。

示例用例

在两个渲染进程之间设置 MessageChannel

在这个示例中,主进程设置一个 MessageChannel,然后将每个端口发送到不同的渲染进程。这使得渲染进程能够相互发送消息,而无需使用主进程作为中间层。

main.js (Main Process)
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])
  })
})

然后,在你的 preload 脚本中,你通过 IPC 接收端口并设置监听器。

```js title='preloadMain.js and preloadSecondary.js (Preload scripts)' @ts-window-type={electronMessagePort:MessagePort} 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 调用,但为了简化这个示例,我们没有这样做。你可以在本页下方找到上下文隔离的示例,位于[在主进程与上下文隔离页面的主世界之间直接通信](#communicating-directly-between-the-main-process-and-the-main-world-of-a-context-isolated-page)

这意味着 window.electronMessagePort 全局可用,你可以从应用中的任何位置调用它的 `postMessage`,向另一个渲染进程发送消息。

```js title='renderer.js (Renderer Process)' @ts-window-type={electronMessagePort:MessagePort}
// elsewhere in your code to send a message to the other renderers message handler
window.electronMessagePort.postMessage('ping')

Worker 进程

在这个示例中,你的应用有一个以隐藏窗口实现的 worker 进程。你希望应用页面能够直接与 worker 进程通信,以避免通过主进程中继带来的性能开销。

main.js (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.
  // Listen for message sent from the top-level frame
  mainWindow.webContents.mainFrame.ipc.on('request-worker-channel', (event) => {
    // 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!
  })
})
worker.html
<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>
app.html
<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>

响应流

Electron 内置的 IPC 方法仅支持两种模式:即发即忘(例如 send)或请求-响应(例如 invoke)。使用 MessageChannels,你可以实现“响应流”,其中单个请求会以数据流的形式返回响应。

```js title='renderer.js (Renderer Process)' @ts-expect-error=[18] 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.

```js  title='main.js (Main Process)'
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 消息会被传递到隔离世界,而不是主世界。有时你希望直接将消息传递到主世界,而不必经过隔离世界。

main.js (Main Process)
const { BrowserWindow, app, MessageChannelMain } = require('electron')

const path = require('node: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])
})
preload.js (Preload Script)
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)
})
index.html
<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.test * 2)
    }
  }
}
</script>

本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 el/electron