@teqfw/web: Сервисы

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

В предыдущей публикации я описал обработку статики web-плагином из своего набора инструментов для построения web-приложений, который я назвал Tequila Framework. В этой публикации я опишу, каким образом этот же плагин обрабатывает запросы к API-сервисам.


Для тех, кто ещё не в курсе — у меня несколько нестандартный подход к созданию приложений, я программирую на "чистом" JavaScript (ES 2015+) с использованием пространств имён, собственного DI-контейнера (@teqfw/di) и обильным применением аннотаций JSDoc вместо статической типизации TypeScript'а. Некоторые считают это "некошерным", а я считаю, что JS и TS — как два рукава реки. Где-то в будущем они опять сольются и, по большому счёту, всё равно, по какому рукаву плыть. Для меня на данный момент аннотации JSDoc дают основные преимущества статической типизации (контроль типов) и не влекут за собой необходимость транспиляции.



Под катом — пару подробностей, какой интерфейс сторонним плагинам предоставляет web-плагин для добавления API-сервисов в web-приложение, и как можно использовать одни и те же DTO на сервере и в браузере.


Значение основных используемых терминов можно посмотреть в пункте "Определения". Для понимания изложенного нужно предварительно ознакомиться с предыдущими публикациями:


  • @teqfw/di
  • @teqfw/core
  • @teqfw/web

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


Фабрика обработчика находится в модуле TeqFw_Web_Back_Plugin_Web_Handler_Service. В общей очереди обработчик API-запросов находится перед обработчиком запросов к статике и определяет пространство api для своих запросов (пространства описаны в "Структура URL"):


"web": {
  "handlers": [
    {
      "factoryId": "TeqFw_Web_Back_Plugin_Web_Handler_Service",
      "before": "TeqFw_Web_Back_Plugin_Web_Handler_Static",
      "spaces": ["api"],
      "weight": 100
    }
  ]
}

Основы


В контексте данной статьи, web-сервис — это некоторая функция, которая принимает входные данные, выполняет над ними некоторую операцию и возвращает назад некоторый результат. Функция находится на сервере, по некоторому адресу, доступ к ней осуществляется по HTTP. Таким образом, в сервисе можно выделить следующие составляющие:


  • функцию
  • данные запроса
  • данные ответа
  • адрес

Протокол HTTP — это правила обмена текстовой информацией по сети. От клиента к серверу и обратно пересылаются массивы текста, представляющего информацию двух типов:


  • заголовки запроса/ответа
  • тело сообщения

Обработчик API-запросов извлекает данные из тела HTTP-запроса, анализирует HTTP-заголовки запроса и находит соответствующую сервис-функцию для выполнения операции, передаёт ей входные данные, дожидается завершения обработки, формирует заголовки и тело ответа и возвращает их клиенту.


В web-плагине обработчик API-запросов не разделяет HTTP-запросы по их методу (GET, POST, DELETE, ...) и сопоставляет сервис-функции запросам исключительно на основании адреса запрашиваемого ресурса. В качестве входных и выходных данных POST-запроса используется только JSON. Сервисы применяются для выполнения каких-либо операций на сервере и для передачи-получения данных между фронтом и бэком.


Адресация


Внутри пространства ./api/ каждому teq-плагину приложения отводится своё собственное подпространство, которое совпадает с npm-именем этого плагина ("teq-плагин" = "npm-пакет" + "./teqfw.json"). Т.е., любой сервис плагина @teqfw/web будет находиться по адресу, начинающемуся на http://...[/$root][/$door]/api/@teqfw/web/. Внутри своего адресного пространства плагин сам распределяет адреса для своих сервисов:


  • /sale/order/
  • /sale/order/:id
  • /user/current/profile

Процессор запросов обрабатывает только HTTP-запросы с методами HEAD, GET, POST, PUT, DELETE, PATCH. Сервис-функция сама должна определять, каким образом ей реагировать на различные методы. В моих плагинах сервис-функции отрабатывают одинаково на все методы, а разделение "получения" и "сохранения" данных происходит на уровне адресации:


  • /sale/get/:id
  • /sale/save

Создание и регистрация сервисов


Для создания сервиса default-экспорт сервисного модуля должен имплементировать интерфейс TeqFw_Web_Back_Api_Service_IFactory и создавать:


  • маршурт (интерфейс TeqFw_Web_Back_Api_Service_IRoute);
  • сервис-функцию;

Модули регистрируются в teq-дескрипторе плагинов (./teqfw.json):


{
  "web": {
    "services": [
      "TeqFw_Web_Back_Service_Load_Config",
      "TeqFw_Web_Back_Service_Load_Namespaces"
    ]
  }
}

Маршрут


Маршрут относится к shared-коду, т.к. он используется на стороне сервера (для разбора запросов и формирования ответа) и на стороне клиента (для формирования запроса и разбора ответа), а также для определения адреса соответствующего сервиса. В плагине я располагаю модули маршрутов в каталоге ./src/Shared/Service/Route/. В отличие от большинства других модулей у модуля маршрута нет default-экспорта, т.к. он является контейнером для трёх взаимосвязанных сущностей:


  • DTO запроса;
  • DTO ответа;
  • фабрики по созданию data-transfer-объектов (также содержит адрес сервиса, которому эти запросы/ответы соответствуют);

Сервис-функция


Это чисто серверный код — асинхронная функция, которой на вход передаётся объект (контекст сервиса), из которого она извлекает нужную информацию и куда помещает результат выполнения операции:


/**
 * @param {TeqFw_Web_Back_Api_Service_IContext} context
 */
async function service(context) {}

Контекст сервиса


Контекст (TeqFw_Web_Back_Api_Service_Context) создаётся обработчиком API-запросов после того, как он находит маршрут, соответствующий запрошенному адресу, а также связанные с ним фабрики для создания DTO запроса и ответа. В контексте находятся:


  • контекст HTTP-запроса (TeqFw_Web_Back_Api_Request_IContext);
  • DTO с данными запроса;
  • параметры маршрута (для адресов типа ./sale/get/:id);
  • пустой DTO для размещения сервисом данных ответа;
  • объект для размещения сервисом заголовков ответа;

В контексте сервиса находятся структурированные данные (Object), а в запросе/ответе HTTP находится JSON-текст. За преобразование DTO <=> JSON отвечает как раз обработчик API-запросов. Все сервис-функции работают с контекстом сервиса, в котором запрос/ответ находятся в виде объекта.


Фронтенд-шлюз


Модуль TeqFw_Web_Front_Service_Gate представляет собой шлюз для выполнения клиентских запросов к сервисам. Шлюз преобразовывает входные данные в JSON и POST'ом отправляет их на сервер. В преобразовании данных и адресации сервиса используется route-объект.


В общем случае код в браузере выглядит так:


// извлекаем нужные компоненты кода через DI 
// (в конструкторе объекта или в фабричной функции)
/** @type {TeqFw_Web_Front_Service_Gate} */
const gate = spec['TeqFw_Web_Front_Service_Gate$'];
/** @type {Vnd_Prj_Shared_Service_Route_Name.Factory} */
const route = spec['Vnd_Prj_Shared_Service_Route_Name#Factory$'];

// выполняем запрос (в какой-то внутренней функции или методе)
/** @type {Vnd_Prj_Shared_Service_Route_Name.Request} */
const req = route.createReq();
req.param = 'value';
/** @type {Vnd_Prj_Shared_Service_Route_Name.Response} */
const res = await gate.send(req, route);
if (res) {
    ...
}

Шлюз во время выполнения запроса использует объект с интерфейсом TeqFw_Web_Front_Api_Gate_IAjaxLed для оповещения пользователя о том, что выполняется AJAX-запрос, и объект с интерфейсом TeqFw_Web_Front_Api_Gate_IErrorHandler для оповещения пользователя о возникших ошибках. Default имплементации этих интерфейсов из web-плагина просто выводят оповещения на консоль. Разработчик на уровне своего приложения может в ./teqfw.json переопределить имплементации этих интерфейсов и обрабатывать события на своё усмотрение:


{
  "di": {
    "replace": [
      {
        "orig": "TeqFw_Web_Front_Api_Gate_IAjaxLed",
        "alter": "Vnd_Prj_Front_Model_Gate_AjaxLed",
        "area": "front"
      }, {
        "orig": "TeqFw_Web_Front_Api_Gate_IErrorHandler",
        "alter": "Vnd_Prj_Front_Model_Gate_ErrorHandler",
        "area": "front"
      }
    ]
  }
}

Обработка ошибок


Ошибкой считается такое выполнение запрошенной операции при котором соответствующая сервис-функция не смогла завершиться (произошло исключение) или обработчик API-запросов не смог сформировать ответ из результатов сервис-функции. Исключения перехватываются на уровне обработчика API-запросов и передаются клиенту в виде ответа с кодом 500 (Internal Server Error). Фронтенд-шлюз в такой ситуации вызывает объект с интерфейсом TeqFw_Web_Front_Api_Gate_IErrorHandler для оповещения пользователя об ошибке, а вызывающему коду возвращает false вместо данных ответа сервиса.


Демо


В качестве демонстрации работы сервисов привожу приложение @flancer64/habr_teqfw_web, выполняющее запрос к сервису "Fl64_Habr_Web_Back_Service_Load_Plugins" — на получение кратких данных по всем плагинам этого приложения.


Компоненты сервиса:


  • Fl64_Habr_Web_Back_Service_Load_Plugins: конструктор сервис-функции;
  • Fl64_Habr_Web_Shared_Service_Route_Load_Plugins: объект, содержащий адрес сервису, структуру запроса/ответа и фабрики для создания запросов/ответов;

На фронте сервис используется моделью Fl64_Habr_Web_Front_Module.


Так как сервис не использует данные запроса, то обычный GET-запрос также отрабатывает.


Резюме


На примере web-сервисов видно, каким образом может использоваться один и тот же код, как в браузере, так и в nodejs-приложениях:


  • Fl64_Habr_Web_Shared_Service_Dto_Plugin
  • Fl64_Habr_Web_Shared_Service_Route_Load_Plugins

Т.к. в демо-приложении используется уже достаточное количество es-модулей, то в браузере можно видеть "файловую структуру", загруженную с сервера:



Использование "интерфейсов" (классов, в которых функциональность только обозначена, но не реализована) и "имплементаций" (классов с реализованной функциональностью, которые DI-контейнер загружает вместо "интерфейсов") позволяет одни и те же web-сервисы использовать как в web-сервере на базе библиотеки nodejs/http, так и на базе библиотеки nodejs/http2. И даже совмещать оба сервера в одном проекте (например, для разработки использовать HTTP/1, а для production'а — HTTP/2). Об этом в следующей статье.

Источник: https://habr.com/ru/post/568044/


Интересные статьи

Интересные статьи

Всем привет! Меня зовут Виктория, в Typeable я занимаюсь вопросами архитектуры приложений и не могла пройти мимо вечного вопроса: быть или не быть? Точнее переводить нам ...
Персональный компьютер Lenovo ThinkCentre M75n на процессоре AMD Ryzen 5 PRO Журнал Computer Reseller News (CRN) ежегодно составляет рейтинг Internet of Things 50, выбирая полсотни «...
В обновлении «Сидней» Битрикс выпустил новый продукт в составе Битрикс24: магазины. Теперь в любом портале можно создать не только лендинг или многостраничный сайт, но даже интернет-магазин. С корзино...
Сравнивать CRM системы – дело неблагодарное. Очень уж сильно они отличаются в целях создания, реализации, в деталях.
В прошлых публикациях мы рассказывали про успех Spotify в Индии и про то, как потоковые платформы подстегнули продажи винила. Сегодня речь пойдет о том, как стриминговые сервисы меняют подход к н...