Aller au contenu principal

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

Avec Electron, les processus communiquent en transmettant des messages via des « canaux » définis par le développeur à l'aide des modules ipcMain et ipcRenderer. 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

Avant de passer aux détails de l’implémentation, vous devez être familier avec l’idée d’utiliser un script de préchargement pour importer des modules Node.js et Electron dans un processus de rendu isolé du contexte.

  • Afin d'avoir une vue d’ensemble complète du modèle de processus d’Electron, vous pouvez lire la documentation modèle de processus.
  • Pour une introduction à l’exposition d'API à partir de votre script de préchargement à l’aide du module contextBridge, consultez le tutoriel Isolation du contexte.

Scénario1 : Moteur de rendu vers le processus principal (unidirectionnel)

Pour envoyer un message IPC unidirectionnel d’un processus de rendu vers le processus principal, vous pouvez utiliser l’API ipcRenderer.send et celui ci sera ensuite reçu par l’API ipcMain.on .

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.

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()
})

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 :

main.js (Main Process)
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()
})
// ...

La callback handleSetTitle ci-dessus a deux paramètres : une structure IpcMainEvent et une string title. 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.

info

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 .

preload.js (Preload Script)
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.

Avertissements de sécurité

Nous n’exposons pas directement l’ensemble de l’API ipcRenderer.send pour des raisons de sécurité. 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:

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>

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 :

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

À 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. Cela peut être fait en utilisant ipcRenderer.invoke jumelé avec 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.

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()
})

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.

Un mot sur la gestion des erreurs

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 .

main.js (Main Process)
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()
})
// ...
à propos des noms des canaux

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.

info

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.

preload.js (Preload Script)
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electronAPI', {
openFile: () => ipcRenderer.invoke('dialog:openFile')
})
Avertissements de sécurité

Nous n’exposons pas directement l’ensemble de l’API ipcRenderer.invoke pour des raisons de sécurité . 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.

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>

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 :

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

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.

Éviter les approches plus anciennes si possible

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.

info

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.

preload.js (Preload Script)
// 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')
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')
})

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'avec invoke, 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 .

main.js (Main Process)
const { ipcMain } = require('electron')
ipcMain.on('synchronous-message', (event, arg) => {
console.log(arg) // affiche "ping" dans la console Node
event.returnValue = 'pong'
})
preload.js (Preload Script)
// 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. Les messages doivent être envoyés à un processus de rendu via son instance WebContents. Cette instance de WebContents contient une méthode send qui peut être utilisée de la même manière que 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.

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()
})

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 .

main.js (Main Process)
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)
info

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 :

preload.js (Preload Script)
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() .

Avertissements de sécurité

Nous n’exposons pas directement l’ensemble de l’API ipcRenderer.on pour des raisons de sécurité . 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.

info

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.

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

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:

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>

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 .

renderer.js (Renderer Process)
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.

preload.js (Preload Script)
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)
})
renderer.js (Renderer Process)
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.

main.js (Main Process)
// ...
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.
  • Transmettre un messagePort aux deux moteurs de rendu à partir du processus principal. 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.