Продолжаю описание своего видения того, какими могли бы быть 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 (сервисам приложения), но это уже другая история, т.к. в этой и так уже много букв. Спасибо всем, кто их осилил.