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

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