Руководство по созданию настольного приложения в Electron

Если вы занимаетесь веб-разработками, то, вероятно, наслышаны об Electron. Для тех же, кто не в курсе, скажу, что это достаточно противоречивый фреймворк для создания приложений, совместимых с Windows, macOS и Linux, на основе веб-технологий и единой базы кода. Несмотря на то, что его приложения могут уступать нативным в скорости и легковесности, Electron позволяет веб-разработчикам развертывать их на 3 главных PC платформах, а, значит, экономит наше время и деньги. Фактически ряд широко распространенных настольных приложений, включая Visual Studio Code, Slack и Discord, созданы с помощью Electron. 

В данном руководстве мы создадим простое приложение Electron. Предварительно вам необходимо ознакомиться с Node.js, лежащей в основе рассматриваемого фреймворка. Помимо этого не обойтись без первоначальных знаний о библиотеке Express. И в качестве бонуса используем TypeScript вместо JavaScript, после чего вы, вряд ли, захотите вернуться к JS.

При желании можете обратиться к моему репозиторию за более подробным материалом о приложении Electron с фронтендом Angular

Введение 

По сути, приложения Electron работают по той же фронтенд/бэкенд модели, что и большинство веб-приложений. Отличие же в том, что фронтенд-часть выполняется не в браузере, а в своем отдельном окне, при этом бэкенд-компонент является приложением Node, выполняющимся локально, а не на сервере. 

Процесс создания приложения можно разбить на несколько основных этапов: 

  1. Настройка среды
  2. Настройка TypeScript
  3. Создание бэкенда 
  4. Создание фронтенда 
  5. Тестирование приложения 
  6. Сборка приложения 

О безопасности: внашем примере для обеспечения взаимодействия фронтенда и бэкенда послужит сервер Express, что позволит нам оптимизировать процесс разработки и легко перенести в Electron уже имеющиеся у вас веб-приложения. Однако в производственной среде с этой целью всегда применяется Electron IPC.

Итак, приступим. 

1. Настройка среды 

Сначала убедитесь, что на вашем компьютере установлен Node. 

Затем создайте новый каталог для приложения Electron и, запустив терминал, перейдите в него. Теперь для инициализации нового проекта Node выполните npm init. Следуйте указаниям, выбирая варианты по умолчанию, если не уверены в ответе. 

Примечание: по желанию на данном этапе вы также можете настроить систему контроля версий Git. Сделав это, убедитесь, что она игнорирует папки /dist и /build, поскольку в них будет находиться транслированный код и скомпилированные исполняемые файлы. 

Установка зависимостей 

Для установки необходимых зависимостей выполните из корневого каталога проекта следующие команды:

npm i --save-dev electron electron-packager typescript ts-node-dev
npm i express @types/express

2. Настройка TypeScript 

В папке проекта создайте файл с именем tsconfig.json и скопируйте в него следующее: 

{
    "compilerOptions": {
      "target": "es6",
      "module": "commonjs",
      "strict": true,
      "esModuleInterop": true,
      "skipLibCheck": true,
      "forceConsistentCasingInFileNames": true,
      "outDir": "dist",
      "experimentalDecorators": true
    },
    "include": [
      "src/**/*.ts"
    ],
    "exclude": [
      "node_modules"
    ]
  }

compilerOptions  —  это набор опций, благодаря которым TypeScript понимает, как интерпретировать код и компилировать его на JavaScript. Однако, признавая TypeScript отличным языком программирования, не стоит забывать, что его необходимо компилировать на старый добрый JavaScript перед применением в производственной среде. Мы не будем подробно останавливаться на каждой опции, обратим лишь внимание на одну из них  —  outDir. Именно она сообщает место сохранения окончательного кода JS. В данном руководстве TS будет размещать код в директории dist, расположенной в корневом каталоге проекта. 

Опция include информирует транспилятор, где искать файлы с кодом TS, а опция exclude явно указывает, какие компоненты исключить. 

Исходные директории 

Обратите внимание, что опция include в tsconfig.json ссылается на директорию src, которую нам еще предстоит создать в каталоге проекта. Именно здесь будет размещаться весь бэкенд-код. Также нам понадобится директория frontend:

Внешний вид папки проекта

Преобразования package.json 

Нам потребуется изменить файл package.json. Для этого откройте его и замените содержимое "scripts" на следующее: 

"scripts": {
    "transpile": "tsc",
    "watch": "ts-node-dev --respawn --poll ./src/main.ts",
    "app": "electron .",
    "build": "electron-packager . --out=build --overwrite"
},

Эти скрипты определяют команды, которые мы можем выполнять в проекте. Команда transpile транспилирует код TypeScript в JavaScript. 

watch осуществляет постоянный контроль кода TypeScript и в случае внесения изменений автоматически выполняет его напрямую без предварительной транспиляции в JavaScript. Во время тестирования данная процедура может оказаться очень полезной. 

Команда app запускает приложение Electron для тестирования, а build компилирует его окончательную выполняемую версию для текущей платформы, сохраняя ее в директории build

Все три команды можно выполнить в терминале в следующем формате: npm run <command>, например npm run transpile.

Наконец, изменим значение свойства "main" на "dist/main.js", благодаря чему Electron запустит файл main.js в своем главном процессе. Это не означает, что мы будем самостоятельно писать данный файл  —  он будет чуть позже транспилирован из кода TypeScript. 

3. Создание бэкенда 

Обычный бэкенд Node содержит лишь код для обработки HTTP-запросов от клиента. Помимо этого, бэкенд Electron содержит еще и код для обработки самого процесса Electron. 

Создадим новый файл main.ts в директории src, скопировав в нее следующий код: 

// Импортируем модули 
import { app, BrowserWindow } from 'electron'
import express from 'express'
import path from 'path'

// Произвольный порт для Electron 
const PORT = 7259

// Окно приложения Electron 
let win: BrowserWindow | null

// Функция для создания нового окна приложения 
const createWindow = () => {
  win = new BrowserWindow({
    width: 600,
    height: 600,
    backgroundColor: '#ffffff',
    autoHideMenuBar: true,
  })
  // Загружаем файл HTML из каталога frontend 
  win.title = 'Hello World | Electron'
  win.loadURL(`http://localhost:${PORT}`)
  win.on('closed', () => {
    win = null
  })
}

// Настройка обратных вызовов GUI для разных событий 

// Код помещен в конструкцию if, позволяя проводить тестирование в браузере во время разработки 
if (app) {
  // В момент инициализации приложения вызываем  createWindow 
  app.on('ready', () => {
    createWindow()
  })
  // При закрытии всех окон приложения выходим из него (не касается //MacOS) 
  app.on('window-all-closed', () => {
    // Даже когда все окна закрыты, приложения MacOS обычно продолжают выполняться в фоновом режиме, пока пользователь явно не выйдет из приложения через панель Dock. 
    if (process.platform !== 'darwin') {
      app.quit()
    }
  })
  // Когда приложение, выполнявшееся в фоновом режиме, "активировано", вызываем  createWindow 
  // Необходимость этого объясняется вышеупомянутой особенностью MacOS выполнять приложения при закрытых окнах 'window-all-closed' 
  app.on('activate', () => {
    if (win === null) {
      createWindow()
    }
  })
}

// Инициализируем сервер Express 
const expressApp: express.Application = express()

// Проверяем доступность каталога frontend для клиентов 
expressApp.use(express.static(path.join(__dirname, "../frontend")))

// Конечная точка Hello World 
expressApp.get("/api/hello", async (req, res) => {
  res.status(200).send('Hi! Electron is pretty cool.')
})

// Добавляем при необходимости другие конечные точки 

// Перенаправляем все другие точки во frontend 
expressApp.all("*", async (req, res) => {
  res.sendFile(path.join(__dirname, "../frontend/index.html"))
})

// Запускаем сервер Express 
expressApp.listen(process.env.PORT || PORT, () => {
  console.log(`Running (port ${process.env.PORT || PORT})`)
})

Давайте разберем этот код. Сначала мы импортируем необходимые модули:

  • app и BrowserWindow из electron. Первый  —  для взаимодействия с приложением, а второй  —  для управления GUI.
  • express и path необходимы для обслуживания фронтенда.

Далее устанавливаем переменную const PORT для настройки порта, который будет прослушиваться сервером. Это может быть любое произвольное число. Однако советую выбирать числа в диапазоне 49152–65535 во избежание конфликтов с зарезервированными или часто используемыми портами. 

На этом этапе уже начинается работа с Electron. Сначала определяем переменную win, которая будет содержать объект, представляющий окно приложения. В случае отсутствия окон приложения ее значением также может быть null.

Затем определяем функцию createWindow для создания нового окна приложения и сохраняем ее в переменной win. Применяя разные опции, задаем размер и имя окна, а функция win.loadURL дает окну команду загрузить и отобразить localhost:7259, откуда и предоставляется HTML-файл фронтенда, который мы вскоре создадим. В завершении этого этапа определяем в этой функции обратный вызов win.on, который при закрытии окна устанавливает значение объекта win обратно на null.

До настоящего момента наш код сообщал Electron, что и как нужно делать с окном приложения, но ничего не говорил про реализацию работы самого приложения. Этой цели послужат строки с 30 по 50. С помощью app.on они определяют 3 функции обратных вызовов, которые срабатывают при определенных событиях. 

При начальной инициализации процесса приложения Electron запускается событие ready, вслед за чем мы создаем новое окно приложения и отображаем его, используя функцию createWindow

Как только закрываются все ранее открытые окна, запускается событие window-all-closed. В Windows или Linux это говорит о том, что пользователь намерен окончательно выйти из приложения, поэтому мы вызываем app.quit для завершения его работы. Однако в macOS приложения могут выполняться в фоновом режиме, пока пользователь не закроет их напрямую через панель Dock, поэтому этот особый случай учитывается в конструкции if.

В завершении этапа происходит обработка события activate, которое вызывается при переходе приложения из фонового режима на передний план. Очевидно, что это характерно только для macOS, поскольку только данная платформа позволяет приложению изначально выполняться в фоновом режиме. При запуске этого события функцией createWindow еще раз создается новое окно приложения. 

Поскольку вы знакомы с Node и Express, мы не будем рассматривать код после 52 строки. 

Итак, у нас есть необходимый код для запуска приложения Electron и сервера Express в фоновом режиме. Но нам по-прежнему требуется фронтенд для взаимодействия с сервером. 

4. Создание фронтенда 

Обратите внимание, что в ранее рассмотренном коде упоминается файл index.html. Вы, конечно, можете применять любые фронтенд-технологии на свой вкус, однако для целей данного руководства достаточно одного самого простого HTML-файла. 

Скопируйте следующий код в файл index.html, расположенный в корневом каталоге проекта. 

<!DOCTYPE html>
<html lang="en">

<head>
	<meta http-equiv="content-type" content="text/html; charset=utf-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>Electron Hello World</title>
	<style>
		@media (prefers-color-scheme: dark) {
			html {
				background-color: #222222;
				color: white;
			}
		}
	</style>
</head>

<body style="font-family: Arial, Helvetica, sans-serif;">
	<div>
		<button id="messageButton" type='button' onclick='loadDoc()'>Get Message</button>
		<h1 id='heading'></h1>
	</div>

	<script>
		const loadDoc = () => {
			var xhttp = new XMLHttpRequest();
			document.getElementById('messageButton').disabled = true
			xhttp.onreadystatechange = (res) => {
				if (res.target.readyState == 4) {
					document.getElementById('messageButton').disabled = false
				}
				if (res.target.readyState == 4 && res.target.status == 200) {
					document.getElementById('messageButton').disabled = true
					document.getElementById('heading').innerHTML = res.target.responseText;
					setTimeout(() => {
						document.getElementById('messageButton').disabled = false
						document.getElementById('heading').innerHTML = ''
					}, 5000);
				}
			};
			xhttp.open('GET', 'http://localhost:7259/api/hello', true);
			xhttp.send();
		}
	</script>
</body>

</html>

Здесь нет ничего особенного  —  обычная HTML-страница с простым стилем для взаимодействия с бэкендом. Отметим лишь, что любой HTTP-запрос, осуществляемый фронтендом, должен иметь правильный номер порта. 

5. Тестирование приложения 

На этом этапе приложение готово для сборки. Приступайте к генерации исполняемого файла, только убедившись, что все работает, как нужно. 

Запускаем код командой npm run watch и внимательно следим за его изменением. Затем открываем в браузере файл index.html. Несмотря на то, что приложения Electron обычно в браузере не выполняются, по своей сути, они являются веб-приложениями, поэтому в целях удобства тестирования этот вариант вполне допустим. Electron основан на движке Chromium, в связи с чем желательно использовать Chrome, Edge или другие браузеры на базе Chromium. 

Протестируйте приложение и по мере необходимости вносите любые изменения. Не забывайте обновлять окно браузера после корректировки фронтенд-кода.

Технически запуск приложения в браузере ничем не отличается от аналогичного процесса в контейнере Electron, в чем вы сами можете убедиться, запустив приложение в контейнере командой в npm run app.

Убедившись, что все работает, переходите к следующему разделу. 

6. Сборка приложения 

Итак, все части пазла на своих местах, и осталось только скомпилировать приложение для конечных пользователей. Выполняем npm run transpile, чтобы убедиться, что последняя версия кода преобразована в JavaScript. После этого с помощью npm run build собираем окончательную выполняемую версию приложения. Обратите внимание, что таким образом вы создаете приложение для текущей версии ОС, но, изучив опции electron-packager, вы узнаете и способы компиляции для других платформ, что позволит внести соответствующие корректировки в скрипт package.json.

Скомпилированное приложение должно находиться в новой директории build.

Запуск скомпилированного приложения

Заключение

Надеюсь, что благодаря этому руководству вы научились разрабатывать настольные приложения в Electron.

Благодарю за внимание!

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Imran Remtulla: Dive Into Electron

Предыдущая статья4 типа архитектуры программного обеспечения
Следующая статьяSQLite: как организовывать таблицы