/jet

Библиотека проектирования веб-интерфейсов

Primary LanguageTypeScript

Jet

Реактивная javascript библиотека проектирования пользовательских веб-интерфейсов

Не все алгоритмы, заложеные в механиках были полностью протестированы. Ядро в стадии обкатки, держите это в уме и используйте Jet только по назначению! В качестве сборочного модуля используется ESBuild. Стабильная работа шаблонов гарантируется только с ним.

jet core creation

Пролог и идея

История библиотеки берёт своё начало с необходимости описания толстого клиента виджета конструктора формы опросов со сложными конфигурациями и механиками. Но сновная сложность заключалась в том, что проект виджета в одном из дочерних проектов крутится в окружении старой сборки chromium в 1C, из-за чего использование современных фреймворков оказалось слишком большим риском. Тогда было задумано написать свой, лёгкий, простой, быстрый и стабильный движок проектирования интерфейсов, способный запустить абстрактный реактивный компонент в относительно древних браузерных клиентах. API библиотеки Jet, как и сама библиотека, не велики. Jet нельзя назвать декларативной в полном смысле этого слова. Остальных, вообще-то, тоже, но Jet ещё более императивен. Во-первых, его жизненный цикл управляется наполовину публичными методами, как, например, и в "декларативном" react, setState вызывает автоматическое обновление, но с уже вложенными под капот оптимизациями. Этот классический state driven подразумевает и создание, и удаление, и патчинг. Но на удаление, как и создание виртуальных узлов, можно повлиять императивно, без посредничества объектов состояний, как это реализовано в других реактивных фреймворках.

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

Управление данными обеспечено через props и eventBus.

  • Props является чистым объектом данных, забранным по Rest API. С его помощью данные спускаются вниз по виртуальному дереву. В Root компоненте, при старте приложения, когда данных с апи ещё нет пропы должны быть пустым объектом.
  • EventBus - хранилище функций передачи данных. С его помощью данные можно передавать в любое место приложения, собирая новые состояния из чего угодно для чего угодно, как вверх, так и вниз по узлам. Шина надёжно интегрирована в абстракцию, все существующие на странице узлы будут получать актуальные данные по подписке.

Здесь jet ни подо что не косит. Я нигде не встречал фреймворка, в котором event emitter действовал бы в глобальном скоупе всего дерева. Он есть у vue, но действует только в скоупе непосредственного родителя - совершенно по другому. А при этом глобальный event emitter часто выручал меня с оптимизацией в реакте, где flux не вывозил перформансом. Его можно встретить в проектах клиентов с кастомным frontend, часто работающем гибридно (частисная динамика). проброс синглтона EventBus - важная фича библиотеки.

Назначение

Jet рекомендуется использовать для небольших внутренних виджетов и клиентов, независимых RestApi компонентов итп. Проектировать на нём сложную распределённую систему не рекомендуется, по крайней мере до тех пор, пока не будет глубоко интегрированное History Api. Также рекомендуется для разработки клиентов Geogocoding Information Systems (GIS), поскольку в них часто генерируемые сложносоставные события и их аргументы являются первичным и единственным актуальным источником данных во многих интерактивных сценариях.

Шаблонизация

Библиотека использует только одну зависимость на клиенте - старый добрый Mustache, работает он с шаблонами через dependency injection как строки. Очерь рекомендуется ознакомиться с документацией mustache для понимания процесса рендера списков элементов и понимания сути механизма рендера. Шаблонизация поддерживается в ESBuild через специальный флаг в параметрах - goto gulpfile.js:18

Комбинация state and event driven подходов

В библиотеке используется комбинированный подход state + event driven - она является антиподом flux-подхода. Её идеология в том, что любая интерактивная часть сценариев, влекущих за собой какие-то эффекты в пользовательском интерфейсе - это события, не только изначально, но и на всём стэке виртуального дерева компонентов. Именно с перехвата события начинается история взаимодействия со сценарием в любой state driven библиотеке, и на нём же событие, как правило и затухает. Далее данные как правило следуют в store или меняют текущее состояние. Но в любой современной библиотеке, поддерживающей в своей основе чистый state driven development встаёт ребром вопрос передачи данных и реакции на них вверх по дереву - в родительские узлы. В react для этого добавили redux, позже появился context api, которым, если умело пользоваться и много писать, можно обойтись и собрать быстрый рабочий интерфейс. Во Vue добавили Vuex, который как любой другой flux много весит и много ест. Во Vue есть emitter, но он работает локально, в скоупе родителя компонента и по сути является частью механизма перехвата native событий. В react вообще нет emit функции, а перехват события в непосредственных родителей происходит через prop function. И того не требуется потому что чаще всего используется глобальный биндинг в redux или provider данных компонента. Что vue, что react работают с подходом глобального биндинга и локальных событий.

Jet представляет льтернативный подход - локальный биндинг и глобальные события.

Полуавтоматический контроль состояния для оптимизации обновлений

Способ передачи данных в Jet - самая главная фича и его основное отличие от других библиотек. В него встроен EventBus, который хранит в себе карты функций.

Любой компонент может подписаться на любой компонент и получать любые данные от любого компонента к любому компоненту.

А обновление состояния компонента обновит состояние только в дереве этого компонента. Его основная работа - сравнить новый узел (созданный через DOMParser) с существующим, обновить существующий узел и все указанные в шаблоне локальные события.

Если при обновлении процесс сопоставит два одинаковых узла с виртуальным существующим компонентом, он не будет просить его обновиться.

React как минимум проведёт shallowUpdate и решит. Jet ничего решать не будет - ему это не надо. Механизм обновления будет работать с виртуальным деревом только на подмену компонента, его создание и удаление (в случае условного рендера). Если логика работы компонента подразумевает обновление дочерних виртуальных компонентов, то необходимо вручную собрать связку, где parent и child будут отправителем и подписантом. Таким образом после setState будет вызываться emit, этот state получит дочерний компонент по подписке и далее решит что с ним делать как и в случае shallowUpdate реакта.

Очень мало кода

Самый низкий уровень библиотеки уместился в одном абстрактном классе, не превышающем размеры приличного.
Тут настолько мало кода что описание документации скатится к описанию приёмов использования механизмов, которое я и так произвожу вследствие тестирования и шлифовки библиотеки. Используемым технологиям уже много лет, они давно работают стабильно и используются по всему миру до сих пор: gulp, esbuild, typecscript, mustache. Любой middle+ frontend dev может открыть начинку и понять что происходит - несмотря на название, как бы намекающее на rocket science, на самом деле в нём нет ничего сверхъестественного. Более того, он прост как РД-8. В этом его основное преимущество, дающее и рост динамики, и охват поддержки браузерами.

TODO по механике

  • Исправить имплозию компонентов.
    Теоретически это упущение грозит утечками. При имплозии внутренних частей компонентов нужно не только рекурсивно удалять текущее дерево детей. Нужно также удалить текущий инстанс из виртуального родителя, если таковой имеется. Короче нужен onDestroy в конструкторе, прокидываемый при mount / update create, отрабатываемый на implose.

  • Сделать что-нибудь с парсингом событий. Минор. Чтение атрибутов локальных событий компонентом вызывает лёгкое отвращение, хоть и покрывает большинство кейсов.