Делаем кроссплатформенное нативное десктоп приложение на Angular
Как вы уже наверно знаете, Angular уже есть во многих платформах:
Ну и, конечно, здесь не хватало десктопа (не будем пока про Electron).
Для создания десктоп приложений существует много решений с использованием шаблонов, например, такие решения как JavaFx, Qt, WPF. Все они, кроме последней, являются кросплатформенными.
А что если бы мы хотели использовать знакомый нам фреймворк и сделали бы на нем нативное приложение? Собственно, этим я и занялся.
Для начала посмотрел что есть на текущий момент, и что уже, возможно, было сделано под Angular.
На самом деле, таким образом хочу показать, что на Angular можно делать все что угодно, и несколько лет подряд делаю всякие штуки под него.
Поиск
libui-node
Это легкая, портативная библиотека графического интерфейса, которая использует нативные возможности GUI, для каждой платформы, которую она поддерживает. Идет как альтернатива Еlectron.
Пример простого приложения:
const win = new libui.UiWindow('Test window', 800, 600, false);
Под капотом у него простые биндинги на libui. (libui: a portable GUI library for C). Собирается все это через node-gyp, утилиту, предназначенную для компилирования нативных расширений для Node.js. libui-node включает в себя более 30 готовых компонентов, ну, и если вам вдруг вздумается что то кастомное сотворить, то необходимо будет погрузиться в код на C. И к тому же, сами компоненты были написаны 2 года назад, и с тех пор не обновлялись. Возможно настолько все хорошо, что не нужно вносить изменения, и этих 30 компонентов вполне хватает для разработки, ну или проект никому и не нужен совсем.
Ну и, собственно, готовое приложение может выглядит вот так:
libui-node
Proton-native и Vuido
А вот тут немного интересней, proton-native и vuido это тот же самый libui-node, только под React и Vue. Под компоненты libui-node написаны соответствующие обертки. Несмотря на количество звезд на github (9к и 6к), проекты заброшены, и почти никто не использует. Из всего что смог найти, это были очень простые приложения. Еще одну проблему, которую я обнаружил, это проблемы с кастомизацией самого UI, это невозможно сделать в рамках libui, и автор проекта подумывает переписать все на Qt.
Сам libui довольно популярен для написания всяких биндингов, энтузиасты затащили его даже в php
Готовое приложение может выглядеть следующим образом:
Proton-native
Довольно скучный интерфейс без кастомизации, поэтому этот вариант отпал сразу.
А может взять Qt?
Qt, js, css
Конечно же вы слышали про Qt, и то, что его можно обнаружить везде, но не многие слышали что он из коробки сейчас интегрирован с Javascript. QML позволяет декларативное построение пользовательских интерфейсов, используя биндинги по свойствам и, тем самым, позволяя расширять возможность существующих QML элементов. Конечно же это более строгий Javascript чем в вебе. Можно писать нечто на подобии ES5 c использованием QML объектов, но у вас не будет DOM API.
Просто на заметку, как бы вы писали на Qt под C++:
#include
#include
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 {
function factorial(a) {
a = parseInt(a);
if (a <= 0)
return 1;
else
return a * factorial(a - 1);
}
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
Как и многие библиотеки для Gui, Qt использует свой event/message loop для обработки событий из виджетов. Следовательно, когда мы вызываем условно app.exec () Qt запускает цикл сообщений и блокирует его там же. Все это хорошо, когда есть только один цикл сообщений во всем приложении. Но поскольку нам необходимо использовать Qt с NodeJs и, последний, к тому же имеет свой event loop, невозможно их так легко интегрировать. Но такие решения уже были сделаны, например та же связка с Electron или yode. У этих решений существует своя особенность, они поднимают как минимум 2 процесса — для главного потока, и для рендерера. Несмотря на это, у этого подхода есть существенный выигрыш, не нужно модифицировать NodeJs или Chromium.
В случае с NodeGui ситуация немного иная, есть один процесс для всего, и тем самым, нет необходимости шарить события между процессами. Для этого был форкнут Nodejs— и были сделаны небольшие доработки для необходимых биндингов в Qt. И теперь необходимо запускать процесс не так как обычноnode main.js, а qode main.js. К счастью, qode опубликован как npm модуль в пакет @nodegui/qode. Для того чтобы запустить простой hello world вам необходимо установить еще немного пакетов, подробнее для каждой ОС вы можете посмотреть на официальном сайте: https://docs.nodegui.org/docs/guides/getting-started
По умолчанию в nodegui все является виджетами, и их можно прикручивать к различным шаблонам. В nodegui на данный момент имеется 2 типа шаблона: FlexLayout и QGridLayout.
Стили в 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.
Как уже писал в начале, Angular умеет в большинство из платформ, но до сих пор не было платформы под десктоп. Из-за хорошо продуманного и структурированного API Angular, реализация nodegui под Angular сводится к написанию кастомного platformBrowserDynamic с Renderer и их подменой в приложении.
Но как это все работает изнутри?
У нас есть условный main.ts, c него и начнем.
Процесс начальной загрузки состоит из двух частей: создания платформы и прокидывания в него стартового модуля.
platformBrowserDynamic().bootstrapModule(AppModule);
Через createPlatformFactory мы можем создать абсолютно любую нужную платформу. Для нас это означает, что мы не хотим работать с обычным DOM, и дополнительно прокинем описание схемы взаимодействия элементов при работе с рендером. Подробнее про создание платформы можно ознакомиться в исходниках.
В стартовом модуле мы описываем какой компонент рендерить первым. При создании экземпляра компонента Angular вызывает renderComponent
и, связывая его с нужным рендером, которое он получает, с этим экземпляром компонента. Все, что Angular будет делать в отношении рендеринга компонента (создание элементов, настройка атрибутов, подписка на события и т. д.), будет проходить через этот рендерер. Поэтому нам необходимо подменить RendererFactory.
В первую очередь в Renderer нас будет интересовать метод createElement. В этом методе мы получаем название тега, и по нему нам необходимо создать нужный компонент. Благо, в nodegui есть базовый набор компонентов, который я бережно перенес и описал как они будут рендерится в рамках Angular, закинув все в общий справочник компонентов. Остальные действия со стандартыми компонентами так же будут проходить через этот рендерер. Подробнее.
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)”
.
На данный момент доступно 16 стандартных компонентов. Но если необходимо написать свой кастомный компонент, то всегда имеется возможность сделать это через QWidget.
Также был сделан свой роутер, чтобы наше приложение было максимально совместимо с Angular.
const appRoutes: Routes = [
{ path: 'home', component: HomeComponent },
{ path: 'about', component: AboutComponent }
];
// AppModule imports
...
NodeguiRouterModule.forRoot(appRoutes),
Angular nodegui app
weather app
Собираем в прод
Для того чтобы собирать готовое приложение в nodegui существует свой упаковщик — @nodegui/packer.
Утилита очень простая, состоит пока из 2х команд.
npx nodegui-packer — init MyApp
Эта команда создаст папку для упаковывания, содержащий шаблон. Вы можете изменить содержимое, чтобы добавить иконку, изменить название, описание и другую информацию приложения, а также, чтобы добавить нужные зависимости.
npx nodegui-packer — pack
Эта команда запускает нужный инструмент для упаковки (например, macdeployqt для mac) и упаковывает зависимости.
В заключении
В заключении, хочется сравнить результаты с другими веб решениями на десктопе (результаты запуска под Mac OS).
download size
memory use
Ссылка на проект:
irustm/angular-nodegui
*Build performant, native and cross-platform desktop applications with Angular. Angular NodeGUI is powered by Angular
Информация про проект:
https://t.me/ngFanatic
Информация про мои open source проекты
https://twitter.com/irustm