014 Коммуникация между процессами IPC
Реализуем получение данных на фронте от мэин-процесса
Электрон предоставляет нам глобальную шину IPC (Item Process Communication), которая реализует общение серверной и клиентской части приложения. Практически любой процесс может открыть канал, который будут слушать все подписанные на этот канал процессы
Сообщения с бэка в рендерер-процесс могут быть посланы через webContents.send
. Уже через ipcRenderer
можно отправить запрос из рендерер-процесса на бэк (в основной процесс) либо слушать сообщения по какому-то каналу.
Попробуем отправить сообщение из мэин-процесса в рендерер-процесс:
Тут нужно отметить, что мы создаём канал для конкретного окна window.webContents.send
- тут окно window
main > index.js
import { app, BrowserWindow } from "electron";
app.on("ready", () => {
let window = new BrowserWindow({
width: 1280,
height: 720,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
},
});
window.webContents.loadFile("renderer/index.html");
// отправляем сообщение по нашему каналу
window.webContents.on("did-finish-load", () => {
window.webContents.send("mainchannel", { message: "App is running" });
});
window.on("ready-to-show", () => {
window.show();
});
});
ipcRenderer
тут принимает в себя два аргумента: event
- информация о самом событии (сообщении) и data
- само переданное сообщение
renderer > index.js
import { ipcRenderer } from "electron";
require("application.css");
ipcRenderer.on("mainchannel", (event, data) => {
alert(data.message);
});
Реализуем получение данных от рендерер-процесса на мэин-процессе
Тут нам уже понадобится общая шина на все рендерер-процессы, из которой можно будет подписаться в мэйне. Конкретно этим модулем будет ipcMain
Тут уже главное отличие заключается в том, что мы создаём канал через ipcRenderer
Создаём канал в рендерер-процессе, из которого отправим данные на “сервер”
renderer > index.js
import { ipcRenderer } from "electron";
require("application.css");
ipcRenderer.on("mainchannel", (event, data) => {
alert(data.message);
});
// срабатывание функции при загрузке приложения
window.onload = () => {
const action = document.querySelector(".login");
action.addEventListener("click", () => {
ipcRenderer.send("action", { message: "hello!" });
});
};
main > index.js
import { app, BrowserWindow, ipcMain } from "electron";
ipcMain.on("action", (event, data) => {
console.log("Message from Renderer Process" + data.message);
});
Реализуем систему запросов и ответов между основным и рендерер-процессом
Конкретно нам нужно:
- с фронта отправить запрос на бэк
- чтобы бэк обработал информацию
- и вернул нам этот запрос обратно
- на фронте мы должны получить и вывести эти данные пользователю
В коде ниже представлена реализация отправки данных. По нажатию на кнопку мы вызываем метод loadAndDisplayData
, внутри которого данные отправятся и примутся через определённый промежуток времени. Внутри loadAndDisplayData
мы вызываем loadData
и тут сталкиваемся с проблемой, что нам нужно получить данные, которые приходят в метод листенера - а именно в ipcRenderer.once()
.
renderer > index.js
import { ipcRenderer } from "electron";
require("application.css");
ipcRenderer.on("mainchannel", (event, data) => {
alert(data.message);
});
const loadAndDisplayData = () => {
loadData();
};
const loadData = () => {
// создаём канал loaddata
ipcRenderer.send("loaddata");
// получаем данные только однажды, так как срабатывание происходит при нажатии кнопки
// мы так же могли использовать по отдельности методы: on и removeListener, но так как срабатывание нам нужно однажды, то используем once
ipcRenderer.once("data", (event, data) => {
//// return data // мы не можем так вернуть данные в loadAndDisplayData
});
};
// срабатывание функции при загрузке приложения
window.onload = () => {
const action = document.querySelector(".login");
action.addEventListener("click", loadAndDisplayData);
};
Чтобы решить проблему, нам нужно воспользоваться Promise API
, который и вернёт нам вызванные данные
renderer > index.js
import { ipcRenderer } from "electron";
require("application.css");
// Этот метод отправляет запрос на получение данных в loadData и выводит полученное значение на экран
const loadAndDisplayData = () => {
// примет данные из resolve прошлой функции
loadData().then((data) => {
// вставляем сразу полученные данные в нашу форму
document.querySelector(".backedMessage").innerHTML = data.number;
});
};
// Этот метод занимается только получением данных из основного процесса
const loadData = () => {
// тут мы возвразаем промис, из которого можно сразу получить результат в днругой функции
return new Promise((resolve, reject) => {
ipcRenderer.send("loaddata");
ipcRenderer.once("data", (event, data) => resolve(data));
});
};
// срабатывание функции при загрузке приложения
window.onload = () => {
// получаем доступ к кнопке
const action = document.querySelector(".login");
// кнопка будет активировать функцию загрузки и вывода
action.addEventListener("click", loadAndDisplayData);
};
В основном процессе генерируем новое значение и возвращаем его в рендерер процесс
main > index.js
import { app, BrowserWindow, ipcMain } from "electron";
app.on("ready", () => {
let window = new BrowserWindow({
width: 1280,
height: 720,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
},
});
window.webContents.loadFile("renderer/index.html");
window.on("ready-to-show", () => {
window.show();
});
// Реализуем притяние данных данных из renderer
ipcMain.on("loaddata", () => {
const number = Math.random() * 10;
// отправляем обратно в renderer по каналу data, так как по нему renderer слушает сообщения
window.webContents.send("data", { number }); // передаём число
});
});
Так же хорошей практикой будет прописывать обработку ошибок на случай непредвиденных ситуаций
renderer > index.js
const loadAndDisplayData = () => {
loadData().then((data) => {
document.querySelector(".backedMessage").innerHTML = data.number;
}).catch(error => {
console.log("Сообщение не было получено" + error.message);
});
};
015 Модуль remote
На данный момент этот модуль полностью закрыт в новых версиях Electron. Вместо
remote
нужно использовать preload-скрипты
С помощью модуля remote
, мы можем пользоваться определёнными модулями из основного процесса
// импортируем модуль remote
import { remote } from "electron";
require("application.css");
// получаем из него только нужные для нас процессы основного процесса
const { dialog } = remote;
window.onload = () => {
const action = document.querySelector(".login");
action.addEventListener("click", () => {
// выводим сообщение
dialog.showMessageBox({ message: "You have clicked the button" });
// Выводим сообщение об ошибке
// заголовок и текст
dialog.showErrorBox("Error", "Unknown error");
});
};
Ну и так же можно получить остальные элементы
import { remote } from "electron";
const { app, dialog, BrowserWindow } = remote;
require("application.css");
window.onload = () => {
const action = document.querySelector(".login");
action.addEventListener("click", () => {
// при нажатии на кнопку будет создаваться новое окно
let win = new BrowserWindow({
width: 500,
height: 500,
});
// тут сработает выход
app.quit();
});
};
016 Preload Script
Эта настройка откроет devTools отдельным окном
window.webContents.openDevTools({ mode: "detach" });
Как и упоминалось ранее - использовать открытого доступа к файловой системе из инструментов разработчика - не есть хорошая практика. Вместо этого стоит использовать preload-скрипт, который уже, в свою очередь, предоставит нам нужный функционал нашей системе.
Уберём свойство nodeIntegration
и заменим его подключением preload
main > index.js
import path from "path";
import { app, BrowserWindow } from "electron";
app.on("ready", () => {
let window = new BrowserWindow({
width: 1280,
height: 720,
webPreferences: {
// Тут указываем путь к прелоаду: путь к программе - папка прелоада - js в прелоаде
preload: path.join(app.getAppPath(), "preload", "index.js"),
},
});
window.webContents.loadFile("renderer/index.html");
// откроем девтулз отдельно от приложения
window.webContents.openDevTools({ mode: "detach" });
window.on("ready-to-show", () => {
window.show();
});
});
Самое главное, что мы получили сейчас - это отсутствие доступа к модулям nodejs и файловой системе из devTools
Предназначен этот модуль для создания узких, контролируемых интерфейсов через которые renderer
сможет взаимодействовать с Node.js API
чекер_сети
Конкретно сейчас мы реализуем возможность определять, подключено ли приложение к сети. В preload
мы запишем функционал, который будет создавать два канала, оповещающих о том, что приложение подключено к интернету. В main
процессе мы будем подписываться на эти два канала и выводить сообщение о том, что приложение подключено или отключено от интернета
Чтобы проверить работоспособность, кликни сюда:devtools
preload > index.js
import { ipcRenderer } from "electron";
document.addEventListener("DOMContentLoaded", () => {
window.addEventListener("offline", () => {
ipcRenderer.send("offline");
});
window.addEventListener("online", () => {
ipcRenderer.send("online");
});
});
main > index.js
import path from "path";
import { app, BrowserWindow, ipcMain } from "electron";
ipcMain.on("offline", () => {
console.log("App is offline");
});
ipcMain.on("online", () => {
console.log("App is online");
});
Так же мы можем настраивать логику работы приложения, если у пользователя нет интернета. Например, когда пользователь что-то делает на компьютере, то мы сохраняем данные на компьютер, а когда интернет появляется, мы можем синхронизировать эти данные с сервером. Это, по сути, пример использования API renderer процесса в main процессе с помощью preload скрипта
main > index.js
import path from "path";
import { app, BrowserWindow, ipcMain } from "electron";
let online;
ipcMain.on("offline", () => {
online = false;
console.log("App is offline");
});
ipcMain.on("online", () => {
online = true;
console.log("App is online");
});
const createWindow = () => {
let window = new BrowserWindow({
width: 1280,
height: 720,
webPreferences: {
preload: path.join(app.getAppPath(), "preload", "index.js"),
},
});
window.webContents.loadFile("renderer/index.html");
window.webContents.openDevTools({ mode: "detach" });
window.on("ready-to-show", () => {
window.show();
});
};
app.on("ready", () => {
createWindow();
if(online) {
// что-то делать
} else {
// сообщить пользователю, что нет интернета
}
});
Теперь передаём процессы из preload
в renderer
процесс.
В прелоад подключим contextBridge
, который позволит создать псевдоним для передаваемых функций и вложим в эти свойства функции, которые доступны только на бэке (в ноде).
preload > index.js
import { shell, contextBridge } from "electron";
// и сюда вкладываем функции
// первым аргументом идёт тот объект, который попадёт в рендерер-процесс
// прямого доступа к функциям ноды в рендерере не будет - только доступ к нашим функциям
contextBridge.exposeInMainWorld("URLWorker", {
// будет просто показывать версию
node: () => process.versions.node,
chrome: () => process.versions.chrome,
electron: () => process.versions.electron,
// будет открывать ссылки в браузере
openURL: (url) => shell.openExternal(url),
});
renderer > index.js
require("application.css");
window.onload = () => {
const button = document.querySelector(".login");
button.addEventListener("click", () => {
// выведет версию испольхуемого хрома
// вызвается функция при обращении к нашему заданному псевдониму в прелоаде
console.log(URLWorker.chrome());
// откроет ссылку в браузере
// URLWorker.openURL('https://ru.reactjs.org/docs');
});
};
017 API Браузера
В renderer
процессе мы имеем доступ ко всем функциям браузера, которые мы можем писать в нативном JS
require("application.css");
const updateOnlineStatus = () => {
document.getElementById("status").innerHTML = navigator.onLine
? "online"
: "offline";
};
window.addEventListener("online", updateOnlineStatus);
window.addEventListener("offline", updateOnlineStatus);
updateOnlineStatus();
Тут можно эмулировать отключение интернета
Так же нужно сказать, что так как мы работаем с бнраузерным API, то у нас есть доступ к веб SQL, локальному кэшу и остальным уникальным функциям браузеров
Объект Notification
создаёт системное уведомление. Мы можем вызывать его как API браузера на нашем ПК.
require("application.css");
window.onfocus = () => {
window.addEventListener("online", () => {
const alert = new Notification("Electron App", {
body: "you are in online",
// уведомление будет без звукового сопровождения
silent: true,
});
});
window.addEventListener("offline", () => {
const alert = new Notification("Electron App", {
body: "you are in offline",
});
});
};
Так как мы пишем приложение в одном браузере, то, как упомяналось ранее, мы работаем только над одним проектом и можем не думать над кросс-браузерностью