MessagePorts in Electron

    Here is a very brief example of what a MessagePort is and how it works:

    1. // main.js ///////////////////////////////////////////////////////////////////
    2. // In the main process, we receive the port.
    3. ipcMain.on('port', (event) => {
    4. // When we receive a MessagePort in the main process, it becomes a
    5. // MessagePortMain.
    6. const port = event.ports[0]
    7. // MessagePortMain uses the Node.js-style events API, rather than the
    8. // web-style events API. So .on('message', ...) instead of .onmessage = ...
    9. port.on('message', (event) => {
    10. // data is { answer: 42 }
    11. const data = event.data
    12. })
    13. // MessagePortMain queues messages until the .start() method has been called.
    14. port.start()
    15. })

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

    MessagePort objects can be created in either the renderer or the main process, and passed back and forth using the 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).

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

    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. ipcMain.on('request-worker-channel', (event) => {
    19. // For security reasons, let's make sure only the frames we expect can
    20. // access the worker.
    21. if (event.senderFrame === mainWindow.webContents.mainFrame) {
    22. // Create a new channel ...
    23. const { port1, port2 } = new MessageChannelMain()
    24. // ... send one end to the worker ...
    25. // ... and the other end to the main window.
    26. event.senderFrame.postMessage('provide-worker-channel', null, [port2])
    27. // Now the main window and the worker can communicate with each other
    28. // without going through the main process!
    29. }
    30. })
    31. })
    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>
    1. // renderer.js ///////////////////////////////////////////////////////////////
    2. const 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. }
    19. }
    20. makeStreamingRequest(42, (data) => {
    21. })
    22. // We will see "got response data: 42" 10 times.

    When 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. })