Эта статья не о том, как нужно писать приложения на 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();
#!/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-приложение.
Спасибо всем, кто дочитал. Можно пинать.