Эта статья подготовлена для блога AG Grid Ареком Наво
Electron — это кроссплатформенный фреймворк для создания нативных настольных приложений с использованием веб-технологий. Он построен на базе Node.js и браузера Chromium, что позволяет использовать все последние возможности и улучшения браузера и языка JavaScript.
Electron позволяет вам создать наилучший пользовательский опыт, одновременно заботясь обо всех сложных деталях создания нативного приложения. С помощью Electron вы можете повторно использовать свои знания и кодовую базу в Интернете и на всех настольных платформах. Если вы уже умеете разрабатывать фронтенд веб-приложений и бэкенд Node.js, вы будете чувствовать себя как дома с Electron, поскольку он, по сути, объединяет эти два компонента в одно приложение.
В этой статье вы узнаете, как интегрировать AG Grid — продвинутую и производительную библиотеку сеток JavaScript — в ваше приложение Electron. Вы создадите простое приложение для выполнения дел со встроенной функциональностью для сохранения и восстановления его состояния из файла JSON.
Вы можете следить за ходом работы с помощью этого репозитория GitHub.
Как работает Electron?
Базовое приложение Electron состоит из двух процессов. Главный процесс действует как точка входа для приложения, имея доступ к API Node.js и модулям (включая встроенные). Он также контролирует жизненный цикл приложения и управляет его окнами с помощью API, предоставляемых Electron.
Таким образом, если основной процесс похож на бэкенд вашего веб-приложения, то процесс рендеринга больше похож на фронтенд. Он отвечает за рендеринг пользовательского интерфейса приложения и запускается в каждом открытом окне. Таким образом, он должен следовать веб-стандартам (таким как HTML5, CSS3 и ECMAScript) и использовать веб-интерфейсы браузера. Никакой код, специфичный для Node, не допускается.
Для взаимодействия между процессами (например, для обработки передачи данных или вызова собственных функций из пользовательского интерфейса) можно использовать скрипты предварительной загрузки и межпроцессное взаимодействие (IPC). Скрипты предварительной загрузки запускаются в процессе рендеринга перед основным скриптом и имеют доступ к API Node.js. Вы можете использовать их с модулем contextBridge
Electron, чтобы безопасно открывать привилегированные API для процесса рендеринга. В частности, вы можете создавать помощников, используя модуль ipcRenderer
для взаимодействия с основным процессом.
Использование AG Grid с Electron
Хорошим способом начать работу с Electron является Electron Forge — «полный инструмент для создания, публикации и установки современных приложений Electron». В Electron Forge есть все необходимое для работы над приложением Electron, включая шаблон на базе Webpack.
Настройка проекта
Чтобы начать новый проект в Electron Forge, убедитесь, что у вас установлен git и Node.js версии 12.13.0 или новее. Затем выполните следующие команды для создания проекта, установки дополнительных зависимостей и запуска сервера разработки:
npx create-electron-app@latest project --template=webpack
cd project
npm install ag-grid-community
npm run start
Управление окном приложения
По умолчанию шаблон включает файлы для основного процесса и процесса рендеринга. Внутри файла src/main.js вы увидите начальную точку вашего приложения. Ниже мы рассмотрим функцию createWindow()
и слушателей событий:
const { app, BrowserWindow } = require("electron");
// ...
const createWindow = () => {
const mainWindow = new BrowserWindow();
mainWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY);
mainWindow.webContents.openDevTools();
};
app.on("ready", createWindow);
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
Модуль app
управляет жизненным циклом событий вашего приложения. Когда приложение готово, оно создает новое окно, вызывая createWindow()
, которая в свою очередь создает новый экземпляр BrowserWindow
. Используя метод loadURL()
, окно загружает содержимое, обслуживаемое сервером разработки Webpack. Оно также открывает инструменты разработки для облегчения отладки с помощью метода webContents.openDevTools()
.
Другие слушатели событий обрабатывают специфические для macOS побочные ситуации, такие как сохранение приложения открытым без окон (window-all-closed
) или открытие нового окна при активации из дока (activate
).
Добавление сценария предварительной загрузки
Чтобы обеспечить доступ к нативным API из процесса рендеринга, вам придется раскрыть некоторые функции в скрипте предварительной загрузки. Хотя шаблон не включает его по умолчанию, добавить его самостоятельно очень просто.
Создайте новый файл src/preload.js и отредактируйте поле config.forge.plugins
в package.json
, чтобы сообщить Electron Forge о скрипте предварительной загрузки:
{
/* ... */
"config": {
"forge": {
"packagerConfig": {},
/* ... */
"plugins": [
[
"@electron-forge/plugin-webpack",
{
"mainConfig": "./webpack.main.config.js",
"renderer": {
"config": "./webpack.renderer.config.js",
"entryPoints": [
{
"html": "./src/index.html",
"js": "./src/renderer.js",
"preload": {
"js": "./src/preload.js"
},
"name": "main_window"
}
]
}
}
]
]
}
}
/* ... */
}
Чтобы инициализировать скрипт, укажите его при создании BrowserWindow
в файле src/main.js с помощью глобальной переменной, предоставляемой Webpack:
// ...
const createWindow = () => {
const mainWindow = new BrowserWindow({
webPreferences: {
preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY,
},
});
// ...
};
// ...
Создание пользовательского интерфейса процесса рендеринга
Когда основное управление окнами готово, можно приступать к работе над процессом рендеринга. Вы будете использовать AG Grid для создания простого списка дел с возможностью добавлять, удалять и отмечать выполненные пункты.
Разработка процесса рендеринга очень похожа на создание фронтенд веб-приложения. Вы можете использовать все фронтенд-фреймворки и API, доступные в среде браузера. В этом руководстве вы будете использовать обычный HTML, JS и CSS, чтобы все было просто.
Начните с создания структуры пользовательского интерфейса в файле src/index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>AG Grid + Electron</title>
</head>
<body>
<div class="container">
<h1>Electron TO-DO</h1>
<div class="btn-container">
<button id="save-btn" class="btn">Save</button>
<button id="restore-btn" class="btn">Restore</button>
<button id="add-btn" class="btn add-btn">Add</button>
</div>
<div class="divider"></div>
<div id="grid" class="ag-theme-alpine"></div>
</div>
</body>
</html>
Затем добавьте необходимые стили в src/index.css :
html,
body {
height: 100%;
margin: 0;
}
#grid {
width: 100%;
flex: 1;
}
.container {
margin: auto;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
height: calc(100% - 2rem);
padding: 1rem;
width: 30rem;
}
.btn-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 0.5rem;
width: 100%;
}
.divider {
margin: 0.5rem 0;
background: #e6e6e6;
height: 0.125rem;
}
.checkbox {
height: 100%;
margin: 0;
width: 1.25rem;
}
.btn {
flex: 1;
padding: 0.5rem;
background: #6b7280;
border: none;
color: #fff;
font-size: 1rem;
}
.btn:hover {
background: #9ca3af;
cursor: pointer;
}
.add-btn {
padding: 0.5rem;
background: #f97316;
border: none;
}
.add-btn:hover {
background: #fb923c;
cursor: pointer;
}
.remove-btn {
display: inline-flex;
max-height: 1.25rem;
max-width: 1.25rem;
font-size: 1.25rem;
justify-content: center;
align-items: center;
background-color: #ef4444;
}
.remove-btn:hover {
background-color: #f87171;
}
С этой настройкой вы можете перейти к файлу src/renderer.js для инициализации сетки:
import "ag-grid-community/dist/styles/ag-grid.css";
import "ag-grid-community/dist/styles/ag-theme-alpine.css";
import "./index.css";
import { Grid } from "ag-grid-community";
let rowData = [];
const columnDefs = [
// ...
];
const gridOptions = {
columnDefs,
rowData,
};
const saveBtn = document.getElementById("save-btn");
const restoreBtn = document.getElementById("restore-btn");
const addBtn = document.getElementById("add-btn");
const addTodo = () => {
// ...
};
const removeTodo = (rowIndex) => {
// ...
};
const saveToFile = () => {
// ...
};
const restoreFromFile = async () => {
// ...
};
const setupGrid = () => {
const gridDiv = document.getElementById("grid");
new Grid(gridDiv, gridOptions);
addBtn.addEventListener("click", addTodo);
saveBtn.addEventListener("click", saveToFile);
restoreBtn.addEventListener("click", restoreFromFile);
};
document.addEventListener("DOMContentLoaded", setupGrid);
Вся настройка, включая создание экземпляра Grid
и добавление обработчиков событий, происходит после загрузки DOM. Сетка создается с использованием предоставленной конфигурации, определяя ее столбцы и входные данные:
// ...
let rowData = [];
const columnDefs = [
{ field: "task", editable: true, flex: 1 },
{
field: "completed",
width: 120,
cellRenderer(params) {
const input = document.createElement("input");
input.type = "checkbox";
input.checked = params.value;
input.classList.add("checkbox");
input.addEventListener("change", (event) => {
params.setValue(input.checked);
});
return input;
},
},
{
field: "remove",
width: 100,
cellRenderer(params) {
const button = document.createElement("button");
button.textContent = "✕";
button.classList.add("btn", "remove-btn");
button.addEventListener("click", () => removeTodo(params.rowIndex));
return button;
},
},
];
// ...
Колонки определяются определенными параметрами, такими как имя поля
, ширина
, или пользовательский cellRenderer
, если вы хотите отобразить данные по-другому. editable
включает встроенную поддержку редактирования, позволяя пользователю изменять название задачи. В то же время, flex
является альтернативой width
, указывая на то, что колонка должна заполнять оставшееся пространство.
Для столбцов «выполнено» и «удалить» пользовательские рендеры ячеек отображают флажок и кнопку, чтобы, соответственно, изменить статус задачи или полностью удалить ее из списка. Фактическое изменение данных сетки осуществляется с помощью params.setValue()
и отдельной функции removeTodo()
.
И addTodo()
, и removeTodo()
работают с использованием объекта gridOptions.api
. После предоставления гриду, gridOptions
получает свойство api
, позволяющее управлять гридом:
// ...
const addTodo = () => {
rowData = [...rowData, { task: "New Task", completed: false }];
gridOptions.api.setRowData(rowData);
};
const removeTodo = (rowIndex) => {
rowData = rowData.filter((value, index) => {
return index !== rowIndex;
});
gridOptions.api.setRowData(rowData);
};
// ...
Значения из элементов rowData
также привязываются к сетке, то есть если пользователь изменит статус завершения или название задачи, новое значение будет отражено в одном из элементов rowData
.
После всех этих изменений пользовательский интерфейс приложения стал полностью функциональным и выглядит следующим образом:
Осталось только реализовать функцию сохранения и восстановления. Для этого вам придется вернуться к основному процессу.
Добавление нативной функциональности
Внутри файла src/main.js создайте новую функцию, handleCommunication()
, для обработки реализации IPC:
const { app, BrowserWindow, ipcMain, dialog } = require("electron");
// ...
const handleCommunication = () => {
ipcMain.removeHandler("save-to-file");
ipcMain.removeHandler("restore-from-file");
ipcMain.handle("save-to-file", async (event, data) => {
try {
const { canceled, filePath } = await dialog.showSaveDialog({
defaultPath: "todo.json",
});
if (!canceled) {
await fs.writeFile(filePath, data, "utf8");
return { success: true };
}
return {
canceled,
};
} catch (error) {
return { error };
}
});
ipcMain.handle("restore-from-file", async () => {
try {
const { canceled, filePaths } = await dialog.showOpenDialog({
properties: ["openFile"],
filters: [
{
name: "json",
extensions: ["json"],
},
],
});
if (!canceled) {
const [filePath] = filePaths;
const data = await fs.readFile(filePath, "utf8");
return { success: true, data };
} else {
return { canceled };
}
} catch (error) {
return { error };
}
});
};
// ...
Сначала, используя ipcMain.removeHandler()
, убедитесь, что к используемым каналам не прикреплены существующие обработчики на случай повторной активации окна (специфично для macOS). Метод ipcMain.handle()
позволяет вам обрабатывать определенные события и отвечать данными, просто возвращая значение из обработчика.
В данном приложении используются каналы "save-to-file"
и "restore-from-file"
. Их обработчики используют модуль dialog
для вызова встроенных диалогов открытия или сохранения системы. Полученные пути затем передаются встроенному в Node.js модулю fs
для чтения из файла или записи в файл.
handleCommunication()
следует вызывать из функции createWindow()
:
// ...
const createWindow = () => {
const mainWindow = new BrowserWindow({
webPreferences: {
preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY,
},
});
mainWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY);
mainWindow.webContents.openDevTools();
handleCommunication();
};
// ...
Чтобы иметь возможность отправить IPC-сообщение из процесса рендеринга, вам придется использовать скрипт предварительной загрузки и модуль contextBridge
:
// src/preload.js
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("electronAPI", {
saveToFile(data) {
return ipcRenderer.invoke("save-to-file", data);
},
restoreFromFile() {
return ipcRenderer.invoke("restore-from-file");
},
});
contextBridge.exposeInMainWorld()
безопасно раскрывает предоставленный API для процесса рендеринга. Помните, что в сценарии предварительной загрузки вы получаете доступ к привилегированным API, которые, по соображениям безопасности, должны быть свободно доступны из фронтенда вашего приложения Electron.
Открытые методы используют модуль ipcRenderer
для отправки сообщений слушателю ipcMain
на другом процессе. В случае канала "save-to-file"
предоставляются дополнительные данные в виде строки JSON для сохранения.
Когда мост готов, вы можете вернуться к процессу рендеринга и завершить интеграцию, добавив соответствующие обработчики для двух последних кнопок:
// ...
const saveToFile = () => {
window.electronAPI.saveToFile(JSON.stringify(rowData));
};
const restoreFromFile = async () => {
const result = await window.electronAPI.restoreFromFile();
if (result.success) {
rowData = JSON.parse(result.data);
gridOptions.api.setRowData(rowData);
}
};
// ...
Каждый обработчик использует методы из объекта electronAPI
, доступного на window
. В saveToFile()
, rowData
строится и отправляется в основной процесс для записи в файл. В случае операции восстановления сначала await
получает строгированное содержимое файла, чтобы затем разобрать и присвоить его сетке.
Теперь ваше приложение может использовать родной диалог файла для восстановления и сохранения своего состояния:
Итоговое приложение выводит JSON-файл, как показано ниже:
[{"task":"Learn Electron","completed":true},{"task":"Learn AG Grid","completed":false}]
Заключение
Теперь вы знаете, как использовать AG Grid в приложении Electron. Вы узнали о процессах Electron — что это такое и как они работают. Наконец, вы установили IPC для взаимодействия между ними, чтобы реализовать в вашем приложении встроенную функциональность.
Как вы видели, благодаря Electron вы можете быстро превратить свое веб-приложение в устанавливаемое нативное приложение для настольных компьютеров со всеми сопутствующими функциональными возможностями.
AG Grid — это высокопроизводительная библиотека таблиц JavaScript, которую легко настроить. Она хорошо интегрируется с вашими любимыми фреймворками, такими как React, и отлично работает с другими частями экосистемы JavaScript, такими как Electron. Ознакомьтесь с официальной документацией, чтобы узнать больше.