e100402b13
The `counter` channel should be `update-counter` channel
571 lines
20 KiB
Markdown
571 lines
20 KiB
Markdown
---
|
|
title: Inter-Process Communication
|
|
description: Use the ipcMain and ipcRenderer modules to communicate between Electron processes
|
|
slug: ipc
|
|
hide_title: false
|
|
---
|
|
|
|
# Inter-Process Communication
|
|
|
|
Inter-process communication (IPC) is a key part of building feature-rich desktop applications
|
|
in Electron. Because the main and renderer processes have different responsibilities in
|
|
Electron's process model, IPC is the only way to perform many common tasks, such as calling a
|
|
native API from your UI or triggering changes in your web contents from native menus.
|
|
|
|
## IPC channels
|
|
|
|
In Electron, processes communicate by passing messages through developer-defined "channels"
|
|
with the [`ipcMain`] and [`ipcRenderer`] modules. These channels are
|
|
**arbitrary** (you can name them anything you want) and **bidirectional** (you can use the
|
|
same channel name for both modules).
|
|
|
|
In this guide, we'll be going over some fundamental IPC patterns with concrete examples that
|
|
you can use as a reference for your app code.
|
|
|
|
## Understanding context-isolated processes
|
|
|
|
Before proceeding to implementation details, you should be familiar with the idea of using a
|
|
[preload script] to import Node.js and Electron modules in a context-isolated renderer process.
|
|
|
|
* For a full overview of Electron's process model, you can read the [process model docs].
|
|
* For a primer into exposing APIs from your preload script using the `contextBridge` module, check
|
|
out the [context isolation tutorial].
|
|
|
|
## Pattern 1: Renderer to main (one-way)
|
|
|
|
To fire a one-way IPC message from a renderer process to the main process, you can use the
|
|
[`ipcRenderer.send`] API to send a message that is then received by the [`ipcMain.on`] API.
|
|
|
|
You usually use this pattern to call a main process API from your web contents. We'll demonstrate
|
|
this pattern by creating a simple app that can programmatically change its window title.
|
|
|
|
For this demo, you'll need to add code to your main process, your renderer process, and a preload
|
|
script. The full code is below, but we'll be explaining each file individually in the following
|
|
sections.
|
|
|
|
```fiddle docs/fiddles/ipc/pattern-1
|
|
```
|
|
|
|
### 1. Listen for events with `ipcMain.on`
|
|
|
|
In the main process, set an IPC listener on the `set-title` channel with the `ipcMain.on` API:
|
|
|
|
```javascript {6-10,22} title='main.js (Main Process)'
|
|
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()
|
|
}
|
|
//...
|
|
```
|
|
|
|
The above `handleSetTitle` callback has two parameters: an [IpcMainEvent] structure and a
|
|
`title` string. Whenever a message comes through the `set-title` channel, this function will
|
|
find the BrowserWindow instance attached to the message sender and use the `win.setTitle`
|
|
API on it.
|
|
|
|
:::info
|
|
Make sure you're loading the `index.html` and `preload.js` entry points for the following steps!
|
|
:::
|
|
|
|
### 2. Expose `ipcRenderer.send` via preload
|
|
|
|
To send messages to the listener created above, you can use the `ipcRenderer.send` API.
|
|
By default, the renderer process has no Node.js or Electron module access. As an app developer,
|
|
you need to choose which APIs to expose from your preload script using the `contextBridge` API.
|
|
|
|
In your preload script, add the following code, which will expose a global `window.electronAPI`
|
|
variable to your renderer process.
|
|
|
|
```javascript title='preload.js (Preload Script)'
|
|
const { contextBridge, ipcRenderer } = require('electron')
|
|
|
|
contextBridge.exposeInMainWorld('electronAPI', {
|
|
setTitle: (title) => ipcRenderer.send('set-title', title)
|
|
})
|
|
```
|
|
|
|
At this point, you'll be able to use the `window.electronAPI.setTitle()` function in the renderer
|
|
process.
|
|
|
|
:::caution Security warning
|
|
We don't directly expose the whole `ipcRenderer.send` API for [security reasons]. Make sure to
|
|
limit the renderer's access to Electron APIs as much as possible.
|
|
:::
|
|
|
|
### 3. Build the renderer process UI
|
|
|
|
In our BrowserWindow's loaded HTML file, add a basic user interface consisting of a text input
|
|
and a button:
|
|
|
|
```html {11-12} title='index.html'
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
|
|
<title>Hello World!</title>
|
|
</head>
|
|
<body>
|
|
Title: <input id="title"/>
|
|
<button id="btn" type="button">Set</button>
|
|
<script src="./renderer.js"></script>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
To make these elements interactive, we'll be adding a few lines of code in the imported
|
|
`renderer.js` file that leverages the `window.electronAPI` functionality exposed from the preload
|
|
script:
|
|
|
|
```javascript title='renderer.js (Renderer Process)'
|
|
const setButton = document.getElementById('btn')
|
|
const titleInput = document.getElementById('title')
|
|
setButton.addEventListener('click', () => {
|
|
const title = titleInput.value
|
|
window.electronAPI.setTitle(title)
|
|
});
|
|
```
|
|
|
|
At this point, your demo should be fully functional. Try using the input field and see what happens
|
|
to your BrowserWindow title!
|
|
|
|
## Pattern 2: Renderer to main (two-way)
|
|
|
|
A common application for two-way IPC is calling a main process module from your renderer process
|
|
code and waiting for a result. This can be done by using [`ipcRenderer.invoke`] paired with
|
|
[`ipcMain.handle`].
|
|
|
|
In the following example, we'll be opening a native file dialog from the renderer process and
|
|
returning the selected file's path.
|
|
|
|
For this demo, you'll need to add code to your main process, your renderer process, and a preload
|
|
script. The full code is below, but we'll be explaining each file individually in the following
|
|
sections.
|
|
|
|
```fiddle docs/fiddles/ipc/pattern-2
|
|
```
|
|
|
|
### 1. Listen for events with `ipcMain.handle`
|
|
|
|
In the main process, we'll be creating a `handleFileOpen()` function that calls
|
|
`dialog.showOpenDialog` and returns the value of the file path selected by the user. This function
|
|
is used as a callback whenever an `ipcRender.invoke` message is sent through the `dialog:openFile`
|
|
channel from the renderer process. The return value is then returned as a Promise to the original
|
|
`invoke` call.
|
|
|
|
:::caution A word on error handling
|
|
Errors thrown through `handle` in the main process are not transparent as they
|
|
are serialized and only the `message` property from the original error is
|
|
provided to the renderer process. Please refer to
|
|
[#24427](https://github.com/electron/electron/issues/24427) for details.
|
|
:::
|
|
|
|
```javascript {6-13,25} title='main.js (Main Process)'
|
|
const { BrowserWindow, dialog, ipcMain } = require('electron')
|
|
const path = require('path')
|
|
|
|
//...
|
|
|
|
async function handleFileOpen() {
|
|
const { canceled, filePaths } = await dialog.showOpenDialog()
|
|
if (canceled) {
|
|
return
|
|
} else {
|
|
return 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()
|
|
})
|
|
//...
|
|
```
|
|
|
|
:::tip on channel names
|
|
The `dialog:` prefix on the IPC channel name has no effect on the code. It only serves
|
|
as a namespace that helps with code readability.
|
|
:::
|
|
|
|
:::info
|
|
Make sure you're loading the `index.html` and `preload.js` entry points for the following steps!
|
|
:::
|
|
|
|
### 2. Expose `ipcRenderer.invoke` via preload
|
|
|
|
In the preload script, we expose a one-line `openFile` function that calls and returns the value of
|
|
`ipcRenderer.invoke('dialog:openFile')`. We'll be using this API in the next step to call the
|
|
native dialog from our renderer's user interface.
|
|
|
|
```javascript title='preload.js (Preload Script)'
|
|
const { contextBridge, ipcRenderer } = require('electron')
|
|
|
|
contextBridge.exposeInMainWorld('electronAPI', {
|
|
openFile: () => ipcRenderer.invoke('dialog:openFile')
|
|
})
|
|
```
|
|
|
|
:::caution Security warning
|
|
We don't directly expose the whole `ipcRenderer.invoke` API for [security reasons]. Make sure to
|
|
limit the renderer's access to Electron APIs as much as possible.
|
|
:::
|
|
|
|
### 3. Build the renderer process UI
|
|
|
|
Finally, let's build the HTML file that we load into our BrowserWindow.
|
|
|
|
```html {10-11} title='index.html'
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
|
|
<title>Dialog</title>
|
|
</head>
|
|
<body>
|
|
<button type="button" id="btn">Open a File</button>
|
|
File path: <strong id="filePath"></strong>
|
|
<script src='./renderer.js'></script>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
The UI consists of a single `#btn` button element that will be used to trigger our preload API, and
|
|
a `#filePath` element that will be used to display the path of the selected file. Making these
|
|
pieces work will take a few lines of code in the renderer process script:
|
|
|
|
```javascript title='renderer.js (Renderer Process)'
|
|
const btn = document.getElementById('btn')
|
|
const filePathElement = document.getElementById('filePath')
|
|
|
|
btn.addEventListener('click', async () => {
|
|
const filePath = await window.electronAPI.openFile()
|
|
filePathElement.innerText = filePath
|
|
})
|
|
```
|
|
|
|
In the above snippet, we listen for clicks on the `#btn` button, and call our
|
|
`window.electronAPI.openFile()` API to activate the native Open File dialog. We then display the
|
|
selected file path in the `#filePath` element.
|
|
|
|
### Note: legacy approaches
|
|
|
|
The `ipcRenderer.invoke` API was added in Electron 7 as a developer-friendly way to tackle two-way
|
|
IPC from the renderer process. However, there exist a couple alternative approaches to this IPC
|
|
pattern.
|
|
|
|
:::warning Avoid legacy approaches if possible
|
|
We recommend using `ipcRenderer.invoke` whenever possible. The following two-way renderer-to-main
|
|
patterns are documented for historical purposes.
|
|
:::
|
|
|
|
:::info
|
|
For the following examples, we're calling `ipcRenderer` directly from the preload script to keep
|
|
the code samples small.
|
|
:::
|
|
|
|
#### Using `ipcRenderer.send`
|
|
|
|
The `ipcRenderer.send` API that we used for single-way communication can also be leveraged to
|
|
perform two-way communication. This was the recommended way for asynchronous two-way communication
|
|
via IPC prior to Electron 7.
|
|
|
|
```javascript title='preload.js (Preload Script)'
|
|
// You can also put expose this code to the renderer
|
|
// process with the `contextBridge` API
|
|
const { ipcRenderer } = require('electron')
|
|
|
|
ipcRenderer.on('asynchronous-reply', (_event, arg) => {
|
|
console.log(arg) // prints "pong" in the DevTools console
|
|
})
|
|
ipcRenderer.send('asynchronous-message', 'ping')
|
|
```
|
|
|
|
```javascript title='main.js (Main Process)'
|
|
ipcMain.on('asynchronous-message', (event, arg) => {
|
|
console.log(arg) // prints "ping" in the Node console
|
|
// works like `send`, but returning a message back
|
|
// to the renderer that sent the original message
|
|
event.reply('asynchronous-reply', 'pong')
|
|
})
|
|
```
|
|
|
|
There are a couple downsides to this approach:
|
|
|
|
* You need to set up a second `ipcRenderer.on` listener to handle the response in the renderer
|
|
process. With `invoke`, you get the response value returned as a Promise to the original API call.
|
|
* There's no obvious way to pair the `asynchronous-reply` message to the original
|
|
`asynchronous-message` one. If you have very frequent messages going back and forth through these
|
|
channels, you would need to add additional app code to track each call and response individually.
|
|
|
|
#### Using `ipcRenderer.sendSync`
|
|
|
|
The `ipcRenderer.sendSync` API sends a message to the main process and waits _synchronously_ for a
|
|
response.
|
|
|
|
```javascript title='main.js (Main Process)'
|
|
const { ipcMain } = require('electron')
|
|
ipcMain.on('synchronous-message', (event, arg) => {
|
|
console.log(arg) // prints "ping" in the Node console
|
|
event.returnValue = 'pong'
|
|
})
|
|
```
|
|
|
|
```javascript title='preload.js (Preload Script)'
|
|
// You can also put expose this code to the renderer
|
|
// process with the `contextBridge` API
|
|
const { ipcRenderer } = require('electron')
|
|
|
|
const result = ipcRenderer.sendSync('synchronous-message', 'ping')
|
|
console.log(result) // prints "pong" in the DevTools console
|
|
```
|
|
|
|
The structure of this code is very similar to the `invoke` model, but we recommend
|
|
**avoiding this API** for performance reasons. Its synchronous nature means that it'll block the
|
|
renderer process until a reply is received.
|
|
|
|
## Pattern 3: Main to renderer
|
|
|
|
When sending a message from the main process to a renderer process, you need to specify which
|
|
renderer is receiving the message. Messages need to be sent to a renderer process
|
|
via its [`WebContents`] instance. This WebContents instance contains a [`send`][webcontents-send] method
|
|
that can be used in the same way as `ipcRenderer.send`.
|
|
|
|
To demonstrate this pattern, we'll be building a number counter controlled by the native operating
|
|
system menu.
|
|
|
|
For this demo, you'll need to add code to your main process, your renderer process, and a preload
|
|
script. The full code is below, but we'll be explaining each file individually in the following
|
|
sections.
|
|
|
|
```fiddle docs/fiddles/ipc/pattern-3
|
|
```
|
|
|
|
### 1. Send messages with the `webContents` module
|
|
|
|
For this demo, we'll need to first build a custom menu in the main process using Electron's `Menu`
|
|
module that uses the `webContents.send` API to send an IPC message from the main process to the
|
|
target renderer.
|
|
|
|
```javascript {11-26} title='main.js (Main Process)'
|
|
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')
|
|
}
|
|
//...
|
|
|
|
```
|
|
|
|
For the purposes of the tutorial, it's important to note that the `click` handler
|
|
sends a message (either `1` or `-1`) to the renderer process through the `update-counter` channel.
|
|
|
|
```javascript
|
|
click: () => mainWindow.webContents.send('update-counter', -1)
|
|
```
|
|
|
|
:::info
|
|
Make sure you're loading the `index.html` and `preload.js` entry points for the following steps!
|
|
:::
|
|
|
|
### 2. Expose `ipcRenderer.on` via preload
|
|
|
|
Like in the previous renderer-to-main example, we use the `contextBridge` and `ipcRenderer`
|
|
modules in the preload script to expose IPC functionality to the renderer process:
|
|
|
|
```javascript title='preload.js (Preload Script)'
|
|
const { contextBridge, ipcRenderer } = require('electron')
|
|
|
|
contextBridge.exposeInMainWorld('electronAPI', {
|
|
onUpdateCounter: (callback) => ipcRenderer.on('update-counter', callback)
|
|
})
|
|
```
|
|
|
|
After loading the preload script, your renderer process should have access to the
|
|
`window.electronAPI.onUpdateCounter()` listener function.
|
|
|
|
:::caution Security warning
|
|
We don't directly expose the whole `ipcRenderer.on` API for [security reasons]. Make sure to
|
|
limit the renderer's access to Electron APIs as much as possible.
|
|
:::
|
|
|
|
:::info
|
|
In the case of this minimal example, you can call `ipcRenderer.on` directly in the preload script
|
|
rather than exposing it over the context bridge.
|
|
|
|
```javascript title='preload.js (Preload Script)'
|
|
const { ipcRenderer } = require('electron')
|
|
|
|
window.addEventListener('DOMContentLoaded', () => {
|
|
const counter = document.getElementById('counter')
|
|
ipcRenderer.on('update-counter', (_event, value) => {
|
|
const oldValue = Number(counter.innerText)
|
|
const newValue = oldValue + value
|
|
counter.innerText = newValue
|
|
})
|
|
})
|
|
```
|
|
|
|
However, this approach has limited flexibility compared to exposing your preload APIs
|
|
over the context bridge, since your listener can't directly interact with your renderer code.
|
|
:::
|
|
|
|
### 3. Build the renderer process UI
|
|
|
|
To tie it all together, we'll create an interface in the loaded HTML file that contains a
|
|
`#counter` element that we'll use to display the values:
|
|
|
|
```html {10} title='index.html'
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
|
|
<title>Menu Counter</title>
|
|
</head>
|
|
<body>
|
|
Current value: <strong id="counter">0</strong>
|
|
<script src="./renderer.js"></script>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
Finally, to make the values update in the HTML document, we'll add a few lines of DOM manipulation
|
|
so that the value of the `#counter` element is updated whenever we fire an `update-counter` event.
|
|
|
|
```javascript title='renderer.js (Renderer Process)'
|
|
const counter = document.getElementById('counter')
|
|
|
|
window.electronAPI.onUpdateCounter((_event, value) => {
|
|
const oldValue = Number(counter.innerText)
|
|
const newValue = oldValue + value
|
|
counter.innerText = newValue
|
|
})
|
|
```
|
|
|
|
In the above code, we're passing in a callback to the `window.electronAPI.onUpdateCounter` function
|
|
exposed from our preload script. The second `value` parameter corresponds to the `1` or `-1` we
|
|
were passing in from the `webContents.send` call from the native menu.
|
|
|
|
### Optional: returning a reply
|
|
|
|
There's no equivalent for `ipcRenderer.invoke` for main-to-renderer IPC. Instead, you can
|
|
send a reply back to the main process from within the `ipcRenderer.on` callback.
|
|
|
|
We can demonstrate this with slight modifications to the code from the previous example. In the
|
|
renderer process, use the `event` parameter to send a reply back to the main process through the
|
|
`counter-value` channel.
|
|
|
|
```javascript title='renderer.js (Renderer Process)'
|
|
const counter = document.getElementById('counter')
|
|
|
|
window.electronAPI.onUpdateCounter((event, value) => {
|
|
const oldValue = Number(counter.innerText)
|
|
const newValue = oldValue + value
|
|
counter.innerText = newValue
|
|
event.sender.send('counter-value', newValue)
|
|
})
|
|
```
|
|
|
|
In the main process, listen for `counter-value` events and handle them appropriately.
|
|
|
|
```javascript title='main.js (Main Process)'
|
|
//...
|
|
ipcMain.on('counter-value', (_event, value) => {
|
|
console.log(value) // will print value to Node console
|
|
})
|
|
//...
|
|
```
|
|
|
|
## Pattern 4: Renderer to renderer
|
|
|
|
There's no direct way to send messages between renderer processes in Electron using the `ipcMain`
|
|
and `ipcRenderer` modules. To achieve this, you have two options:
|
|
|
|
* Use the main process as a message broker between renderers. This would involve sending a message
|
|
from one renderer to the main process, which would forward the message to the other renderer.
|
|
* Pass a [MessagePort] from the main process to both renderers. This will allow direct communication
|
|
between renderers after the initial setup.
|
|
|
|
## Object serialization
|
|
|
|
Electron's IPC implementation uses the HTML standard
|
|
[Structured Clone Algorithm][sca] to serialize objects passed between processes, meaning that
|
|
only certain types of objects can be passed through IPC channels.
|
|
|
|
In particular, DOM objects (e.g. `Element`, `Location` and `DOMMatrix`), Node.js objects
|
|
backed by C++ classes (e.g. `process.env`, some members of `Stream`), and Electron objects
|
|
backed by C++ classes (e.g. `WebContents`, `BrowserWindow` and `WebFrame`) are not serializable
|
|
with Structured Clone.
|
|
|
|
[context isolation tutorial]: context-isolation.md
|
|
[security reasons]: ./context-isolation.md#security-considerations
|
|
[`ipcMain`]: ../api/ipc-main.md
|
|
[`ipcMain.handle`]: ../api/ipc-main.md#ipcmainhandlechannel-listener
|
|
[`ipcMain.on`]: ../api/ipc-main.md
|
|
[IpcMainEvent]: ../api/structures/ipc-main-event.md
|
|
[`ipcRenderer`]: ../api/ipc-renderer.md
|
|
[`ipcRenderer.invoke`]: ../api/ipc-renderer.md#ipcrendererinvokechannel-args
|
|
[`ipcRenderer.send`]: ../api/ipc-renderer.md
|
|
[MessagePort]: ./message-ports.md
|
|
[preload script]: process-model.md#preload-scripts
|
|
[process model docs]: process-model.md
|
|
[sca]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
|
|
[`WebContents`]: ../api/web-contents.md
|
|
[webcontents-send]: ../api/web-contents.md#contentssendchannel-args
|