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​

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.

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.

const { app, BrowserWindow, ipcMain } = require('electron/main')
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()

app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})

app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})

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:

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

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.

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é

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:

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. 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.

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é

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.

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. 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.

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é

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.

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.
  • 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.