Pull to refresh

Миниатюрное десктопное GUI приложение на PHP — 2 МБ хватит для всех

Reading time6 min
Views16K

Я часто заморачиваюсь на тему минимизации размера своих GUI приложений. Прошлая моя статья была про Nuklear. Но сейчас захотелось более современных технологий. Чтоб HTML5, CSS3 и PHP. Чтоб приложение ни от чего не зависело, т.е. построено по принципу "всё включено". И чтоб конечный размер приложения не превысил 2 мегабайта. Получится ли?

В Linux я часто пользуюсь утилитой df. Мне её очень не хватает в Windows, а искать аналоги лень. А уж тем более было лень запускать всякие там Explorer, Powershell и т.п. - это слишком нормально :-). Так что было сделано волевое решение сделать свою утилиту, на РНР 5, с бутстрапом и JQuery.

Краткое решение моей задачи: CivetWeb + WebView + PH7.

Т.е. результирующее приложение это и веб-сервер, который раздаёт статические файлы и выполняет скрипты (CivetWeb). И непосредственно браузер, который к этому веб-серверу подключается (WebView). PHP выполняется как СGI-BIN, через сторонний интерпретатор PH7.

Здесь заключена основная фишка связки - в качестве интерпретатора СGI-BIN может быть использован любой язык/компилятор/интерпретатор. Хоть Haxe используй, хоть Go, хоть на Powershell веб-страницы генерируй, хоть полноценный PHP возьми. Так же прямо в CivetWeb встроен интерпретатор Lua, который позволяет делать легковесные, но полноценные приложения.

Windows 11 версия приложения
Windows 11 версия приложения

Хватит лирики, перейдём к коду. PHP не самый лучший язык для создания системных приложений. Например, в нём нет функции для получения системных дисков. Но когда это нас останавливало? В Windows воспользуемся перебором всего алфавита и проверкой существования директории. В Linux же функция построчно читает файл /etc/fstab , разбивает каждую строку по пробелам чтоб получить столбцы, и проверяет на существование директории:

function fs_get_roots() {
    static $roots = null;
    if ($roots === null) {
        if (strncasecmp(PHP_OS, 'WIN', 3) === 0) {
            $driveLetters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
            for ($i = 0; $i < strlen($driveLetters); $i++) {
                $curDrive = $driveLetters[$i].':\\';
                if (is_dir($curDrive)) {
                    $roots[] = $curDrive;
                }
            }
        } else {
            foreach (file('/etc/fstab') as $line) {
                if ($line[0] != '#') {
                    $line = str_replace('\t', ' ', $line);
                    do $line = str_replace('  ', ' ', $line, $count); while($count);
                    $rows = explode(' ', $line);
                    if (count($rows) && is_dir($rows[1])) {
                        $roots[] = $rows[1];
                    }
                }
            }
        }      
    }
    return $roots;
}

Корни файловой системы кэшируются в статической переменной $roots. Т.е. если несколько раз вызвать эту функцию при генерации страницы, то последующий вызов отработает очень быстро, повторно диск сканироваться не будет.

Linux-версия
Linux-версия

С одной стороны, код получился не очень красивым. Прямой парсинг системных файлов используется в Linux для написания низкоуровневых утилит. Например, какая-нибудь из реализаций df вполне может читать /etc/fstab, такой код нормален для программ написанных на Си. Но не для высокоуровневых языков программирования. Угадывание имени диска по алфавиту - вообще злостный хак. Например, если в DVD-приводе будет вставлен диск, то при каждом обращении он будет раскручиваться... А ещё диски могут быть двухбуквенными, смонтированными в директорию, быть доступными только по GUID и т.д. и т.п. - все они будут пропущены.

С другой стороны, результат достигнут. PHP предназначен для генерирования веб-страниц. Но при желании даже на нём можно писать системное программное обеспечение.

JQuery

В качестве отображения использую пример прогрессбара на бутстрапе. Здесь вам и сам Bootstrap, и HTML5, и ещё и JQuery зачем-то подключен.

Одним из желаний было чтобы в приложение была встроена система локализации. Чтоб русскоязычные пользователи видели приложение на русском, чехи - на чешском и т.д. Система локализации будет сделана на JQuery. Ну надо же его хоть для чего-нибудь использовать:

$(document).ready(function() {
    var userLang = (navigator.language || navigator.userLanguage)?.substring(0, 2)?.toLowerCase(); 
    $('[data-' + userLang + ']').each(function(element) {
        var localized = $(this).data(userLang);
        $(this).text(localized);
    });
});

В переменной userLang хранится 2 буквы языка браузера пользователя. Например, ru. Далее перебираем все теги у которых есть атрибут data-ru . Далее просто устанавливается текст текущего элемента, полученный из данных.

Пример HTML разметки для использования такой системы локализации:

<h2 data-ru="Системные диски" data-cz="Systémové disky">System Drives</h2>

Здесь сразу же виден основной недостаток такой системы локализации - код становится очень перегруженным, все пользователи всегда загружают данные для всех языков. Такая локализация подходит если в проекте нужно перевести порядка десятка коротких фраз и на малое количество языков. Т.е. годится только для примера.

PH7

PH7 - альтернативная реализация PHP для встраиваемых систем. PH7 разрабатывалась чтоб оживить интерфейс в роутерах, где до этого часто HTML-коды встраивались прямо в прошивку, написанную на Си.

PH7 бесплатен для проектов с открытым исходным кодом. Основным преимуществом является крайне малый размер - у меня получилось скомпилировать ph7-cgi в приложение размером порядка 300 килобайт.

Недостатков у PH7 чуть больше чем достоинств. Основные:

  1. Устаревшая версия PHP. Версия 5.3 вышла в 2009 году...

  2. Нет поддержки регулярных выражений. Это автоматически отметает возможность использования многих библиотек (Smarty, Twig и т.д.)

  3. Не все функции работают кроссплатформенно.

  4. Не все функции работают.

Например, функции disk_total_space и disk_free_space всегда возвращают пустое значение в Linux, что видно на скриншоте. Я не стал делать альтернативную реализацию этих функций (например через разбор выхлопа df) т.к. это не является целью данной публикации. Да и в принципе проблематично вызывать сторонние утилиты, т.к. на месте функции exec стоит заглушка всегда возвращающая пустое значение.

В общем, PHP в данной публикации скорее элемент юмора, чем реальной пользы.

CivetWebView

Краткое решение уже было написано выше. Рассмотрим клиентско-серверную часть: CivetWeb + WebView.

CivetWeb (бывший Mongoose до смены лицензии) является стандартом де-факто когда нужно что-то быстро раздать по HTTP на десктопе. Это не промышленное решение типа Nginx - моему локальному серверу не нужно раздавать миллионы файлов в секунду. И сотни злобных хакеров не будут пытаться его взломать. И очень большой вопрос, будет ли он вообще когда-либо пущен во внешнюю сеть.

WebView же - просто возможность встроить системный браузер в своё приложение. Для Windows это будет Edge, для Linux - GTK WebKit, для мака - Cocoa или WebKit. Т.е. какой-нибудь однозначно современный браузер, который хорошо поддерживает и HTML5, и CSS3, и JavaScript. При этом результирующее приложение не разрастётся на сотни мегабайт, т.к. непосредственно движок в полученное приложение встроен не будет.

Обе эти технологии для меня выглядят хорошо. Единственное, я так и не нашёл ни одного проекта, объединяющих их. Поэтому пришлось делать свой: CivetWebView - https://github.com/DeXP/CivetWebView

По сути CivetWebView - это объединение кода двух примеров: CivetWeb и WebView. На текущий момент проект скорее в стадии прототипа - реализован лишь необходимый мне функционал. Многие вещи заданы просто как константы в коде.

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

Размер

Полное Win32 приложение занимает 1.53МБ. Самым объёмным компонентом является CivetWebView, который занимает почти мегабайт. Следующим идёт интерпретатор РНР размером 282 килобайта. Завершают тройку лидеров Bootstrap и JQuery, которые в сумме занимают 274 килобайта (без отладочных map-файлов). Сам код приложения (PHP + HTML + CSS) занимает порядка 5 килобайт.

Исходные коды и архивы с бинарными файлами можно скачать на гитхабе: https://github.com/DeXP/CivetWebView-PH7-Example

Лицензии

Всё кроме PH7 лицензировано под "да делайте с этим что хотите, нам абсолютно всё равно". Т.е. если исключить из связки PH7, то можно делать коммерческие приложения. Кто-нибудь мне объяснит, зачем?)

Зачем всё это надо

Изначально у меня не стояла задача создания сферического GUI проекта в вакууме. Был проект онлайн интерпретатора визуальных новелл с консоли DS - VNDS-Online. И мне сразу хотелось иметь возможность запускать эти игры не только на каком-нибудь сайте, но и локально на своём компьютере. Или на каком-нибудь другом устройстве.

vnds-online не может работать без серверной части. Как минимум, нужно иметь возможность получать список имеющихся игр. Этот функционал было не слишком сложно переписать на LUA.

Кроме того, хотелось дать возможность запускать игры простым двойным кликом по ЕХЕ-файлу. Чтоб даже самый далёкий от веб-разработки человек мог играть.

Проект в итоге получился компактным за счёт того, что большая часть кода подразумевает исполнение в браузере. Который уже установлен в систему, хорошо отлажен и оптимизирован. В общем, спасибо большое ВаЮрику за замечательный движок.

Однако vnds-online тоже есть куда развиваться. Например, в движок явно нужно встроить возможность локализации. Ещё хотелось бы возможность адаптировать картинку под современные широкие экраны. Ну и отладка существующих багов разумеется.

Вопросы вместо выводов

Вместо вывода хочу задать сообществу два вопроса:

  1. Кому-нибудь нужен CivetWebView? Стоит ли его развивать? Вы бы им пользовались? Для чего? Есть ли какие-нибудь настоящие сценарии использования, где бы оно пригодилось? Или мир уже захватили Electron на пару с Node.js?

  2. Кому-нибудь нужен ещё один интерпретатор VNDS? Возможно под какие-нибудь странные платформы, но с современным браузером. Стоит ли вообще развивать vnds-online?

Полезные ссылки

Tags:
Hubs:
Total votes 33: ↑32 and ↓1+31
Comments58

Articles