Межпроцессное взаимодействие
Межпроцессное взаимодействие (IPC) является ключевой частью создания функционально богатых настольных приложений в Electron. Поскольку основной и рендерные процессы имеют разные обязанности в модели процесса Electron, IPC является единственным способом выполнения многих распространенных задач, таких как вызов нативного API из вашего пользовательского интерфейса или вызов изменений в вашем веб-содержимом из нативных меню.
Каналы IPC
В Electron процессы обмениваются сообщениями, передавая их через определенные разработчиками «каналы» с использованием модулей ipcMain и ipcRenderer. Эти каналы являются произвольными (вы можете назвать их как угодно) и двунаправленными (вы можете использовать одно и то же имя канала для обоих модулей).
В этом руководстве мы рассмотрим некоторые базовые паттерны IPC с конкретными примерами, которые вы можете использовать в качестве ссылки для кода вашего приложения.
Понимание контекстно-изолированных процессов
Прежде чем переходить к деталям реализации, вы должны ознакомиться с концепцией использования скрипта предварительной загрузки для импорта модулей Node.js и Electron в контекстно-изолированном процессе рендеринга.
- Для получения полного обзора модели процессов Electron вы можете прочитать документацию по модели процессов.
- Для ознакомления с экспонированием API из скрипта предварительной загрузки с использованием модуля
contextBridge, ознакомьтесь с учебником по изоляции контекста.
Паттерн 1: рендер-процесс к основному процессу (односторонний)
Для отправки одностороннего сообщения IPC из процесса рендеринга в основной процесс, вы можете использовать API ipcRenderer.send для отправки сообщения, которое затем будет получено API ipcMain.on.
Обычно этот паттерн используется для вызова API основного процесса из вашего веб-содержимого. Мы продемонстрируем этот паттерн, создав простое приложение, которое может программно изменять заголовок окна.
Для этой демонстрации вам потребуется добавить код в основной процесс, процесс рендеринга и скрипт предварительной загрузки. Полный код приведен ниже, но мы будем объяснять каждый файл по отдельности в следующих разделах.
- main.js
- preload.js
- index.html
- renderer.js
const {app, BrowserWindow, ipcMain} = require('electron')
const path = require('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')
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. Прослушивание событий с помощью ipcMain.on
В основном процессе установите прослушиватель IPC на канале set-title с помощью API ipcMain.on:
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()
}
//...
Указанный выше handleSetTitle обратный вызов имеет два параметра: структуру IpcMainEvent и строку title. Всякий раз, когда приходит сообщение по каналу set-title, эта функция найдет экземпляр BrowserWindow, привязанный к отправителю сообщения, и использует API win.setTitle для него.
Убедитесь, что вы загружаете index.html и preload.js точки входа для следующих шагов!
2. Экспонирование ipcRenderer.send через предварительную загрузку
Для отправки сообщений в прослушиватель, созданный выше, вы можете использовать API ipcRenderer.send. По умолчанию процесс рендеринга не имеет доступа к модулям Node.js или Electron. Как разработчик приложения, вы должны выбрать, какие API экспонировать из скрипта предварительной загрузки с помощью API contextBridge.
В скрипт предварительной загрузки добавьте следующий код, который экспонирует глобальную переменную window.electronAPI в ваш процесс рендеринга.
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
setTitle: (title) => ipcRenderer.send('set-title', title)
})
В этот момент вы сможете использовать функцию window.electronAPI.setTitle() в процессе рендеринга.
Мы не экспонируем напрямую весь API ipcRenderer.send по соображениям безопасности. Убедитесь, что вы ограничиваете доступ рендерного процесса к API Electron по мере возможности.
3. Создание пользовательского интерфейса процесса рендеринга
В загруженном HTML-файле вашего 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>Hello World!</title>
</head>
<body>
Title: <input id="title"/>
<button id="btn" type="button">Set</button>
<script src="./renderer.js"></script>
</body>
</html>
Чтобы сделать эти элементы интерактивными, мы добавим несколько строк кода в импортированный файл renderer.js, использующий функциональность window.electronAPI, экспонированную скриптом предварительной загрузки:
const setButton = document.getElementById('btn')
const titleInput = document.getElementById('title')
setButton.addEventListener('click', () => {
const title = titleInput.value
window.electronAPI.setTitle(title)
});
В этот момент ваша демонстрация должна быть полностью функциональной. Попробуйте использовать поле ввода и посмотрите, что происходит с заголовком вашего BrowserWindow!
Паттерн 2: рендер-процесс к основному процессу (двусторонний)
Распространенное применение двустороннего IPC — вызов модуля основного процесса из кода вашего процесса рендеринга и ожидание результата. Это можно сделать с помощью ipcRenderer.invoke в паре с ipcMain.handle.
В следующем примере мы откроем диалоговое окно выбора файла из процесса рендеринга и вернём путь выбранного файла.
Для этой демонстрации вам потребуется добавить код в основной процесс, процесс рендеринга и скрипт предварительной загрузки. Полный код приведен ниже, но мы будем объяснять каждый файл по отдельности в следующих разделах.
- main.js
- preload.js
- index.html
- renderer.js
const {app, BrowserWindow, ipcMain, dialog} = 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().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')
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. Прослушивание событий с помощью ipcMain.handle
В основном процессе мы создадим функцию handleFileOpen(), которая вызовет dialog.showOpenDialog и вернёт значение пути к файлу, выбранному пользователем. Эта функция используется в качестве обратного вызова всякий раз, когда через канал dialog:openFile от процесса визуализации отправляется сообщение ipcRender.invoke. Возвращаемое значение затем возвращается как Promise первоначальному вызову invoke.
Ошибки, возникающие через handle в основном процессе, не прозрачны, так как они сериализуются, и в процесс визуализации предоставляется только свойство message исходной ошибки. Подробности см. в #24427.
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()
})
//...
Префикс dialog: в имени канала IPC не влияет на код. Он служит только в качестве пространства имён, что помогает улучшить читаемость кода.
Убедитесь, что вы загружаете точки входа index.html и preload.js для следующих шагов!
2. Экспонирование ipcRenderer.invoke через загрузчик
В скрипте загрузчика мы экспонируем однострочную функцию openFile, которая вызывает и возвращает значение ipcRenderer.invoke('dialog:openFile'). Мы будем использовать этот API на следующем шаге для вызова системного диалога открытия файла из пользовательского интерфейса процесса визуализации.
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
openFile: () => ipcRenderer.invoke('dialog:openFile')
})
Мы не экспонируем весь API ipcRenderer.invoke напрямую по соображениям безопасности. Убедитесь, что доступ процесса визуализации к API Electron ограничен по возможности.
3. Создание пользовательского интерфейса процесса визуализации
Наконец, давайте создадим HTML-файл, который мы загрузим в наш 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>
Пользовательский интерфейс состоит из одного элемента кнопки #btn, который будет использоваться для запуска нашего API загрузчика, и элемента #filePath, который будет использоваться для отображения пути к выбранному файлу. Для работы этих компонентов потребуется несколько строк кода в скрипте процесса визуализации:
const btn = document.getElementById('btn')
const filePathElement = document.getElementById('filePath')
btn.addEventListener('click', async () => {
const filePath = await window.electronAPI.openFile()
filePathElement.innerText = filePath
})
В приведенном выше фрагменте мы прослушиваем клики по кнопке #btn и вызываем наш API window.electronAPI.openFile() для запуска системного диалога открытия файла. Затем мы отображаем путь к выбранному файлу в элементе #filePath.
Примечание: устаревшие подходы
API ipcRenderer.invoke был добавлен в Electron 7 в качестве удобного для разработчиков способа решения задач двусторонней IPC из процесса визуализации. Однако существуют несколько альтернативных подходов к этому шаблону IPC.
Мы рекомендуем использовать ipcRenderer.invoke, когда это возможно. Следующие шаблоны двусторонней связи между процессами визуализации и основным процессом документированы для исторических целей.
В следующих примерах мы вызываем ipcRenderer непосредственно из скрипта загрузчика, чтобы примеры кода были компактными.
Использование ipcRenderer.send
API ipcRenderer.send, который мы использовали для односторонней связи, также можно использовать для организации двусторонней связи. До Electron 7 это был рекомендуемый способ асинхронной двусторонней связи через IPC.
// 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')
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')
})
У этого подхода есть несколько недостатков:
- Вам нужно настроить второй прослушиватель
ipcRenderer.onдля обработки ответа в процессе визуализации. С помощьюinvokeзначение ответа возвращается в виде Promise первоначальному вызову API. - Нет очевидного способа сопоставить сообщение
asynchronous-replyс исходным сообщениемasynchronous-message. Если у вас очень часто происходят сообщения туда и обратно по этим каналам, вам потребуется добавить дополнительный код приложения для отслеживания каждого вызова и ответа индивидуально.
Использование ipcRenderer.sendSync
API ipcRenderer.sendSync отправляет сообщение в основной процесс и ожидает синхронный ответ.
const { ipcMain } = require('electron')
ipcMain.on('synchronous-message', (event, arg) => {
console.log(arg) // prints "ping" in the Node console
event.returnValue = 'pong'
})
// 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
Структура этого кода очень похожа на модель invoke, но мы рекомендуем **избегать использования этого API** по причинам производительности. Его синхронная природа означает, что он заблокирует процесс визуализации до получения ответа.
Шаблон 3: основной процесс – процесс визуализации
При отправке сообщения из основного процесса в процесс визуализации необходимо указать, какой процесс визуализации получает сообщение. Сообщения должны отправляться в процесс визуализации через его экземпляр WebContents. Этот экземпляр WebContents содержит метод send, который можно использовать так же, как и ipcRenderer.send.
Чтобы продемонстрировать этот шаблон, мы создадим счётчик, управляемый системным меню.
Для этой демонстрации вам потребуется добавить код в основной процесс, процесс визуализации и скрипт загрузчика. Полный код приведен ниже, но мы объясним каждый файл по отдельности в следующих разделах.
- main.js
- preload.js
- index.html
- renderer.js
const {app, BrowserWindow, Menu, ipcMain} = require('electron')
const path = require('path')
function createWindow () {
const mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
const menu = Menu.buildFromTemplate([
{
label: app.name,
submenu: [
{
click: () => mainWindow.webContents.send('update-counter', 1),
label: 'Increment',
},
{
click: () => mainWindow.webContents.send('update-counter', -1),
label: 'Decrement',
}
]
}
])
Menu.setApplicationMenu(menu)
mainWindow.loadFile('index.html')
// 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')
contextBridge.exposeInMainWorld('electronAPI', {
handleCounter: (callback) => ipcRenderer.on('update-counter', callback)
})
<!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.handleCounter((event, value) => {
const oldValue = Number(counter.innerText)
const newValue = oldValue + value
counter.innerText = newValue
event.sender.send('counter-value', newValue)
})
1. Отправка сообщений с помощью модуля webContents
Для этой демонстрации нам сначала нужно создать пользовательское меню в основном процессе с использованием модуля Electron Menu, который использует API webContents.send для отправки сообщения IPC из основного процесса в целевой рендерер.
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')
}
//...
Для целей данного учебника важно отметить, что обработчик click отправляет сообщение (либо 1, либо -1) в процесс рендерера через канал update-counter.
click: () => mainWindow.webContents.send('update-counter', -1)
Убедитесь, что вы загружаете точки входа index.html и preload.js для следующих шагов!
2. Экспонирование ipcRenderer.on через предустановку
Как и в предыдущем примере взаимодействия рендерер-в-основной, мы используем модули contextBridge и ipcRenderer в скрипте предустановки для экспонирования функциональности IPC для процесса рендерера:
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
onUpdateCounter: (callback) => ipcRenderer.on('update-counter', callback)
})
После загрузки скрипта предустановки процесс рендерера должен иметь доступ к функции-обработчику window.electronAPI.onUpdateCounter().
Мы не экспонируем напрямую весь API ipcRenderer.on по соображениям безопасности. Убедитесь, что доступ рендерера к API Electron ограничен по возможности.
В случае этого минимального примера вы можете вызвать ipcRenderer.on напрямую в скрипте предустановки вместо экспонирования через мост контекста.
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
})
})
Однако, этот подход имеет ограниченную гибкость по сравнению с экспонированием ваших API предустановки через мост контекста, так как ваш обработчик не может напрямую взаимодействовать с вашим кодом рендерера.
3. Создание пользовательского интерфейса процесса рендерера
Для объединения всего этого, мы создадим интерфейс в загруженном HTML-файле, который содержит элемент #counter, который мы будем использовать для отображения значений:
<!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>
Наконец, чтобы обновить значения в HTML-документе, мы добавим несколько строк манипуляции DOM, чтобы значение элемента #counter обновлялось всякий раз, когда мы вызываем событие update-counter.
const counter = document.getElementById('counter')
window.electronAPI.onUpdateCounter((_event, value) => {
const oldValue = Number(counter.innerText)
const newValue = oldValue + value
counter.innerText = newValue
})
В приведенном коде мы передаем обратный вызов в функцию window.electronAPI.onUpdateCounter, экспонированную нашим скриптом предустановки. Второй параметр value соответствует значению 1 или -1, которые мы передавали в вызов webContents.send из нативного меню.
Необязательно: возврат ответа
Для главного-в-рендеринг IPC нет эквивалента для ipcRenderer.invoke. Вместо этого вы можете отправить ответ обратно в основной процесс из callback ipcRenderer.on.
Мы можем продемонстрировать это с небольшими изменениями в коде из предыдущего примера. В процессе рендерера используйте параметр event, чтобы отправить ответ обратно в основной процесс через канал counter-value.
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)
})
В основном процессе прослушивайте события counter-value и обрабатывайте их соответствующим образом.
//...
ipcMain.on('counter-value', (_event, value) => {
console.log(value) // will print value to Node console
})
//...
Шаблон 4: Рендерер-в-рендерер
Нет прямого способа отправлять сообщения между процессами рендерера в Electron с использованием модулей ipcMain и ipcRenderer. Для достижения этой цели у вас есть два варианта:
- Используйте основной процесс в качестве посредника для сообщений между рендерерами. Это подразумевает отправку сообщения от одного рендерера в основной процесс, который перенаправит сообщение другому рендереру.
- Передайте MessagePort из основного процесса в оба рендерера. Это позволит осуществить прямое общение между рендерерами после первоначальной настройки.
Сериализация объектов
Реализация IPC в Electron использует стандарт HTML алгоритм структурированного клонирования для сериализации объектов, передаваемых между процессами, что означает, что только определённые типы объектов могут передаваться через каналы IPC.
В частности, DOM-объекты (например, Element, Location и DOMMatrix), объекты Node.js, основанные на классах C++ (например, process.env, некоторые члены Stream) и объекты Electron, основанные на классах C++ (например, WebContents, BrowserWindow и WebFrame) не сериализуются с помощью алгоритма структурированного клонирования.
© GitHub Inc.
Licensed under the MIT license.
https://www.electronjs.org/docs/latest/tutorial/ipc