La communication inter-processus
La communication inter-processus (IPC) est un élément clé de la création d’applications de bureau avec Electron. Étant donné que les processus principaux et de rendu ont des responsabilités distinctes au sein du modèle de processus d’Electron, IPC est l'unique moyen d’effectuer un grand nombre de tâches courantes, telles que l’appel d’une API native à partir de votre interface utilisateur ou le déclenchement de modifications de votre contenu Web à partir de menus natifs.
Les Canaux IPC
In Electron, processes communicate by passing messages through developer-defined "channels" with the ipcMain
and ipcRenderer
modules. Le nom de ces canaux est arbitraire (vous pouvez les nommer comme vous voulez) et peut être utilisé de façon bidirectionnelle (vous pouvez utiliser le même nom de canal pour les deux modules).
Dans ce guide, nous allons passer en revue quelques modèles IPC fondamentaux avec des exemples concrets pouvant être utilisés comme référence lors du codage de votre application.
Comprendre les processus isolés du contexte
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.
Scénario1 : Moteur de rendu vers le processus principal (unidirectionnel)
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.
Vous utiliserez généralement ce scénario pour appeler une API du processus principal à partir de votre contenu web. Nous allons le démontrer en créant une application simple qui peut modifier par programme le titre de sa fenêtre.
Pour cette démo, vous devrez ajouter du code à votre processus principal, à votre processus de rendu et à un script de préchargement . Le code complet est ci-dessous, cependant nous détaillerons chaque fichier individuellement dans les sections suivantes.
- main.js
- preload.js
- index.html
- renderer.js
const { app, BrowserWindow, ipcMain } = require('electron/main')
const path = require('node:path')
function createWindow () {
const mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
ipcMain.on('set-title', (event, title) => {
const webContents = event.sender
const win = BrowserWindow.fromWebContents(webContents)
win.setTitle(title)
})
mainWindow.loadFile('index.html')
}
app.whenReady().then(() => {
createWindow()
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
const { contextBridge, ipcRenderer } = require('electron/renderer')
contextBridge.exposeInMainWorld('electronAPI', {
setTitle: (title) => ipcRenderer.send('set-title', title)
})
<!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>
const setButton = document.getElementById('btn')
const titleInput = document.getElementById('title')
setButton.addEventListener('click', () => {
const title = titleInput.value
window.electronAPI.setTitle(title)
})
1. Ecoute des événements à l'aide de ipcMain.on
Dans le processus principal, mise en place d'un écouteur d'IPC sur le canal set-title
à l'ai de de l'API ipcMain.on
:
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('node: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. Chaque fois qu’un message passe par le canal set-title
, cette fonction extrait l’instance BrowserWindow attachée à l’expéditeur du message et lui applique l’API win.setTitle
.
Assurez-vous de charger les points d'entrée index.html
et preload.js
pour les étapes suivantes !
2. Exposition de ipcRenderer.send
via le préchargement
Pour envoyer des messages à l’écouteur créé ci-dessus, vous pouvez utiliser l’API ipcRenderer.send
. Par défaut, le processus de rendu n’a pas d’accès aux modules de Node.js et d'Electron. En tant que développeur d’applications, vous devez choisir les API à exposer à partir de votre script de préchargement à l’aide de l’API contextBridge
.
Pour ce faire, ajoutez le code suivant dans votre script de préchargement et ainsi vous exposerez à votre processus de rendu la variable globale window.electronAPI
.
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
setTitle: (title) => ipcRenderer.send('set-title', title)
})
À ce stade, vous pourrez utiliser la fonction window.electronAPI.setTitle()
dans le processus de rendu.
We don't directly expose the whole ipcRenderer.send
API for security reasons. Assurez-vous de limiter autant que possible l’accès du moteur de rendu aux API Electron.
3. Générer l’interface utilisateur du processus de rendu
Vous allez maintenant ajoutez une interface utilisateur de base composée d’un input de type text et d’un bouton dans le fichier HTML chargé par notre BrowserWindow's:
<!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>
Afin de rendre ces éléments interactifs, nous allons ajouter quelques lignes de code dans le fichier importé renderer.js
qui tirera parti de la fonctionnalité window.electronAPI
exposée depuis le script de préchargement :
const setButton = document.getElementById('btn')
const titleInput = document.getElementById('title')
setButton.addEventListener('click', () => {
const title = titleInput.value
window.electronAPI.setTitle(title)
})
À ce stade, votre démo devrait être entièrement fonctionnelle. Essayez d’utiliser le champ de saisie et voyez ce qu’il advient du titre de votre BrowserWindow !
Scénario2 : Moteur de rendu vers le processus principal (bidirectionnel)
Une application courante d'IPC bidirectionnel est l'appel d'un module du processus principal à partir du code de votre processus de rendu avec l'attente d'un résultat. This can be done by using ipcRenderer.invoke
paired with ipcMain.handle
.
Dans l’exemple suivant, nous allons ouvrir une boîte de dialogue native d'ouverture de fichier à partir du processus de rendu et retourner le chemin d’accès du fichier sélectionné.
Pour cette démo, vous devrez ajouter du code à vos processus principal et processus de rendu et à un script de préchargement . Le code complet est ci-dessous, cependant nous détaillerons chaque fichier individuellement dans les sections suivantes.
- main.js
- preload.js
- index.html
- renderer.js
const { app, BrowserWindow, ipcMain, dialog } = require('electron/main')
const path = require('node:path')
async function handleFileOpen () {
const { canceled, filePaths } = await dialog.showOpenDialog()
if (!canceled) {
return filePaths[0]
}
}
function createWindow () {
const mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
mainWindow.loadFile('index.html')
}
app.whenReady().then(() => {
ipcMain.handle('dialog:openFile', handleFileOpen)
createWindow()
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
const { contextBridge, ipcRenderer } = require('electron/renderer')
contextBridge.exposeInMainWorld('electronAPI', {
openFile: () => ipcRenderer.invoke('dialog:openFile')
})
<!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>
const btn = document.getElementById('btn')
const filePathElement = document.getElementById('filePath')
btn.addEventListener('click', async () => {
const filePath = await window.electronAPI.openFile()
filePathElement.innerText = filePath
})
1. Écoute des événements avec ipcMain.handle
Dans le processus principal, nous allons créer une fonction handleFileOpen()
qui appelle
dialog. howOpenDialog
et retourne le chemin du fichier sélectionné par l'utilisateur. Cette fonction est utilisée comme callback chaque fois qu'un message ipcRender.invoke
est envoyé par le canal dialog:openFile
depuis le processus de rendu. La valeur de retour est ensuite renvoyée sous forme de promesse à l’appel « invoke »
d’origine.
Les erreurs générées par 'handle' dans le processus principal ne sont pas transparentes puisqu'elles sont sérialisées et que seule la propriété 'message' de l’erreur d’origine est fournie au processus de rendu. Pour plus de détails, veuillez vous référer au problème #24427 sur github .
const { app, BrowserWindow, dialog, ipcMain } = require('electron')
const path = require('node:path')
// ...
async function handleFileOpen () {
const { canceled, filePaths } = await dialog.showOpenDialog({})
if (!canceled) {
return filePaths[0]
}
}
function createWindow () {
const mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
mainWindow.loadFile('index.html')
}
app.whenReady().then(() => {
ipcMain.handle('dialog:openFile', handleFileOpen)
createWindow()
})
// ...
Le préfixe dialog:
du nom de canal IPC n’a aucun effet sur le code. Il ne sert que d’espace de nommage afin d'apporter plus de lisibilité au code.
Assurez-vous de charger les points d'entrée index.html
et preload.js
pour les étapes suivantes !
2. Exposition de ipcRenderer.invoke
via le préchargement
Dans le script de préchargement, nous exposons une fonction mono-ligne openFile
appellant et renvoyant la valeur de ipcRenderer.invoke('dialog:openFile')
. Nous utiliserons cette API à l'étape suivante pour appeler la boîte de dialogue native à partir de l'interface utilisateur de notre moteur de rendu.
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
openFile: () => ipcRenderer.invoke('dialog:openFile')
})
We don't directly expose the whole ipcRenderer.invoke
API for security reasons. Veillez toujours à limiter autant que possible l’accès du moteur de rendu aux API Electron.
3. Générer l’interface utilisateur du processus de rendu
Enfin, pour termier, construisons le fichier HTML qui sera chargé dans notre BrowserWindow.
<!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>
L’interface utilisateur se compose d’un seul élément #btn
button qui sera utilisé pour déclencher notre API de préchargement, et l'élément d'id #filePath
qui sera utilisé pour afficher le chemin d’accès du fichier sélectionné. Pour que ces éléments fonctionnent, il ne faut que quelques lignes de code dans le script du processus de rendu :
const btn = document.getElementById('btn')
const filePathElement = document.getElementById('filePath')
btn.addEventListener('click', async () => {
const filePath = await window.electronAPI.openFile()
filePathElement.innerText = filePath
})
Dans le snippet ci-dessus, nous écoutons les clics du bouton #btn
, et appelons notre API window.electronAPI.openFile()
pour activer la boîte de dialogue native d'Ouverture d'un fichier. Nous affichons ensuite le chemin d’accès au fichier sélectionné dans l’élément #filePath
.
Remarque : méthodes plus anciennes
L’API ipcRenderer.invoke
a été ajoutée dans Electron 7 apportant un moyen convivial aux développeurs pour régler les problèmes d'IPC bidirectionnel à partir du processus de rendu. However, a couple of alternative approaches to this IPC pattern exist.
Nous vous recommandons d’utiliser ipcRenderer.invoke
chaque fois que possible. Les modèles d'échange bidirectionnel suivants ne sont documentés qu'à des fins historiques.
Dans les exemples suivants, nous appelons ipcRenderer
directement à partir du script de préchargement pour que les échantillons de code ne soient pas trop volumineux.
Utilisation de ipcRenderer.send
L’API ipcRenderer.send
que nous avons utilisée pour la communication unidirectionnelle peut également être exploitée pour effectuer une communication bidirectionnelle. C’était d'ailleurs la méthode recommandée pour la communication bidirectionnelle asynchrone via IPC avant Electron7.
// Vous pouvez également placer ce code dans le processus de rendu
// avec l'API `contextBridge`
const { ipcRenderer } = require('electron')
ipcRenderer. n('asynchronous-reply', (_event, arg) => {
console.log(arg) // affiche "pong" dans la console DevTools
})
ipcRenderer.send('asynchronous-message', 'ping')
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')
})
Mais cette approche présente certains inconvénients :
- Vous devez configurer un second écouteur
ipcRenderer.on
pour gérer la réponse dans le processus de rendu . Alors qu'avecinvoke
, vous obtenez en retour une Promise de l'appel API original. - Il n’y a pas de moyen évident de jumeler le message
asynchronous-reply
à celui de l'asynchronous-message
d’origine. Si vous avez des messages très fréquents qui vont et viennent via ces canaux, vous devrez ajouter du code supplémentaire pour suivre individuellement chaque appel et réponse.
Utilisation de ipcRenderer.sendSync
L’API ipcRenderer.sendSync
envoie un message au processus principal et attend de manière synchrone une réponse .
const { ipcMain } = require('electron')
ipcMain.on('synchronous-message', (event, arg) => {
console.log(arg) // affiche "ping" dans la console Node
event.returnValue = 'pong'
})
// Vous pouvez également placer ce code dans le processus de rendu
// avec l'API `contextBridge`
const { ipcRenderer } = require('electron')
const result = ipcRenderer.sendSync('synchronous-message', 'ping')
console.log(result) // affiche "pong" dans la console des DevTools
La structure de ce code est très similaire au modèle invoke
, mais nous recommandons d'éviter cette API pour des raisons de performances. Sa nature synchrone fait que le processus de rendu sera bloqué jusqu’à la réception d’une réponse.
Scénario 3 : Processus principal vers moteur de rendu
Lorsque vous envoyez un message du processus principal à un processus de rendu, vous devez spécifier le moteur de rendu destinataire de ce message. Messages need to be sent to a renderer process via its WebContents
instance. This WebContents instance contains a send
method that can be used in the same way as ipcRenderer.send
.
Pour illustrer ce scénario, nous allons créer un compteur contrôlé par le menu natif du système d’exploitation.
Pour cette démo, vous devrez ajouter du code à votre processus principal, à votre processus de rendu et à un script de préchargement . Le code complet est ci-dessous, cependant nous détaillerons chaque fichier individuellement dans les sections suivantes.
- main.js
- preload.js
- index.html
- renderer.js
const { app, BrowserWindow, Menu, ipcMain } = require('electron/main')
const path = require('node: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')
// Open the DevTools.
mainWindow.webContents.openDevTools()
}
app.whenReady().then(() => {
ipcMain.on('counter-value', (_event, value) => {
console.log(value) // will print value to Node console
})
createWindow()
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
const { contextBridge, ipcRenderer } = require('electron/renderer')
contextBridge.exposeInMainWorld('electronAPI', {
onUpdateCounter: (callback) => ipcRenderer.on('update-counter', (_event, value) => callback(value)),
counterValue: (value) => ipcRenderer.send('counter-value', value)
})
<!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>
const counter = document.getElementById('counter')
window.electronAPI.onUpdateCounter((value) => {
const oldValue = Number(counter.innerText)
const newValue = oldValue + value
counter.innerText = newValue.toString()
window.electronAPI.counterValue(newValue)
})
1. Envoie de messages avec le module 'webContents'
Pour cette démo, nous devrons d’abord créer un menu personnalisé dans le processus principal à l’aide du module 'Menu' d’Electron celui-ci utilisera l’API 'webContents.send' pour envoyer un message IPC du processus principal au moteur de rendu cible .
const { app, BrowserWindow, Menu, ipcMain } = require('electron')
const path = require('node: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')
}
// ...
Pour les besoins du tutoriel, il est important de noter que le gestionnaire de click
envoie un message ( 1
ou -1
) au processus de rendu via le canal update-counter
.
click: () => mainWindow.webContents.send('update-counter', -1)
Assurez-vous de charger les points d'entrée index.html
et preload.js
pour les étapes suivantes !
2. Exposition de ipcRenderer.on
via le préchargement
Comme dans l’exemple précédent du moteur de rendu vers le processus principal, nous allons utiliser les modules contextBridge
et ipcRenderer
dans le script de préchargement pour exposer la fonctionnalité IPC au processus de rendu :
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
onUpdateCounter: (callback) => ipcRenderer.on('update-counter', (_event, value) => callback(value))
})
Après avoir chargé le script de préchargement, votre processus de rendu aura accès à la fonction d’écouteur window.electronAPI.onUpdateCounter()
.
We don't directly expose the whole ipcRenderer.on
API for security reasons. Assurez-vous de limiter autant que possible l’accès du moteur de rendu aux API Electron. Also don't just pass the callback to ipcRenderer.on
as this will leak ipcRenderer
via event.sender
. Use a custom handler that invoke the callback
only with the desired arguments.
Dans le cadre de cet exemple minimal, vous pouvez appeler ipcRenderer.on
directement dans le script de préchargement plutôt que de l’exposer via le contextBridge.
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
})
})
Toutefois, cette approche n'est pas très flexible par rapport à l’exposition de vos API de préchargement par contextBridge, car votre écouteur ne peut pas interagir directement avec le code de votre moteur de rendu.
3. Générer l’interface utilisateur du processus de rendu
Pour lier le tout, nous allons créer une interface dans le fichier HTML chargé qui contient un élément d'id #counter
que nous utiliserons pour afficher les valeurs:
<!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>
Enfin, pour mettre à jour les valeurs dans le document HTML, nous allons ajouter quelques lignes de manipulation du DOM afin que la valeur de l’élément #counter
soit mise à jour chaque fois que nous lançons un événement update-counter
.
const counter = document.getElementById('counter')
window.electronAPI.onUpdateCounter((value) => {
const oldValue = Number(counter.innerText)
const newValue = oldValue + value
counter.innerText = newValue.toString()
})
Dans le code ci-dessus, nous passons une callback à la fonction window.electronAPI.onUpdateCounter
exposée à partir de notre script de préchargement. Le deuxième paramètre value
correspond au 1
ou au -1
que nous passions à partir de l’appel webContents.send
du menu natif.
Facultatif : retourner une réponse
Il n’y a pas d’équivalent au ipcRenderer.invoke
pour un IPC du processus principal vers un processus de rendu. Au lieu de cela, vous pouvez renvoyer une réponse au processus principal à partir de la callback avec ipcRenderer.on
.
Nous pouvons en faire la démonstration en modifiant légèrement le code de l’exemple précédent. In the renderer process, expose another API to send a reply back to the main process through the counter-value
channel.
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
onUpdateCounter: (callback) => ipcRenderer.on('update-counter', (_event, value) => callback(value)),
counterValue: (value) => ipcRenderer.send('counter-value', value)
})
const counter = document.getElementById('counter')
window.electronAPI.onUpdateCounter((value) => {
const oldValue = Number(counter.innerText)
const newValue = oldValue + value
counter.innerText = newValue.toString()
window.electronAPI.counterValue(newValue)
})
Dans le processus principal, écoutez les événements counter-value
et gérez-les de manière appropriée.
// ...
ipcMain.on('counter-value', (_event, value) => {
console.log(value) // affichera la valeur dans la console Node
})
// ...
Scénario 4 : Echange entre deux moteurs de rendu
Avec Electron, Il n’y a pas, à l’aide des modules ipcMain
et ipcRenderer
, de moyen direct pour transmettre des messages entre des processus de rendu . Pour y parvenir, vous avez deux options :
- Utiliser le processus principal comme agent de messagerie entre les moteurs de rendu. Cela impliquera l’envoi d’un message d’un moteur de rendu au processus principal, qui devra transmettre ce message à l’autre moteur de rendu.
- Pass a MessagePort from the main process to both renderers. Ceci permettra, après la configuration initiale, une communication directe entre les moteurs de rendu.
Sérialisation d’objets
L’implémentation IPC d’Electron utilisant la norme HTML Structured Clone Algorithm pour sérialiser les objets transmis entre les processus, implique que seuls certains types d’objets pourront être transmis via les canaux IPC.
En particulier, les objets DOM (par exemple, Element
, Location
et DOMMatrix
), les objets de Node.js s'appuyant sur des classes C++ (par exemple, process.env
, certains membres de Stream
) et les objets Electron s'appuyant également sur des classes C++ (par exemple, WebContents
, BrowserWindow
et WebFrame
) ne sont pas sérialisables avec Structured Clone.