@teqfw/web

от автора

Продолжаю описание своего видения того, какими могли бы быть web-приложения в нынешнее время. У меня не очень популярная точка зрения — предпочитаю чистый JavaScript (ES 2015+) на клиенте (браузер) и сервере (nodejs) и совсем не приемлю транспиляцию (даже из JS в JS для поддержки старых браузеров). Я назвал набор инструментов, разрабатываемый мной для создания таких приложений, "Tequila Framework". Просто потому, что мне нравится пустыня, кактусы и префикс "teq-".

В предыдущей публикации я показал пример практического использования пространств имён при создании консольных команд с помощью плагинов @teqfw/di и @teqfw/core. В этой статье я опишу использование плагина @teqfw/web для добавления в приложение web-сервера и предоставление доступа к статическим ресурсам приложения, в том числе и к файлам в каталоге ./node_modules/.

Введение

Значение основных используемых терминов можно посмотреть в предыдущей статье, в пункте "Определения".

Код плагина занимает пространство имён TeqFw_Web. Вот фрагмент teq-дескриптора (файла ./teqfw.json):

"di": {   "autoload": {     "ns": "TeqFw_Web",     "path": "./src"   }

и добавляет две CLI-команды в приложение:

"core": {   "commands": [     "TeqFw_Web_Back_Cli_Server_Start",     "TeqFw_Web_Back_Cli_Server_Stop"   ] }

Команды позволяют запускать и останавливать web-сервер:

$ node ./bin/tequila.mjs help ... Commands:   web-server-start [options]  Start the HTTP/1 server.   web-server-stop             Stop the HTTP/1 server. ...

HTTP сервер и процессор запроса

В nodejs есть библиотека http, которая позволяет приложению создавать web-сервер, работающий по протоколу HTTP/1. Функциональности сервера вполне достаточно для того, чтобы не прибегать к сторонним пакетам (типа expressjs).

Сервер web-плагина представляет собой обёртку над http-сервером nodejs, которая на каждый входящий запрос запускает функцию "процессор запроса" (TeqFw_Web_Back_Server_Request_Processor). В задачи процессора запроса входит:

  • извлечение данных запроса;
  • создание и инициализация контекста обработки запроса;
  • поочередный запуск всех обработчиков запроса;
  • анализ контекста и формирование ответа;

Для работы web-сервера нужно, чтобы в корне проекта существовал каталог ./var/ в который web-сервер записывает PID-файл, который используется затем для остановки сервера командой stop.

Контекст обработки запроса

Контекст — это объект, который имплементирует интерфейс TeqFw_Web_Back_Api_Request_IContext, являющийся контрактом того, на какой функционал рассчитывает процессор запроса и могут рассчитывать обработчики запроса:

  • доступ к HTTP-заголовкам запроса и телу запроса;
  • формирование HTTP-заголовка ответа и тела ответа;
  • доступ к shared-объекту, в котором обработчики могут сохранять информацию, касающуюся данного запроса (например, данные по аутентифицированному пользователю);
  • отметки об обработке запроса;

Контекст создаётся процессором на каждый запрос, передаётся от обработчика к обработчику, а затем участвует в формировании ответа процессором.

Обработчики запроса

Обработчик запроса — это объект, который имплементирует интерфейс TeqFw_Web_Back_Api_Request_IHandler.handle:

/**  * Interface for request handling function.  * @param {TeqFw_Web_Back_Api_Request_IContext} context  * @returns {Promise<void>}  * @interface  * @memberOf TeqFw_Web_Back_Api_Request_IHandler  */ async function handle(context) {}

В общем-то, весь смысл обработчика запроса — быть асинхронным и что-то делать с контекстом запроса. Если "что-то сделанное" обработчиком достаточно с его точки зрения, чтобы процессор смог сформировать ответ на запрос, то обработчик ставит в контексте метку, что запрос обработан. Остальные обработчики, видя эту метку, могут выполнять какие-то действия или пропускать обработку.

Реестр обработчиков

Web-плагин предоставляет остальным плагинам интерфейс для подключения обработчиков. Для этого в стороннем плагине должен существовать модуль, default-экспорт которого имплементирует интерфейс TeqFw_Web_Back_Api_Request_IHandler.Factory (создаёт функцию-обработчик), а идентификатор этого модуля должен быть прописан в teq-дескрипторе стороннего плагина (узел /web/handlers):

  "web": {     "handlers": [       {         "factoryId": "Vnd_Prj_Plugin_Web_Handler_Name",         "weight": 100,         "spaces": ["name"]       }     ]   }

weight указывает на место конкретного обработчика в общей очереди всех обработчиков процессора — чем выше "вес", тем ближе к началу очереди находится обработчик. Про "spaces" ниже, в "Структура URL".

Я знаю, что лучше было бы задавать порядок обработчиков на основании атрибутов before & after и иерархии между плагинами их имплементирующими, но у меня всего три-четыре обработчика, а с весами сделать всё гораздо проще. В перспективе можно сделать так:

{   "factoryId": "Vnd_Prj_Hndl",   "before": ["Vnd1_Prj1_Hndl1", "Vnd2_Prj2_Hndl2"],   "after": ["Vnd3_Prj3_Hndl3", "Vnd4_Prj4_Hndl4"] }

но на данный момент это неактуально.

Реестр обработчиков инициализируется до старта http-сервера. Он пробегает по всем дескрипторам плагинов, находит идентификаторы фабрик для обработчиков запросов, создаёт обработчики и помещает их в очередь. Эту очередь и использует процессор при обработке запросов пользователя.

Структура URL

Все web-адреса, обрабатываемые teq-приложениями, можно свести к такому виду:

https://domain.com/root/door/space/route

  • root: (опционально) путь к корню teq-приложения в пространстве домена;
  • door: область web-приложения со своими cookies и service worker’ами (например, /pub, /admin);
  • space: пространство, в котором сгруппированы ресурсы одного типа; каждый обработчик запросов, при необходимости, определяет собственные пространства (например — /src, /web, /api);
  • route: маршрут ресурса в рамках соответствующего пространства некоторого обработчика запросов;

Примеры адреса:

За разбор адреса на составляющие отвечает модуль TeqFw_Web_Back_Model_Address. Путь к корню и точки входа (doors) регистрируются в дескрипторе (./teqfw.json) головного npm-пакета проекта (в котором собираются все остальные пакеты проекта):

{   "web": {     "root": "app",     "doors": ["admin", "pub"]   } }

Пространства соответствующего обработчика указываются при его описании в дескрипторе ./teqfw.json (см. выше).

Получается примерно такая структура адреса:

Таким образом, один и тот же ресурс доступен через разные точки входа. Эти два адреса указывают на один и тот же файл, несмотря на то, что у них разные точки входа:

Обработчик запросов к статике

Одной из основных функций web-сервера является его способность отдавать клиенту статический контент (html, стили, исходники JS-модулей, медиа-файлы и т.п.). В web-плагине этим занимается обработчик TeqFw_Web_Plugin_Web_Handler_Static:

{   "web": {     "handlers": [       {         "factoryId": "TeqFw_Web_Plugin_Web_Handler_Static",         "spaces": ["src", "web"],         "weight": 10       }     ]   } }

В списке моих обработчиков он стоит самый последний (weight = 10) и обрабатывает запросы, если остальные обработчики оставили его необработанным. Как видно из описания, этот обработчик имеет два пространства для ресурсов: web и src.

src

С src попроще, в это пространство входят все модули с исходным кодом из teq-плагинов приложения. Обработчик использует данные из /di/autload/ дескрипторов плагинов и формирует карту для доступа к файлам, используя имена npm-пакетов и путей к исходникам из autoload’а. Так для плагина @teqfw/web и его настроек autoload’а:

"autoload": {   "ns": "TeqFw_Web",   "path": "./src" }

формируется такое соответствие:

http://.../root/door/src/@teqfw/web/Path/To/Module.mjs      => /.../app/node_modules/@teqfw/web/src/Path/To/Module.mjs

Именно эта карта помогает DI-контейнеру, работающему на фронте (в браузере), получать с сервера исходные коды es6-модулей, преобразовывая логические имена модулей (namespaces) в URL’ы. Другими словами, все исходные коды teq-плагинов в ./node_modules/ доступны с web’а.

web

Не кодом единым жив web-программист — есть ещё и другие статические ресурсы (те же CSS-стили, HTML, медиа-файлы и прочий download). Здесь несколько сложнее. Каждый плагин может иметь в корне каталог ./web/ со статикой на который транслируются соответствующие адреса (вне зависимости от точки входа — door):

http://.../door/web/@scope/package/styles.css => /.../app/node_modules/@scope/package/web/styles.css

Если же каталог web находится в корне проекта, то трансляция адресов происходит несколько иначе:

http://.../door/styles.css => /.../app/web/door/styles.css

Таким образом, адреса

http://.../admin/web/ui/styles.css http://.../pub/web/ui/styles.css

указывают на один и тот же файл в плагине ui (npm-пакет с именем ui):

/.../app/node_modules/ui/web/styles.css

А адреса:

http://.../admin/styles.css http://.../pub/styles.css

указывают на два разных файла:

/.../app/web/admin/styles.css /.../app/web/pub/styles.css

Подобный подход позволяет share’ить статику из плагинов в разных точках входа и одновременно, на уровне своего web-приложения, иметь контент, уникальный для каждой точки входа (например, лого или favicon.ico).

Другие пакеты из ./node_modules/

Обработчик статики позволяет транслировать URL’ы на файловую систему не только для teq-плагинов (автоматически), но и для любого npm-пакета из ./node_modules/ (вручную). Для этого в teq-дескриптор проекта (или плагина) нужно добавить инструкции по трансляции:

{   "web": {     "statics": {        "jq": "/jquery/dist/"     }   } }

После этого на фронте становятся доступными адреса:

http://.../root/door/src/jq/...      => /.../app/node_modules/jquery/dist/...

Демо

Демо-проект, использующий web-плагин — flancer64/habr_teqfw_web.

В демо-проекте задан корень адресации (demo) и две точки входа (admin и pub). Индексный файл для каждой точки входа имеет одинаковые ссылки:

<div>     <ul>         <li><a href="..">back</a></li>         <li>automatic mapping: <a href="src/@flancer64/habr_teqfw_web/Front/Module.mjs">es6-module</a> (from the             project)         </li>         <li>manual mapping: <a href="src/jq/jquery.js">jquery</a> (from ./node_modules/)</li>     </ul> </div> <div>     <div>Logo (./logo.png):</div>     <img src="./logo.png" width="150"> </div>

При этом ссылки на .../Front/Module.mjs и src/jq/jquery.js указывают на один и тот же файл для обеих точек входа, а ссылки на ./logo.png — на разные файлы.

Установка и запуск демо:

$ npm install $ npm run start ... ... HTTP/1 server is listening on port 3000. PID: ...

Затем надо открыть в браузере адрес http://localhost:3000/demo/

По-умолчанию web-сервер слушает порт 3000, но можно задать конкретный порт:

$ node ./bin/tequila.mjs web-server-start -p 3080 ... ... HTTP/1 server is listening on port 3080. PID: ...

Резюме

  • Http-сервер в nodejs сам по себе обладает достаточной функциональностью, не обязательно использовать expressjs, особенно для такой простой вещи, как раздача статики.
  • Пространства имён и настройки трансляции имён в адреса можно использовать для предоставления доступа к статическим файлам teq-плагинов (исходные коды и файлы из ./web/).
  • При необходимости можно предоставить доступ к статике любого npm-плагина (думаю, что можно даже подгружать на фронт исходные коды на TypeScript’е и уже в браузере транслировать их в JS "на лету", но я предпочитаю JS сразу).
  • Необходимость разделять адреса приложения на группы, чтобы для каждой группы можно было отдельно применять cookies и service worker’ы диктует наличие различных точек входа (doors).
  • Каждый плагин может иметь собственную статику в подкаталоге ./web/ адресация которой инварианта по отношению к корню приложения и точке входа.

Web-плагин содержит обработчик не только для запросов к статике, но и для запросов к API (сервисам приложения), но это уже другая история, т.к. в этой и так уже много букв. Спасибо всем, кто их осилил.

ссылка на оригинал статьи https://habr.com/ru/post/567630/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *