Skip to content

Electron: Processes Communication #31

@xwcoder

Description

@xwcoder

Electron: Processes Communication

There are two methods for processes communication, IPC (Inter-Process Communication) and MessagePort

IPC

The node:net module supports IPC with named pipes on Windows, and Unix domain sockets on other operating systems.

In Electron, processes communicate by passing messages through developer-defined channels with the ipcMain and ipcRenderer modules.

Pattern 1: Renderer to main (one-way)

  • Use the ipcRenderer.send API to send messages in preload scripts.

    // preload.js
    const { contextBridge, ipcRenderer } = require('electron')
    
    contextBridge.exposeInMainWorld('electronAPI', {
        setTitle: (title) => ipcRenderer.send('set-title', title)
    })
    // renderer.js
    const setButton = document.getElementById('btn')
    setButton.addEventListener('click', () => {
        window.electronAPI.setTitle('hello world')
    });
  • Use the ipcMain.on API to receive messages in main process.

    // main.js
    const {app, BrowserWindow, ipcMain} = require('electron')
    const path = require('path')
    
    function handleSetTitle (event, title) {
      const webContents = event.sender
      const win = BrowserWindow.fromWebContents(webContents)
      win.setTitle(title)
    }
    
    function createWindow () {
      const mainWindow = new BrowserWindow({
        webPreferences: {
          preload: path.join(__dirname, 'preload.js')
        }
      })
      mainWindow.loadFile('index.html')
    }
    
    app.whenReady().then(() => {
      ipcMain.on('set-title', handleSetTitle)
      createWindow()
    }

Pattern 2: Renderer to main (two-way)

Sending a message to the main process and waiting for a result.

  • Use the ipcRenderer.invoke API to send messages to the main process in preload scripts.

    // preload.js
    const { contextBridge, ipcRenderer } = require('electron')
    
    contextBridge.exposeInMainWorld('electronAPI', {
      openFile: () => ipcRenderer.invoke('dialog:openFile')
    })
    // renderer.js
    const btn = document.getElementById('btn')
    const filePathElement = document.getElementById('filePath')
    
    btn.addEventListener('click', async () => {
      const filePath = await window.electronAPI.openFile()
      filePathElement.innerText = filePath
    })
  • Use the ipcMain.handle API to receive messages and return result, the return result will be wrapped in a Promise.

    const { BrowserWindow, dialog, ipcMain } = require('electron')
    const path = require('path')
    
    async function handleFileOpen() {
      const { canceled, filePaths } = await dialog.showOpenDialog()
      return canceled ? undefined : filePaths[0]
    }
    
    function createWindow () {
      const mainWindow = new BrowserWindow({
        webPreferences: {
          preload: path.join(__dirname, 'preload.js')
        }
      })
      mainWindow.loadFile('index.html')
    }
    
    app.whenReady(() => {
      ipcMain.handle('dialog:openFile', handleFileOpen)
      createWindow()
    })

There is a synchronously api called ipcRenderer.sendSync, which will block the renderer process until a reply is received.

Pattern 3: Main to renderer

  • Use the WebContents.send API to send messages to the renderer process, like the way using ipcRenderer.send.

    // main.js
    const {app, BrowserWindow, Menu, ipcMain} = require('electron')
    const path = require('path')
    
    function createWindow () {
      const mainWindow = new BrowserWindow({
        webPreferences: {
          preload: path.join(__dirname, 'preload.js')
        }
      })
    
      const menu = Menu.buildFromTemplate([
        {
          label: app.name,
          submenu: [
            {
              click: () => mainWindow.webContents.send('update-counter', 1),
              label: 'Increment',
            },
            {
              click: () => mainWindow.webContents.send('update-counter', -1),
              label: 'Decrement',
            }
          ]
        }
      ])
    
      Menu.setApplicationMenu(menu)
    
      mainWindow.loadFile('index.html')
    }
    
    ipcMain.on('counter-value', (_event, value) => {
      console.log(value) // will print value to Node console
    })
  • Use the ipcRenderer.on API to receive messages in preload scripts.

    // preload.js
    const { contextBridge, ipcRenderer } = require('electron')
    
    contextBridge.exposeInMainWorld('electronAPI', {
        onUpdateCounter: (callback) => ipcRenderer.on('update-counter', callback)
    })
    // renderer.js
    const counter = document.getElementById('counter')
    
    window.electronAPI.onUpdateCounter((event, value) => {
      const oldValue = Number(counter.innerText)
      const newValue = oldValue + value
      counter.innerText = newValue
      // return a replay
      event.sender.send('counter-value', newValue)
    })

Pattern 4: Renderer to renderer

There is no direct way to send messages between renderer processes using ipcMain and ipcRenderer modules.

  • Use the main process as a message broker.
  • Pass a MessagePort from the main process to both renderers.

MessagePort

  • In the renderer, the MessagePort behaves exactly as it does on the web.
  • In the main, Electron adds two new classes: MessagePortMain and MessageChannelMain.
  • MessagePort objects can be passed using ipcRenderer.postMessage and WebContents.postMessage.
  • The renderer page can communicate with the main process directly with MessagePort, don’t rely on preload scripts as a broker.

Example: Setting up a MessageChannel between two renderers

// 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])
  })
})
// preloadMain.js and preloadSecondary.js (Preload scripts)
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
  }
})

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions