Хабрахабр

Делаем кроссплатформенное нативное десктоп приложение на Angular

angular-nodegui

Как вы уже наверно знаете, Angular уже есть во многих платформах:

Ну и, конечно, здесь не хватало десктопа (не будем пока про Electron).

Все они, кроме последней, являются кросплатформенными. Для создания десктоп приложений существует много решений с использованием шаблонов, например, такие решения как JavaFx, Qt, WPF.

Собственно, этим я и занялся. А что если бы мы хотели использовать знакомый нам фреймворк и сделали бы на нем нативное приложение?

Для начала посмотрел что есть на текущий момент, и что уже, возможно, было сделано под Angular.

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

Поиск

libui-node

Идет как альтернатива Еlectron. Это легкая, портативная библиотека графического интерфейса, которая использует нативные возможности GUI, для каждой платформы, которую она поддерживает.

Пример простого приложения:

const win = new libui.UiWindow('Test window', 800, 600, false);

(libui: a portable GUI library for C). Под капотом у него простые биндинги на libui. libui-node включает в себя более 30 готовых компонентов, ну, и если вам вдруг вздумается что то кастомное сотворить, то необходимо будет погрузиться в код на C. Собирается все это через node-gyp, утилиту, предназначенную для компилирования нативных расширений для Node.js. Возможно настолько все хорошо, что не нужно вносить изменения, и этих 30 компонентов вполне хватает для разработки, ну или проект никому и не нужен совсем. И к тому же, сами компоненты были написаны 2 года назад, и с тех пор не обновлялись.

Ну и, собственно, готовое приложение может выглядит вот так:

libui-nodelibui-node

Proton-native и Vuido

Под компоненты libui-node написаны соответствующие обертки. А вот тут немного интересней, proton-native и vuido это тот же самый libui-node, только под React и Vue. Из всего что смог найти, это были очень простые приложения. Несмотря на количество звезд на github(9к и 6к), проекты заброшены, и почти никто не использует. Еще одну проблему, которую я обнаружил, это проблемы с кастомизацией самого UI, это невозможно сделать в рамках libui, и автор проекта подумывает переписать все на Qt.

Сам libui довольно популярен для написания всяких биндингов, энтузиасты затащили его даже в php

Готовое приложение может выглядеть следующим образом:

Proton-nativeProton-native

Довольно скучный интерфейс без кастомизации, поэтому этот вариант отпал сразу.

А может взять Qt?

Qt, js, css

QML позволяет декларативное построение пользовательских интерфейсов, используя биндинги по свойствам и, тем самым, позволяя расширять возможность существующих QML элементов. Конечно же вы слышали про Qt, и то, что его можно обнаружить везде, но не многие слышали что он из коробки сейчас интегрирован с Javascript. Можно писать нечто на подобии ES5 c использованием QML объектов, но у вас не будет DOM API. Конечно же это более строгий Javascript чем в вебе.

Просто на заметку, как бы вы писали на Qt под C++:

#include <QApplication>
#include <QPushButton> int main(int argc, char *argv[])
{ QApplication app(argc, argv); *// Important
* QPushButton hello("Hello world!"); hello.resize(100, 30); hello.show(); return app.exec(); *// Important
*}

Как может выглядеть ваш код в Qml:

Item MouseArea { anchors.fill: parent onClicked: console.log(factorial(10)) }
}

Эти компоненты возможно создавать динамически.

А ещё QML имеет большую систему типов, что несомненно будет полезно при определении всего этого в Typescript.

Так же можно легко кастомизировать компоненты:

Reactangle { id: redRectId width: 50 color: red
}

Почти CSS, не правда ли?

Ко всему этому осталось добавить только что Qt умеет в большинство платформ.

А нам бы еще Node.js

При поиске “nodejs + qt” нам сразу же выдаст node-qt, но сразу же бросается в глаза что продукт уже давно мертвый, и последний раз подавал признаки жизни 8 лет назад.

Но тем не менее, в поиске можно обнаружить совсем свежий проект — NodeGui.

NodeGui

Следовательно, когда мы вызываем условно app.exec() Qt запускает цикл сообщений и блокирует его там же. Как и многие библиотеки для Gui, Qt использует свой event/message loop для обработки событий из виджетов. Но поскольку нам необходимо использовать Qt с NodeJs и, последний, к тому же имеет свой event loop, невозможно их так легко интегрировать. Все это хорошо, когда есть только один цикл сообщений во всем приложении. У этих решений существует своя особенность, они поднимают как минимум 2 процесса — для главного потока, и для рендерера. Но такие решения уже были сделаны, например та же связка с Electron или yode. Несмотря на это, у этого подхода есть существенный выигрыш, не нужно модифицировать NodeJs или Chromium.

Для этого был форкнут Nodejs— и были сделаны небольшие доработки для необходимых биндингов в Qt. В случае с NodeGui ситуация немного иная, есть один процесс для всего, и тем самым, нет необходимости шарить события между процессами. К счастью, qode опубликован как npm модуль в пакет @nodegui/qode. И теперь необходимо запускать процесс не так как обычноnode main.js, а qode main.js. Для того чтобы запустить простой hello world вам необходимо установить еще немного пакетов, подробнее для каждой ОС вы можете посмотреть на официальном сайте: https://docs.nodegui.org/docs/guides/getting-started

В nodegui на данный момент имеется 2 типа шаблона: FlexLayout и QGridLayout. По умолчанию в nodegui все является виджетами, и их можно прикручивать к различным шаблонам.

Стили в Nodegui

На данный момент можно задавать стили для виджетов как inline, так и через styleSheet.

widget.setInlineStyle(`color: green`) view.setStyleSheet(` `#helloLabel { color: red; padding: 10px;
} #worldLabel { color: green; padding: 10px;
} #rootView { background-color: black;
}
`);

Qt по умолчанию поддерживает все селекторы в рамках CSS2 (https://doc.qt.io/qt-5/stylesheet-syntax.html#selector-types)

К счастью, такие свойства уже описаны в доках Qt и разжеваны на stackoverflow. Так же не обходится без кастомных свойств для стилизации компонентов.

*QPushButton* { qproperty-iconsize: 20px 20px;
}

Angular

Автор проекта уже реализовал поддержку react, но конечно же, все забыли про существование Angular.

Из-за хорошо продуманного и структурированного API Angular, реализация nodegui под Angular сводится к написанию кастомного platformBrowserDynamic с Renderer и их подменой в приложении. Как уже писал в начале, Angular умеет в большинство из платформ, но до сих пор не было платформы под десктоп.

Но как это все работает изнутри?

У нас есть условный main.ts, c него и начнем.

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

platformBrowserDynamic().bootstrapModule(AppModule);

Для нас это означает, что мы не хотим работать с обычным DOM, и дополнительно прокинем описание схемы взаимодействия элементов при работе с рендером. Через createPlatformFactory мы можем создать абсолютно любую нужную платформу. Подробнее про создание платформы можно ознакомиться в исходниках.

При создании экземпляра компонента Angular вызывает renderComponent и, связывая его с нужным рендером, которое он получает, с этим экземпляром компонента. В стартовом модуле мы описываем какой компонент рендерить первым. д.), будет проходить через этот рендерер. Все, что Angular будет делать в отношении рендеринга компонента (создание элементов, настройка атрибутов, подписка на события и т. Поэтому нам необходимо подменить RendererFactory.

В этом методе мы получаем название тега, и по нему нам необходимо создать нужный компонент. В первую очередь в Renderer нас будет интересовать метод createElement. Остальные действия со стандартыми компонентами так же будут проходить через этот рендерер. Благо, в nodegui есть базовый набор компонентов, который я бережно перенес и описал как они будут рендерится в рамках Angular, закинув все в общий справочник компонентов. Подробнее.

[https://blog.nrwl.io/](https://blog.nrwl.io/)https://blog.nrwl.io/

Чтобы слушать события в рендерер прокидывается название события, и для этих компонентов вешаем обычные eventListener.

listen(target: any, eventName: string, callback: (event: any) => boolean | void): () => void { const callbackFunc = (e: NativeEvent) => callback.call(target, e); target.addEventListener(eventName, callbackFunc); return () => target.removeEventListener(eventName, callbackFunc); }

События компонентов в точности такие же как и Qt, например, вместо привычного (click)=”clickFunc($event)” необходимо писать (clicked) = ”clickFunc($event)”.

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

Также был сделан свой роутер, чтобы наше приложение было максимально совместимо с Angular.

const appRoutes: Routes = [ { path: 'home', component: HomeComponent }, { path: 'about', component: AboutComponent }
]; // AppModule imports
...
NodeguiRouterModule.forRoot(appRoutes),

Angular nodegui app
Angular nodegui app

weather app
weather app

Собираем в прод

Для того чтобы собирать готовое приложение в nodegui существует свой упаковщик — @nodegui/packer.

Утилита очень простая, состоит пока из 2х команд.

npx nodegui-packer — init MyApp

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

npx nodegui-packer — pack

Эта команда запускает нужный инструмент для упаковки(например,macdeployqt для mac) и упаковывает зависимости.

В заключении

В заключении, хочется сравнить результаты с другими веб решениями на десктопе (результаты запуска под Mac OS).

download sizedownload size

memory usememory use

Angular NodeGUI is powered by Angular Ссылка на проект:
irustm/angular-nodegui
*Build performant, native and cross-platform desktop applications with Angular.

Информация про проект:
https://t.me/ngFanatic

Информация про мои open source проекты
https://twitter.com/irustm

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»