Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
В предыдущей публикации я описал обработку статики 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). Об этом в следующей статье.