@teqfw/core

Kate

Administrator
Команда форума
Эта статья не о том, как нужно писать приложения на JavaScript'е. Эта статья о том, как можно писать приложения на JavaScript'е. В прошлой публикации я описал свой "велосипед" — DI-контейнер @teqfw/di. В этой я покажу, каким образом его можно применять для создания консольных приложений.
Сразу отмечу, что речь идет о "чистом" JavaScript (ECMAScript 2015+ aka ES6+). Я признателен авторам TypeScript за то влияние, которое он оказал на развитие JS, но считаю, что в 2021-м году отличия TS от JS не столь драматические, как это было в году 2012-м, и не вижу для себя смысла использовать TS там, где достаточно JS. Если вы считаете по-другому и имеете острое желание высказать своё мнение, то можете сразу переходить к комментам, пропустив саму публикацию.
Те же, кому интересно, как же всё-таки в JS-приложении может использоваться "логическая адресация" элементов кода (пространства имён) вместо "физической" (файловая система) — добро пожаловать под кат.
jxgdepefolxliqqfujsfjsxdw5m.jpeg


Определения​

Для начала зафиксирую некоторые термины:
  • приложение: комплекс программ, исполняемых на различном физическом оборудовании (сервера, смартфоны, планшеты, персональные компьютеры), использующий общую кодовую базу, доступную через web.
  • пакет: (npm-пакет) компонент приложения, управляемый Node Package Manager, из которых и состоит общая кодовая база.
  • модуль: отдельный файл с исходным JS-кодом, соответствующий требованиям, предъявляемым к es-модулям.
  • элемент кода: объект или примитив, который может экспортировать модуль.
  • пространство имён: строка, соответствующая отдельному модулю, уникально идентифицирующая данный модуль среди всех остальных модулей приложения. Идентификация элементов кода модуля производится относительно идентификатора самого модуля.
  • плагин: пакет, содержащий файл ./teqfw.json с конфигурационной информацией, позволяющей DI-контейнеру @teqfw/di в пределах данного пакета сопоставлять пространства имён путям к модулям, этим пространствам соответствующим.
  • teq-приложение: приложение, созданное на базе разрабатываемой мной платформы Tequila Framework.

Области кода в приложении​

В общем случае модули приложения можно разбить на две большие группы:
  • используемые на сервере (в nodejs);
  • используемые на фронте (в браузерах);
Отсюда логично вытекает третья группа — смешанная. Модули из этой группы могут использоваться как в nodejs на сервере, так и в браузерах. Это могут быть различные утилиты/хэлперы, а также DTO, описывающие данные передаваемые между браузером и сервером.
В плагинах файловая структура исходников делится на три области:
  • ./src/
    • ./Back/
    • ./Front/
    • ./Shared/
В коде модулей из области ./Back/ допускается использование инструкций import для обращения к API nodejs и к npm-пакетам, не являющимся плагинами (без дескриптора ./teqfw.json).
import {dirname, join} from 'path';

В коде модулей ./Front/ и ./Shared/ зависимости подтягиваются через DI-контейнер, без использования иструкций import, касающихся API nodejs и npm-пакетов, не являющихся плагинами. Возможно использование import с относительной адресацией внутри одного пакета или с абсолютной адресацией статического ресурса с исходным кодом, но лучше обходиться без этого.
В контексте данной публикации будет рассматриваться только код из back-области.

Bootstrap​

Консольное приложение — это nodejs-приложение. Чтобы это nodejs-приложение могло использовать DI-контейнер, его нужно загрузить обычным способом (через import) из соответствующего npm-пакета (@teqfw/di):
import Container from '@teqfw/di';

После чего создать DI-контейнер и настроить его на использование пространств имён в пакетах @teqfw/di и @teqfw/core:
/** @type {TeqFw_Di_Shared_Container} */
const container = new Container();
const srcCore = join(root, 'node_modules/@teqfw/core/src');
const srcDi = join(root, 'node_modules/@teqfw/di/src');
container.addSourceMapping('TeqFw_Core', srcCore, true, 'mjs');
container.addSourceMapping('TeqFw_Di', srcDi, true, 'mjs');

Теперь DI-контейнер сможет находить модули с исходным кодом по их логическим идентификаторам (namespace'ам) в пакете @teqfw/core. Создаём экземпляр teq-приложения, инициализируем его, передавая через входной параметр путь к корню всего проекта и текущую версию приложения, и запускаем:
const app = await container.get('TeqFw_Core_Back_App$');
await app.init({path: root, version: '0.0.1'});
await app.run();

Полный код bootstrap-скрипта './bin/tequila.mjs'
Полный код может использоваться в качестве стартового для всех teq-приложений практически без изменения (нужно менять только номер текущей версии и путь к корню приложения).

Плагины​

Функциональность к приложению добавляется за счёт плагинов — npm-пакетов, у которых в корне пакета находится "дескриптор плагина" (файл ./teqfw.json). Структура файла зависит от того, какие именно плагины используются в приложении, но минимальное содержимое, соответствующее плагину @teqfw/di, такое:
{
"di": {
"autoload": {
"ns": "Vnd_Plugin",
"path": "./src"
}
}
}

Эти инструкции позволяют конфигурировать DI-контейнер в приложении и маппить используемые пространства имен на файловую систему (см. "Загрузка исходников").
При старте приложения запускается сканер плагинов TeqFw_Core_Back_Scan_Plugin, который пробегает по всем пакетам приложения и ищет те, которые являются плагинами (для которых есть дескриптор ./teqfw.json). Сканер добавляет найденные плагины в реестр плагинов TeqFw_Core_Back_Scan_Plugin_Registry, который доступен любому элементу кода в приложении через DI-контейнер. Структура дескриптора не определена — каждый плагин может искать в дескрипторах других плагинов понятную ему информацию. Выше я привёл часть дескриптора, которая используется DI-плагином (полная структура дескриптора для DI-плагина — в модуле TeqFw_Di_Back_Api_Dto_Plugin_Desc).

Команды​

Core-плагин использует внутри пакет commander и предоставляет сторонним плагинам интерфейс для добавления своих консольных команд к приложению. Для этого в стороннем плагине должен быть модуль, default-экспорт которого возвращает фабрику по созданию соответствующей команды (структура команды в TeqFw_Core_Back_Api_Dto_Command), а идентификатор этого модуля должен быть зарегистрирован в дескрипторе этого стороннего плагина.
Вот пример инструкций, подключающих в ./teqfw.json демо-проекта CLI-команду для вывода списка плагинов, используемых в приложении:
{
"core": {
"commands": [
"Fl64_Habr_Back_Cli_PluginsList"
]
}
}

Содержимое модуля Fl64_Habr_Back_Cli_PluginsList выглядит примерно так (оставлен только значимый для создания команды код):
export function Factory(spec) {
// EXTRACT DEPS
/** @type {TeqFw_Core_Back_Api_Dto_Command.Factory} */
const fCommand = spec['TeqFw_Core_Back_Api_Dto_Command#Factory$'];

// COMPOSE RESULT
const res = fCommand.create();
res.realm = 'demo';
res.name = 'plugins-list';
res.desc = 'Get list of teq-plugins.';
res.action = function() {/* ... */};
return res;
}

Так как core-плагин собирает CLI-команды из других плагинов приложения, то каждый плагин определяет свой риэлм (область), в котором он размещает свои команды. Это позволяет снизить вероятность возникновения конфликтов между одноимёнными командами из разных плагинов.
В результате, при запросе справки по консольным командам приложения, новая команда появляется в списке:
$ node ./bin/tequila.mjs help
Usage: tequila [options] [command]

Options:
-h, --help display help for command

Commands:
demo-plugins-list [options] Get list of teq-plugins.
core-startup-logs print out startup logs from the application core.
core-version get version of the application.
help [command] display help for command

Добавление опций в коде выше отсутствует, можно посмотреть в исходниках:
$ node ./bin/tequila.mjs help demo-plugins-list
Usage: tequila demo-plugins-list [options]

Get list of teq-plugins.

Options:
-s, --short get plugins names and namespaces
-f, --full get plugins names, namespaces and path to the sources directory
-h, --help display help for command

Резюме​

  • Логическая адресация элементов кода при помощи namespace'ов несколько упрощает документирование и конфигурирование приложения по сравнению с адресацией с привязкой к файловой системе и npm-пакетам.
  • Dependency Injection контейнер, основанный на логических идентификаторах элементов кода (namespace'ах) и динамической загрузке кода, позволяет выделить группу es-модулей (./Shared/), которые могут использоваться как в nodejs-приложениях, так и в браузерах.
  • Использование npm позволяет группировать код, связанный по функционалу, по пакетам (плагинам). При этом в одном пакете может находиться код как для фронта web-приложения, так и для серверной части ("микросервис" и "микрофронтенд" в одном npm-пакете).
  • Подобная архитектура позволяет собирать из пакетов "монолитные" web-приложения и переиспользовать пакеты в разных приложениях с похожим функционалом (точно так же, как в Wordpress, Drupal, Magento, с той разницей, что языком программирования для фронта и бэка является один и тот же — ES2015+).
  • Вполне возможно, что всё то же самое можно сделать и на TypeScript'е.
Для чего я написал эту статью?
Во-первых, чтобы получить критические замечания. Мне нужен инструмент для создания прогрессивных web-приложений, и чем больше будет конструктивной критики, тем выше вероятность, что получится хороший инструмент. Ну и, как говорится, одна голова — хорошо, а с мозгами лучше.
Во-вторых, без объяснения, что такое плагины, области кода в приложении, как добавляются CLI-команды в приложение, будет очень сложно объяснить, как добавляется web-сервер и как консольное приложение превращается в web-приложение.
Спасибо всем, кто дочитал. Можно пинать.

Источник статьи: https://habr.com/ru/post/567252/
 
Сверху