[JavaScript] @teqfw/core

Автор Сообщение
news_bot ®

Стаж: 6 лет 1 месяц
Сообщений: 27286

Создавать темы news_bot ® написал(а)
13-Июл-2021 23:31

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

Определения
Для начала зафиксирую некоторые термины:
  • приложение: комплекс программ, исполняемых на различном физическом оборудовании (сервера, смартфоны, планшеты, персональные компьютеры), использующий общую кодовую базу, доступную через 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'

SPL
#!/usr/bin/env node
'use strict';
import {dirname, join} from 'path';
import Container from '@teqfw/di';
const url = new URL(import.meta.url);
const script = url.pathname;
const bin = dirname(script);
const root = join(bin, '..');
try {
    /** @type {TeqFw_Di_Shared_Container} */
    const container = new Container();
    const pathDi = join(root, 'node_modules/@teqfw/di/src');
    const pathCore = join(root, 'node_modules/@teqfw/core/src');
    container.addSourceMapping('TeqFw_Di', pathDi, true, 'mjs');
    container.addSourceMapping('TeqFw_Core', pathCore, true, 'mjs');
    /** @type {TeqFw_Core_Back_App} */
    const app = await container.get('TeqFw_Core_Back_App$');
    await app.init({path: root, version: '0.1.0'});
    await app.run();
} catch (e) {
    console.error('Cannot create or run TeqFW application.');
    console.dir(e);
}

Полный код может использоваться в качестве стартового для всех 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-приложение.
Спасибо всем, кто дочитал. Можно пинать.
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_javascript, #_teqfw, #_dependency_injection, #_es6, #_plugin, #_commander, #_cli, #_javascript
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 29-Мар 06:14
Часовой пояс: UTC + 5