Spec-Zone.ru › Electron

Порты сообщений в Electron

MessagePort — это веб-возможность, которая позволяет передавать сообщения между разными контекстами. Это как window.postMessage, но на разных каналах. Цель данного документа — описать, как Electron расширяет модель обмена сообщениями по каналам, и привести примеры использования портов сообщений в вашем приложении.

Вот очень краткий пример того, что такое MessagePort и как он работает:

renderer.js (Процесс визуализации)
// 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 (Главный процесс)
// 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.

Порты сообщений в главном процессе​

В процессе визуализации класс MessagePort ведет себя точно так же, как и в веб-приложении. Однако главный процесс — это не веб-страница; он не имеет интеграции с Blink, поэтому у него нет классов MessagePort или MessageChannel. Для обработки и взаимодействия с MessagePorts в главном процессе Electron добавляет два новых класса: MessagePortMain и MessageChannelMain. Они ведут себя аналогично соответствующим классам в процессе визуализации.

Объекты MessagePort можно создавать как в процессе визуализации, так и в главном процессе и передавать друг другу с помощью методов ipcRenderer.postMessage и WebContents.postMessage. Обратите внимание, что обычные методы IPC, такие как send и invoke, нельзя использовать для передачи MessagePort , только методы postMessage могут передавать MessagePort.

Передача MessagePort через главный процесс позволяет соединить две страницы, которые в противном случае не могли бы обмениваться данными (например, из-за ограничений на происхождение).

Событие расширения: событие close​

Electron добавляет в MessagePort одну функцию, которая отсутствует в веб-приложениях, чтобы сделать MessagePorts более полезными. Это событие close, которое срабатывает, когда другой конец канала закрывается. Порты также могут быть неявным образом закрыты при сборе мусора.

В процессе визуализации можно прослушивать событие close, присвоив его port.onclose или вызвав port.addEventListener('close', ...). В главном процессе вы можете прослушивать событие close, вызвав port.on('close', ...).

Примеры использования​

Настройка MessageChannel между двумя процессами визуализации​

В этом примере главный процесс создаёт MessageChannel, а затем отправляет каждый порт в разные процессы визуализации. Это позволяет процессам визуализации отправлять сообщения друг другу без необходимости использования главного процесса в качестве посредника.

main.js (Главный процесс)
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])
  })
})

Затем в ваших скриптах предварительной загрузки вы получаете порт через IPC и настраиваете обработчики.

preloadMain.js и preloadSecondary.js (Скрипты предварительной загрузки)
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 и настроить контекстные вызовы bridge для каждого ожидаемого сообщения, но для простоты этого примера мы этого не делаем. Вы можете найти пример изоляции контекста далее на этой странице по адресу Общение напрямую между главным процессом и основным миром изолированной страницы.

Это означает, что window.messagePort глобально доступен, и вы можете вызвать postMessage на нём из любой точки приложения, чтобы отправить сообщение другому процессу визуализации.

renderer.js (Процесс визуализации)
// elsewhere in your code to send a message to the other renderers message handler
window.electronMessagePort.postmessage('ping')

Процесс-воркер​

В этом примере в вашем приложении реализован процесс-воркер в виде скрытого окна. Вы хотите, чтобы страница приложения могла общаться напрямую с процессом-воркером, без накладных расходов на ретрансляцию через главный процесс.

main.js (Главный процесс)
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!
    }
  })
})
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 вы можете реализовать «поток ответов», где на один запрос возвращается поток данных.

renderer.js (Процесс визуализации)
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.
main.js (Главный процесс)
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()
})

Общение напрямую между главным процессом и основным миром изолированной страницы​

Когда изоляция контекста включена, сообщения IPC из главного процесса в процесс визуализации доставляются в изолированный мир, а не в основной. Иногда вам нужно доставлять сообщения напрямую в основной мир, не проходя через изолированный.

main.js (Главный процесс)
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])
})
preload.js (Скрипт предварительной загрузки)
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 * 2)
    }
  }
}
</script>

© GitHub Inc.
Licensed under the MIT license.
https://www.electronjs.org/docs/latest/tutorial/message-ports

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API