Эта статья не о том, как нужно писать приложения на 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'
#!/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-приложение.


Спасибо всем, кто дочитал. Можно пинать.

Комментарии (0)