Electron Electron 中的消息端口

2023-02-16 17:15 更新

?MessagePort?是一個允許在不同上下文之間傳遞消息的Web功能。 就像 ?window.postMessage?, 但是在不同的通道上。 此文檔的目標是描述 Electron 如何擴展 Channel Messaging model ,并舉例說明如何在應(yīng)用中使用 MessagePorts

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

// MessagePorts are created in pairs. 連接的一對消息端口
// 被稱為通道。
const channel = new MessageChannel()

// port1 和 port2 之間唯一的不同是你如何使用它們。 消息
// 發(fā)送到port1 將被port2 接收,反之亦然。
const port1 = channel.port1
const port2 = channel.port2

// 允許在另一端還沒有注冊監(jiān)聽器的情況下就通過通道向其發(fā)送消息
// 消息將排隊等待,直到一個監(jiān)聽器注冊為止。
port2.postMessage({ answer: 42 })

// 這次我們通過 ipc 向主進程發(fā)送 port1 對象。 類似的,
// 我們也可以發(fā)送 MessagePorts 到其他 frames, 或發(fā)送到 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) => {
    // 收到的數(shù)據(jù)是: { answer: 42 }
    const data = event.data
  })

  // MessagePortMain 阻塞消息直到 .start() 方法被調(diào)用
  port.start()
})

關(guān)于 channel 消息接口的使用文檔詳見 Channel Messaging API

主進程中的 MessagePorts

在渲染器中, MessagePort 類的行為與它在 web 上的行為完全一樣。 但是,主進程不是網(wǎng)頁(它沒有 Blink 集成),因此它沒有 MessagePort 或 MessageChannel 類。 為了在主進程中處理 MessagePorts 并與之交互,Electron 添加了兩個新類: MessagePortMain 和 MessageChannelMain。 這些行為 類似于渲染器中 analogous 類。

MessagePort 對象可以在渲染器或主 進程中創(chuàng)建,并使用 ipcRenderer.postMessage 和 WebContents.postMessage 方法互相傳遞。 請注意,通常的 IPC 方法,例如 send 和 invoke 不能用來傳輸 MessagePort, 只有 postMessage 方法可以傳輸 MessagePort

通過主進程傳遞 MessagePort,就可以連接兩個可能無法通信的頁面 (例如,由于同源限制) 。

擴展: close 事件

Electron 向 MessagePort 添加了一項 Web 上不存在的功能,以使 MessagePorts 更有用。那就是關(guān)閉事件,當通道的另一端關(guān)閉時發(fā)出。端口也可以通過垃圾收集隱式關(guān)閉。在渲染器中,您可以通過分配給 port.onclose 或調(diào)用 port.addEventListener('close', ...) 來監(jiān)聽關(guān)閉事件。在主進程中,您可以通過調(diào)用 port.on('close', ...) 來監(jiān)聽關(guān)閉事件。

實例使用

在兩個渲染器之間設(shè)置 MessageChannel

在此示例中,主進程設(shè)置了一個 MessageChannel,然后將每個端口發(fā)送到不同的渲染器。這允許渲染器相互發(fā)送消息,而無需將主進程用作中間進程。

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])
  })
})

然后,在您的預(yù)加載腳本中,您通過 IPC 接收端口并設(shè)置偵聽器。

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 并為每個預(yù)期消息設(shè)置特定的 contextBridge 調(diào)用,但為了本示例的簡單性,我們不這樣做。

這意味著 window.electronMessagePort 是全局可用的,您可以從應(yīng)用程序的任何位置調(diào)用它的 postMessage 以向其他渲染器發(fā)送消息。

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

Worker進程

在此示例中,您的應(yīng)用程序有一個作為隱藏窗口實現(xiàn)的工作進程。您希望應(yīng)用程序頁面能夠直接與工作進程通信,而沒有通過主進程進行中繼的性能開銷。

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 將發(fā)送內(nèi)容給 worker process 同時通過 MessagePort 接收返回值
  const mainWindow = new BrowserWindow({
    webPreferences: { nodeIntegration: true }
  })
  mainWindow.loadFile('app.html')

  // 在這里我們不能使用 ipcMain.handle() , 因為回復(fù)需要傳輸
  // MessagePort.
  ipcMain.on('request-worker-channel', (event) => {
    // 出于安全考慮, 讓我們確保只要我們期望的 frames
    // 可以訪問 worker.
    if (event.senderFrame === mainWindow.webContents.mainFrame) {
      // 創(chuàng)建新的通道 ...
      const { port1, port2 } = new MessageChannelMain()
      // ... 把一端送到 worker ...
      worker.webContents.postMessage('new-client', null, [port1])
      // ... 同時把另一端送到 main window.
      event.senderFrame.postMessage('provide-worker-channel', null, [port2])
      // 現(xiàn)在 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) => {
    // 事件數(shù)據(jù)可以是任何可序列化的對象 (事件甚至可以
    // 攜帶其他 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) => {
  // 一旦收到回復(fù), 我們可以這樣做...
  const [ port ] = event.ports
  // ... 注冊一個接收結(jié)果處理器 ...
  port.onmessage = (event) => {
    console.log('received result:', event.data)
  }
  // ... 并開始發(fā)送消息給 work!
  port.postMessage(21)
})
</script>

回復(fù)流

Electron 的內(nèi)置 IPC 方法僅支持兩種模式:即發(fā)即棄(例如發(fā)送)或請求-響應(yīng)(例如調(diào)用)。使用 MessageChannels,您可以實現(xiàn)“響應(yīng)流”,其中單個請求以數(shù)據(jù)流響應(yīng)。

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 消息從主進程發(fā)送到渲染器是發(fā)送到隔離的世界,而不是發(fā)送到主世界。 有時候你希望不通過隔離的世界,直接向主世界發(fā)送消息。

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()

// 允許在另一端還沒有注冊監(jiān)聽器的情況下就通過通道向其發(fā)送消息 消息將排隊等待,直到有一個監(jiān)聽器注冊為止。
  port2.postMessage({ test: 21 })

  // 我們也可以接收來自渲染器主進程的消息。
  port2.on('message', (event) => {
    console.log('from renderer main world:', event.data)
  })
  port2.start()
  // 預(yù)加載腳本將接收此 IPC 消息并將端口
  // 傳輸?shù)街鬟M程。
  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. 我們在預(yù)加載時創(chuàng)建此 promise ,以此保證
// 在觸發(fā) load 事件之前注冊 onload 偵聽器。
const windowLoaded = new Promise(resolve => {
  window.onload = resolve
})

ipcRenderer.on('main-world-port', async (event) => {
  await windowLoaded
  // 我們使用 window.postMessage 將端口
  // 發(fā)送到主進程
  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>


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號