— Хотел бы я иметь приложение, которое умеет что-то такое, что мне надо. Жаль такого нет.
— А почему бы тебе его самому не написать?
— Это сложно, мне потребуется куча времени, чтобы понять теорию, приступить к практике и, скорее всего, ничего хорошего не выйдет. А вообще я frontend-разработчик и привык к HTML, CSS и JS. Этот стек не позволяет писать десктопные приложения.
— Electron!
Уверен, большинство из нас обмениваются скриншотами. Существует большое количество приложений, способных делать скриншоты и как-то их редактировать (обрезать, рисовать что-то итд). Первые 3, которые мне приходят на ум: Joxy, Monosnap, Gyazo.
Казалось бы, чего ещё желать, когда есть задача и инструменты для неё? Но нет, всё не так просто.
Критерии, по которым я искал приложение
Сохранение скриншота на сервере
Намного удобнее скинуть ссылку на скриншот, чем искать, куда приложение сохранило картинку, а потом прикреплять её. Конечно, некоторым не понравится идея хранить скриншоты неизвестно где. Но, с другой стороны, это позволяет быстро решить задачу — поделиться скриншотом. Такая модель поведения приложения мне вполне подходит.
Рисование на скриншоте
По своему опыту могу сказать, что стрелка у меня присутствует чуть ли не на каждом скриншоте. Намного проще указать на какую-то деталь, чем писать что-то вроде: «Вот эта вот штука между...». Также в стандартный набор инструментов для рисования на скриншоте, по моему мнению, должны входить карандаш, прямоугольник и текст. При этом должна присутствовать возможность переместить, перевернуть и удалить нарисованный элемент.
Версия для Linux
Как оказалось, не так и много приложений существует для этой задачи под Linux. Долгое время я использовал Gyazo и использовал бы дальше, если бы за «рисовашки» на скриншоте не приходилось платить.
Из коробки
Приложение должно сразу работать так как мне надо. Я не хочу что-либо настраивать, регистрироваться на всяких Imgur и т. д. Просто хочу захватить скриншот, кропнуть, сохранить и поделиться ссылкой на него.
На протяжении года я периодически занимался поиском, но в итоге так и не нашёл приложение, удовлетворяющее моим требованиям.
Electron
Время шло и я решил поближе познакомиться с Electron, чтобы понять что он умеет в принципе. Для тех кто не в курсе, Electron — технология, позволяющая создавать десктопные приложения, используя HTML, CSS и JavaScript.
И вот одной из демок демонстрировалось получение скриншота экрана пользователя. Это означало следующее:
- можно наконец-то написать приложение, отвечающее моим требованиям,
- появилась возможность познакомиться с новой для себя технологией на реальном проекте
Куда идти, с чего начать, как оно работает
Эти вопросы я задаю себе каждый раз, когда хочу изучить что-то новое. Зачастую решение одно и то же — документация и какой-нибудь официальный «Get started». У Electron и с тем, и с другим всё в полном порядке. По крайней мере они так пишут на сайте проекта — «It's easier than you think». Это действительно так! Единственное, что надо сразу понять — приложение составляют 2 процесса: Main и Renderer. В Main мы работаем с окнами приложения, меню и т.д., а в Renderer у нас по-сути просто наша страничка.
С чего начать приложение
Сперва давайте создадим проект со следующей структурой:
— package,json
— main.js
— scripts
— renderer.js
Для начала нам этого вполне достаточно. В main.js у нас будет содержаться код, относящийся к главному процессу, а в scripts -> renderer.js рендер. Теперь необходимо создать само приложение. Для этого в main.js пишем:
const electron = require('electron');
const app = electron.app;
app.on('ready', () => {
createWindow();
});
app.on('window-all-closed', function() {
app.quit();
});
Что в итоге мы сделали? Мы подключили Electron, создали само приложение и описали два обработчика. Когда наше приложение будет готово, мы вызовем функцию создания нового окна. А в тот момент, когда все окна будут закрыты — убьём процесс приложения.
Окно
Окей, теперь у нас есть сам процесс приложения. Теперь хотелось бы создать для него окно. Помните мы вызывали функцию createWindow();? Так вот пришла пора описать её.
function createWindow() {
appWindow = new BrowserWindow({
width: 300,
height: 300
});
};
Также необходимо подключить:
const BrowserWindow = electron.BrowserWindow;
Таким образом мы создали окно с заданными размерами. Очень просто, не правда ли?
У окна есть очень много свойств, методов и событий, связанных с ним. К примеру: title, icon, x, y, show(), hide(), close(), minimize, closed, restore, resize. Перечислять весь список бесполезно, поэтому вот ссылка на Browser WIndow.
Но смотреть на пустое окошко не очень весело, да? Давайте подгрузим туда какую-нибудь простую страничку, которая содержит заголовок «Hello, world!». На уровне с main.js
создаём файл index.html, который выглядит как самая обычная страница с <h1>Hello, World!</h1>
. Теперь только остаётся расширить функцию createWindow(), дописав
appWindow.loadURL(`file://${__dirname}/index.html`);
Запускаем наше приложение и видим:
Прям какая-то магия :) Но на секундочку о грустном.
Дебаг
А как же собственно отлавливать ошибки? Всё максимально просто! Если ошибка в главном процессе, то она и вывалится вам в консоль, из которой вы запускаете приложение. Для ловли ошибок на самой странице мы будем использовать браузерную консоль. Да, именно так! Наше приложение по факту является страничкой, запущенной в браузере. Чтобы включить консоль давайте допишем в createWindow();
строчку: appWindow.webContents.openDevTools();
. Теперь при запуске приложения мы сразу же увидим консоль.
Меню
Пока что всё что мы получили — это страничка в окне. Хочется как-то взаимодействовать с ней, к примеру, с помощью меню. Для этого в Electron есть специальный класс Menu. Поставим себе задачу — создать меню из одного элемента, при нажатии на который мы увидим системное всплывающее окно с тайтлом «Hello», сообщением: «Do you like this?» и вариантами ответа ['Yes', 'No'].
В main.js пишем:
const appMenu = require('./scripts/appMenu');
И создаём файл appMenu.js в директории scripts. Внутри себя он содержит следующий код:
const { shell } = require('electron');
const dialog = require('electron').dialog;
const newShotDialog = {
type: 'info',
title: 'Hello',
message: 'Do you like this?',
buttons: ['Yes', 'No']
};
module.exports = function appMenu(app, appWindow) {
return (
[
{
label: 'File',
submenu: [
{
label: 'Click me',
click() {
dialog.showMessageBox(newShotDialog, function(index) {
})
},
},
],
},
]
);
};
Идём далее. В app.on('ready'...
дописываем:
const template = appMenu(app, appWindow);
const menu = electron.Menu.buildFromTemplate(template);
electron.Menu.setApplicationMenu(menu);
После запуска приложения мы увидим, что наше меню стало меньше и теперь содержит один элемент. Нажав на него, мы увидим следующее:
Собственно, мы решили поставленную задачу. Я мог бы посвятить всю статью созданию какого-либо абстрактного приложения (например, аудио-плеера), но тогда бы она вся состояла из «какой-то код» -> «результат», что, согласитесь, не очень интересно (лично я бы закрыл вкладку и пошёл писать что-то своё). Поэтому остальную часть статьи я уделю обзору некоторых важных, на мой взгляд, возможностей Electron на примере моего проекта --shots. Не беспокойтесь, приложение, создание которого я описал выше, можно найти по ссылке
Больше возможностей Electron на примере --shots
Согласитесь, не очень удобно каждый раз заходить в меню за инструментами которые вы часто используете (кроп, стрелка и т.д.). Решить данную проблему можно созданием панельки в области страницы, на которую их и вынести. Уже удобнее (не надо пробираться через громоздкое меню), но всё ещё не идеально (тянуться-то надо).
Можно лучше?
Конечно, можно! Давайте использовать контекстное меню. Его будет вызывать клик правой кнопкой мыши в области окна. Такое меню будет состоять из пунктов, каждый из которых при нажатии вызовет свою функцию. К примеру, я разместил в этом меню следующие элементы:
- New shot
- Save
- Default
- Crop
- Arrow
- Rect
- Pen
- Blur
Этот набор инструментов даст воспользоваться практически всеми возможностями приложения.
А может ещё лучше?
Да, можно ещё больше упростить доступ к возможностям приложения. Добьёмся мы этого при помощи хоткеев. Хоткеи регистрируются при помощи модуля globalShortcut и метода register. Необходимо помнить о различиях клавиатур в различных ОС. Так, например, слушать Command на Linux и Windows бесполезно. Поэтому разработчики Electron предлагают слушать CommandOrControl.
Хоткей в стандартном случае состоит из модификатора (CommandOrControl, Alt, Option...) и кода клавиши (0 — 9, A — Z, Space, Tab...). Стоит отметить, что он не обязательно должен состоять из комбинации двух клавиш. Может встретиться как одна, так и три (возможно и больше, но я не проверял). Например, для вызова модального окна со списком хоткеев я использую F2, а для того, чтобы увеличить масштабирование (zoom in) — CommandOrControl+shift+Plus
. Мне пришлось добавить в эту цепочку shift, потому что иначе комбинация CommandOrControl+shift+Plus
преобразуется в сontrol+shift+=
.
Общение процессов
Скорее всего, вы столкнётесь с необходимостью передать сообщение из одного процесса в другой (вызови мне такую-то функцию, к примеру). Для этого существуют ipcRenderer и ipcMain. Поставим себе простую задачу, чтобы разобраться как это работает. Допустим, при нажатии на пункт в меню мы хотим вызвать некую функцию, которая выведет нам alert. В обработчике клика пропишем что-то вроде appWindow.webContents.send('show');
. Это означает следующее: при нажатии на пункт меню в renderer-процесс будет послано асинхронное сообщение по каналу 'show'. Также можно передать дополнительные аргументы, так что пусть наш alert выведет и переданный аргумент. Немного модифицируем написанное ранее appWindow.webContents.send('show', 'content');
.
Теперь в renderer-процессе напишем:
ipcRenderer.on('show', (event, message) => {
alert(message) // Alerts 'content'
});
Как видите, всё очень просто!
Tray
Полезно чтобы приложение не висело свёрнутым или, что хуже, было просто открыто. Для решения этой проблемы существует трей. И Electron даёт возможность поработать и с ним через класс Tray.
Что по сути такое приложение в трее? Это иконка и меню, вызываемое кликом правой клавиши мыши. Интересно подметить, что конструктор класса Tray принимает ровно один параметр — иконку. Если этот параметр отсутствует, то будет выброшена ошибка. Сначала это ничуть меня не удивило. Есть в конструкторе обязательный параметр, будь добр — передай. Чуть позже расскажу почему в дальнейшем обязательность этого параметра мне показалась странной.
Иконка приложения
Давайте ещё немного поговорим об украшательствах. Как же наше приложение будет жить без иконки?! Добавляется она там же, где мы создавали новое окно, дописыванием в конец icon: __dirname + '/icon.png'. Стоит отметить, что для Windows рекомендуется использовать иконку в формате *.ico. Не страшно, ведь можно воспользоваться os.platform()
.
Моя страничка может достучаться до системы?
Да, конечно. На момент написания этой статьи актуальная версия Electron — 1.4.6, которая работает с NodeJS 6.5. К примеру, у --shots есть возможность локального сохранения файла. Для этого я использую fs.writeFile
. И каждый раз перед таким сохранением проверяю есть ли директория для скриншотов (я назвал её «--shots»). Если она отсутствует, то просто её создаю.
Очень важно понимать, что ваше приложение на Electron — это не просто страничка в стандартном окошке, а вполне себе полноценное приложение.
Дополнительные возможности, которых нет в --shots
Естественно, работая над --shots, я использовал не все возможности Electron.
Например, можно загрузить своё приложение в AppStore и WindowsStore. Но --shots задумывался как приложение для LInux, поэтому особо я не влезал во все эти дебри публикации приложения. Но знаю, что для того чтобы выложить приложение в AppStore, необходимо его подписать. Каждый раз при сборке новой версии --shots я вижу этот warning в консоли и скрещиваю пальцы, чтобы он ни на что не повлиял в дальнейшем.
Ещё на сайте Electron можно встретить один замечательный пункт: «Automatic updates». Но и его я не смог прикрутить к --shots, т.к. открыл статью и увидел:
«There is no built-in support for auto-updater on Linux, so it is recommended to use the distribution’s package manager to update your app».
Проще говоря, на LInux это не работает, так что выкручивайтесь. Например, я делаю запрос на сервер, который возвращает мне номер текущей актуальной версии и, если он не совпадает с версией приложения, вывожу сообщение с просьбой его обновить.
Подводные камни
Вроде, звучит всё круто, должен же быть подвох. Безусловно, есть некоторые неприятные моменты:
Не всё кроссплатформенно
Это заметно даже на примере того же самого авто-апдейта. С прозрачным окном тоже не всё так просто. К примеру, на Linux нужно вбить в командную строку --enable-transparent-visuals --disable-gpu
, а иначе никакого прозрачного окна вы не увидите. Согласитесь, приложение, которое после установки просит пользователя вбить что-то в терминал, уже начинает вызывать подозрение.
Minimize
Очень много времени я потратил на разрешение проблемы с этим. Суть её такова: когда мы сворачиваем окно приложения — срабатывает событие minimize, и окно сворачивается. Вроде всё окей. Но приложение, висящее и в трее, и в доке — это уже странно. Поэтому хотелось бы отлавливать minimize и как-то убирать приложение из дока, оставляя лишь в трее. Для этого есть метод hide(). Пишем обработчик для minimize, вызываем hide — всё отлично. Затем я захотел добавить возможность из меню трея развернуть приложение обратно, и сразу же нашёл метод show(). Всё логично show/hide, но нет. Когда я пытаюсь развернуть приложение из трея, оно намертво зависает. Очень долго я думал, что упускаю что-то важное, но никаких ошибок в консоли не видел. В том числе, выводил само окно перед тем как вызывать show() — окно существует. В общем, отказался от подобного механизма работы приложения и решил попробовать вызвать hide(), когда окно приложения открыто. И да, чудо свершилось. Всё заработало ровно так, как надо. Несколько часов было потрачено на поиск ответа на вопрос: «Да почему ты не работаешь?», а в итоге решение мне подсказал Telegram. Я просто добавил в меню пункт «minimize to tray», а заодно и хоткей повесил на него.
В общем-то для меня подводные камни Electron’а закончились. Скорее всего, мне просто повезло…
Сборка
Настало время поговорить об ещё одной достаточно занимательной вещи — сборке. Electron даёт вам возможность собрать своё приложение и потом проинсталлировать его в различных ОС. Пользователь даже может не догадываться что вы написали своё приложение на web-технологиях. Давайте приступим!
Стандартный подход
На официальном сайте в разделе документации есть 3 ссылки на инструкции по сборке (Linux, MacOS, WIndows). Так как приложение изначально затачивалось под Linux, то сначала я открыл ссылку для него. Первое системное требование сразу же напугало меня: «At least 25GB disk space and 8GB RAM» (не менее 25GB свободного места и 8GB оперативки). «Ладно, что поделать?» — подумал я и начал пытаться собрать приложение из примера. В итоге, сам процесс сборки занял у меня минут 20-30, более того, пользоваться компьютером было невозможно! Я очень рад, что не продолжил работать с этим сборщиком, т.к. узнал об одной пренеприятнейшей вещи: чтобы собрать приложение под какую-то ось, его надо собирать именно из под этой оси!
«Должен быть другой способ,» — говорил я себе. И да, действительно он существует.
Electron builder
Достаточно было просто загуглить «electron builder» и перейти по первой ссылке. С тех самых пор я и использую это сборщик, и вот почему:
- сборка --shots для Linux стала занимать ~ минуту,
- из-под Linux можно собрать и .exe-шник (надо ещё Wine ставить, но ок),
- в процессе сборки можно смотреть мемы, ничего не тормозит.
Всё что вам остаётся — правильно дополнить свой проект.
Подготовка проекта
Этот процесс можно разбить на 2 этапа:
- подготовка иконок,
- подготовка package.json.
Иконки
Приложения, будучи установленными, хотят иметь свои иконки. Для этого в корневой директории проекта необходимо создать директорию «build», которую заполнить по следующей схеме:
— build
—— icons
——— 32x32.png
——— 32x32.png.ico
—— icon.icns
—— icon.ico
Package.json
Если вы просто попробуете собрать свой проект, то консоль начнёт плеваться в вас ошибками. Это происходит по той причине, что в файле должны содержаться стандартные строки:
- name,
- description,
- version,
- author.
Затем надо описать «build» и «scripts». Я намеренно быстро пробежался по этим двум пунктам, т.к. запоминать, что внутри, не требуется, да и вряд ли возможно, особенно если учесть, что есть огромное количество настроек (для «build»), которые могут зависеть от ОС, к которой он и собирается. Например, для Windows можно собрать portable-версию своего приложения и указать gif-файл, который будет отображаться в момент запуска.
В общем, рекомендую использовать именно Electron builder для сборки. Это сэкономит вам кучу нервов, поверьте.
Добавляем своё приложение на сайт Electron
Я считаю неплохой идеей рассказать о своём проекте на Electron на сайте самого Electron. Для этого достаточно послать им pull request, содержащий:
- отредактированный «_data/apps.yml» (добавив ваше приложение в конец);
- иконку приложения 256px на 256px в формате png;
- сообщение о том, что вы связаны с проектом и все члены вашей команды согласны с тем, что приложение появится на сайте Electron.
Опять же, в этом нет ничего сложного. После отправки pr необходимо лишь запастись терпением и ждать когда его примут (в моём случае это заняло около недели).
Подробную инструкцию по добавлению можно найти здесь.
Подводя итог
Нет, Electron не вытеснит нативщиков с рынка, так же как и React Native не убьёт Swift. Electron годится для создания простых приложений и является достаточно интересным проектом. И он чертовски прост! Мне удалось написать --shots, используя стандартный стек технологий frontend-разработчика:
- HTML
- CSS
- PostCSS
- JavaScript
- NodeJS
- PHP
Первый коммит был сделан 24 сентября 2016 года, а первый релиз вышел 17 октября того же года. Это значит, что спустя всего 24 дня я получил версию приложения, которую уже можно было использовать. Это достаточно быстро, если учесть, что я работаю и люблю отдыхать. Так что вперёд, всё в ваших руках!
Полезные ссылки
- Официальный сайт
- Приложения на Electron
- Документация
- Atom discuss
- Electron builder
- Awesome Electron
- --shots на Github
Автор: Binjo