Комментарии коллег к моей последней статье "Почему я 'мучаюсь' с JS" навели меня на мысль, что публикации, касающиеся Tequila Framework, нужно помещать в хаб "Ненормальное программирование".  Почему-то идеи: вот это всё относится к разряду "ненормального" в современной web-разработке. В этой публикации — пример построения "нормального" PWA-приложения с использованием "нормальных" Vue 3 и Quasar UI на базе "ненормальной" платформы Tequila Framework.  В качестве базового функционала, реализуемого приложением, выступает классический "Список задач" (ToDo List). В качестве дополнительного — интернационализация (переключение языка) и очистка кэша приложения (актуально для прогрессивных приложений). Репозиторий демо-пакета — @flancer64/habr_teqfw_vue.
Само приложение — todo.habr.demo.teqfw.com. Основной пакет, в котором реализовано приложение: @flancer64/habr_teqfw_vue. Основные зависимости пакета: Всего в приложении 32 зависимости (commander, vue, vue-router, i18n, ...). Для teq-плагинов (npm-пакетов, совместимых с платформой TeqFW) каждый es6-модуль плагина доступен для автозагрузки посредством совмещения логического пространства имён плагина с файловой структурой es6-модулей. Так для демо-плагина в дескрипторе ./teqfw.json декларируется пространство имён Fl64_Habr_Vue: После чего es6-модуль ./src/Front/Mod.mjs становится доступным для автозагрузки DI-контейнером по идентификатору Fl64_Habr_Vue_Front_Mod как для nodejs-приложения, так и в браузере. Чтобы в браузере иметь доступ к библиотекам, написанным на JS и не совместимым с платформой, нужно: Подключение i18next на стартовую страницу приложения: Пакет-обёртка в своих зависимостях содержит npm-пакет с соответствующей библиотекой и прокидывает (в своём ./teqfw.json) на её рабочий код (например, каталог ./dist/umd/) линк для обработчика статических файлов из web-плагина: Таким образом дистро-код пакета i18next становится доступным для загрузки в браузер. Из браузера дистро-код оригинального пакета извлекается объектом-обёрткой (в конструкторе или init-функции): после чего он становится доступным в DI-контейнере через обёртку: Разумеется, объект-обёртка должен создаваться уже после того, как необходимые библиотеки были загружены на страницу. Пакеты-обёртки позволяют использовать в teq-приложениях любые браузерные JS-библиотеки, доступные через npm. Web-приложение обычно начинается с HTML-файла (демо — ./web/index.html). В нём можно выделить три секции: Launchpad — это фрагмент страницы, в который будет помещено js-приложение после его инициализации и запуска: Resource Loading — это часть, в которой на страницу загружаются ресурсы, несовместимые с TeqFW: Bootstrap JS — это код, который отслеживает окончание загрузки страницы и стартует загрузку и запуск teq-приложения: В демо-приложении в задачи service worker'а (./sw.mjs) входит: Код проверки наличия service worker'а и, при необходимости, его установки на странице-оболочке: Подробнее о service worker'е, манифесте и оболочке — "Минимальное PWA" В задачи bootstrap-функции входит: Так как у некоторых коллег были вопросы по моему стилю оформления кода (раз, два, три), то в этом демо-проекте я придерживался более "нормального" стиля (благо, что в Apple наконец-то подсуетились и в апреле этого года добавили поддержку private-атрибутов в Safari): Модуль, содержащий приложение — Fl64_Habr_Vue_Front_App. Задачи приложения: В TeqFW es6-модуль, создающий объекты, можно оформить в виде фабричной функции или класса. Так как vue-компоненты по сути являются объектами-шаблонами, которые Vue использует в качестве базовых при превращении соответствующих тэгов в фрагменты страницы, то они используются в приложении в единственном числе (один шаблон на всё приложение). Для создания таких объектов-шаблонов достаточно фабричной функции. Вот типовая структура такой функции: Используя DI-контейнер и фабричную функцию для создания шаблона vue-компонента, можно инжектить результирующие singleton-шаблоны в другие фабричные функции для создания других vue-компонентов: Символ $ в конце идентификатора зависимости означает, что DI-контейнер загружает соответствующий es6-модуль, берёт из него default-экспорт и создаёт при помощи этого экспорта (функции или класса) объект, который затем сохраняет у себя внутри и использует каждый раз, когда встречает аналогичную зависимость (шаблон singleton). Таким образом, все фабричные функции для создания шаблонов vue-компонентов отрабатывают только по одному разу, а в дальнейшем контейнер переиспользует результат первого запуска (один и тот же объект). Если бы в конце стояло два символа — $$, то контейнер использовал бы фабричную функцию всякий раз, когда встречал соответствующий идентификатор зависимости, и в конструкторы бы инжектились разные объекты. Vue Router позволяет сделать ленивую загрузку vue-компонентов через динамический импорт: Что очень хорошо совмещается с работой DI-контейнера: В результате соответствующие es6-модули подгружаются в браузер по мере использования (перехода на соответствующий маршрут PWA/SPA). Для локализации текстовых вставок применяется npm-пакет i18next и teq-обёртка для него — @teqfw/i18n. JSON-ресурсы с переводами находятся в каталогах в файлах ru.json, en.json, ... На серверной стороне реализован реестр TeqFw_I18n_Back_Model_Registry, который при старте backend-приложения сканирует teq-плагины в каталоге ./node_modules/ и формирует массив с переводами, совместимый с i18next. Имя npm-пакета при этом используется в качестве namespace'а для i18n-ресурсов соответствующего плагина: Для доставки translation-ресурсов на фронт используется объект-обёртка TeqFw_I18n_Front_Lib и сервис TeqFw_I18n_Back_Service_Load. Функция Fl64_Habr_Vue_Front_App.init.initI18n инициализирует обёртку, загружая соответствующие ресурсы с сервера, и добавляет пропатченную функцию-транслятор во Vue: С пропатченной функцией-траслятором, во vue-компонентах, у которых присутствует атрибут teq.package можно не использовать i18next namespaces, как в html-шаблонах: так и в JS-коде: Если в функции-трансляторе i18next namespace используется в явном виде: то можно напрямую использовать ресурсы из любого teq-плагина приложения. Основной компонент — Fl64_Habr_Vue_Front_Route_Home. Виджеты:  Основной компонент — Fl64_Habr_Vue_Front_Route_Cfg:  Виджеты: С переключением языка интересно. Нужно не только изменить состояние i18next-объекта, который находится в globals, но и перерисовать весь UI с новыми переводами. Для этого во Vue предлагается использовать :key атрибут в самом верхнем элементе иерархии vue-компонентов (у меня — в Fl64_Habr_Vue_Front_Layout_Base): И инкрементировать его каждый раз, когда необходима перерисовка UI'я. Проблема в том, что перерисовку нужно начинать с корня иерархии компонентов, а сигнал на перерисовку — подаваться из глубины иерархии (из Fl64_Habr_Vue_Front_Widget_Cfg_Lang). Во Vue 3 для этого предлагается использовать пару provide / inject. В теории можно создать в Layout_Base реактивный объект и предоставить его дочерним компонентам через provide, а во вложенном Widget_Cfg_Lang получить реактивный объект и изменить его состояние, чтобы перерисовать UI, начиная с самого верха. Вот только состояние объекта изменяется (это видно под отладчиком), а перерисовки не происходит. Поэтому "грязный хак" — реактивный объект вешается на функцию-конструктор: а затем реактивный объект извлекается в Widget_Cfg_Lang: Вот так — работает, а через Vue 3 provide / inject — нет. Возможно, я что-то криво делаю с "родным DI" (хотя порядок создания и доступа к реактивному объекту правильный и сам объект langChangeCounter изменяется при смене языка). Поскольку это всё-таки PWA, то кэширование статики на уровне service worker'а добавляет некоторой специфики. Во-первых, приложение получает возможность работать offline, а во-вторых — приложению нужен какой-то способ получать с сервера изменения для файлов, сохранённых в кэше. В демо-приложении применяется радикальный способ — пользователь может полностью удалить кэш service-worker'а прямо из приложения:  В Chrome есть инструменты, облегчающие жизнь PWA-разработчику:  но в смартфонах очистку кэша лучше вынести на уровень пользователя. Демо-проект показывает, как можно интегрировать в "ненормальное" teq-приложение код "нормальных" браузерных npm-пакетов, написанных на JS (транспилированных в JS из других языков). Я использовал Vue, потому что мне он больше по душе, но уверен, что точно так же можно оборачивать React (насчёт Angular'а не уверен, т.к. он сам по себе — платформа): В общем, всё, что может загрузиться на страницу — можно обернуть и сделать доступным через DI-контейнер (в nodejs таких проблем нет, там и так всё доступно через связки export-import пакетного уровня). Мне понравился Vue 3 и Quasar UI, поэтому мой ToDo List выглядит так (за выбор цветовой гаммы прошу сильно не пинать — я начинал с монохромных дисплеев). Несмотря на то, что иногда мой код считают "диалектом", каждый отдельный mjs-файл — это валидный JS. Его можно импортировать как обычный es6-модуль без всяких дополнительных ухищрений и использовать в своём коде. Вот только зависимости в spec-объект конструкторов и фабричных функций придётся добавлять вручную. Самым ценным для себя я считаю знакомую структуру es6-модулей в браузере:  Сравните со структурой в IDE:  Разница в файлах объясняется тем, что в браузер подгрузились не все модули — "loading on demand" (хотя в кэше service-worker'а находятся все модули). Также обратите внимание на удобство использования namespace'ов при документировании приложения: Некоторые считают, длинные идентификаторы для объектов кода не слишком удобными, но чем больше в приложении объектов кода (классов, функций, констант), тем длиннее становятся идентификаторы. Просто мы их прячем в имена npm-пакетов и пути к соответствующему файлу внутри пакета. Так что длинные идентификаторы — это просто следствие больших проектов. И вообще, прямой import es6-модуля через DI-контейнер из любого пакета с использованием логического пространства имён позволяет строить "модульные монолиты" — когда монолитное приложение собирается из модулей (пакетов), но запускается как единое целое. Более того, ничего не мешает одним и тем же пакетам быть как частями большого "монолита", так и частями "микросервисов", обслуживающих этот "монолит". Код из shared-секций пакетов может, например, работать в back-секциях "микросервисов" и во front-секциях "монолита" (или "монолитов"). Да, я в курсе, что ровно то же самое можно сделать и без всякого DI, на основе "нормальных" export'ов и import'ов. Вот поэтому я и поместил публикацию в хаб "Ненормальное программирование". А что касается хаба "JavaScript" — ну так это самый, что ни на есть JS и есть. wiredgeese.com