Skip to content

Введение

Что такое LiteMW

LiteMW это набор связанных библиотек для разработки серверных приложений на Typescript/Javascript.

Ключевой концепцией LiteMW является построение процессов обработки запросов
на основе промежуточных обработчиков (middleware). Этот же механизм используют такие библиотеки как: express, koa, fastify и множество других, в том числе в других языках и средах.

Промежуточные обработчики

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

Скорее всего, разрабатывая на express и подобных фреймворках, вы могли сталкиваться с библиотеками bodyparser, passport, helmet, multer. Все они представляют собой мидлвейр, который встраивается в цепочку обработки запроса до того как очередь дойдёт до ваших конечных обработчиков. Тем самым "наполняя" контекст запроса данными, среди которых например могут быть тело запроса, информация о пользователе, файлы, cookie.

Достоинства и недостатки

Подход с использованием мидлвейров (он же реализация паттерна "Chain of Responsibility" ) упрощает разработку, позволяя разбить обработку запроса на более простые блоки, выполняющие определённую функцию. Однако, когда в программе появляется много мидлвейров, которые записывают свои данные в состояние контекста обработки (хранилищем этого состояния могут быть req, res, res.locals, ctx и тд.), становится крайне трудно понять какие данные должны быть в контексте, а какие нет, даже используя типизацию Typescript .

В случае большинства фреймворков (таких, как express и koa), одном из решений может быть аугментация типа их контекста (или состояния res.locals / ctx.state). Такой вариант подходит, если ваш мидлвейр работает глобально, и соответственно записываемые им объекты будут во всех цепочках обработки (например в случае passport и bodyparser).

Что, конечно, происходит не всегда: зачастую определённые мидлвейры требуются на конкретные маршруты или контроллеры - в таком случае данное решение тоже подходит, но в типе контекста появится множество опциональных параметров, например следующий код добавит в объект req опциональные поля rights, lessons и students:

ts
declare global {
  namespace Express {
    interface Request {
      rights?: Right[]
      lessons?: Lesson[]
      students?: Student[]
      ...
    }
  }
}

Вторым вариантом является указание типов напрямую в дженериках вашей библиотеки, для express это будет выглядеть следующим образом:

ts
app.get<Params,ResBody,ReqBody,ReqQuery,Locals>(
  '/api/v1/path', (req,res) => {}
)

Соответственно, вместо Params, ResBody, ReqBody, ReqQuery и Locals вы можете указать ожидаемый тип.

Итого для типизации обычных мидлвейров требуется либо писать множество аугментаций, либо в месте использования писать множество boilerplate-кода; это всё, конечно же, замедляет процесс разработки, либо поощряет отсутствие типизации вообще.

MVC фреймворки

С решением описанной проблемы прекрасно справляются тяжеловесные фреймворки, вроде Nest или Adonis.

В большинстве случаев подобные фреймворки предоставляют Dependency Injection контейнер для управления провайдерами и контроллерами; а многие функции, такие как извлечение тела и параметров запроса, валидация, авторизация, представлены декораторами. Например в случае Nest:

ts
@Controller('lessons')
export class LessonsController {
  @Roles(Role.Admin)
  @UseGuards(AuthGuard)
  @UseGuards(RolesGuard)
  @Post('change/:id')
  changeLesson(
    @Param('id', ParseIntPipe) id: number,
    @Body() lessonChangeDto: LessonChangeDto
  ) {
    // return something
  }
}

Данный код создаёт контроллер с префиксом lessons и обработчиком post запроса по маршруту change/:id, он доступен определённой роли авторизованному пользователю, и в аргументах функции представлены параметр запроса и тело с некоторой схемой LessonsChangeDto (объекты Role, AuthGuard, RolesGuard и LessonChangeDto создаёт разработчик).

Подобное использование декораторов позволяет декларативно описывать контроллеры и обработчики, но зачастую приводит к излишнему усложнению логики. Например, в Nest существуют:

  1. Мидлвейры
  2. Пайпы (pipes)
  3. Гарды (guards)
  4. Интерсепторы (interceptor)

И зачастую сложно сразу и точно определить порядок, в котором они применяются, либо же приходится изворачиваться для того, чтобы этот порядок обойти, например, так как сначала выполнится гард, валидацию тела запроса (если оно требуется в гарде) придётся выносить в аналогичный гард.

На данном этапе можно заметить, что все представленные выше сущности технически выполняются одинаково: они получают контекст запроса, обрабатывают его, и передают управление дальше, или заканчивают обработку.

Решение

LiteMW в свою очередь предоставляет простые и прозрачные инструменты на основе мидлвейров, при этом решает проблему типизации контекста ---> Почему LiteMW