Skip to content

Создание приложения

В данном разделе показан пример простейшей программы с использованием LiteMW, рассмотрены особенности наших мидлвейров и примеры работы с ними; возможности роутеров, и возможности работы с состоянием контекста.

Первая программа

Рассмотрим минимальный код для запуска приложения.

ts
import { createRouter } from '@litemw/router';
import Koa from 'koa';

const router = createRouter('/api');

router.get('/endpoint', (ctx) => {
    console.log(`Get request handled`);
    ctx.body = 'response'
})

const app = new Koa();
app.use(router.routes());
app.listen(3000)

Здесь используется функция createRouter из пакета @litemw/router для создания LiteMW роутера, она принимает опциональные аргументы: префикс, который будет применён к каждому дочернему обработчику и опции идентичные опциям Koa роутера, схематично представим её тип:

ts
function createRouter<...>(
  prefix?: string,
  opts?: RouterOptions
): Router<...>;

В реальности её тип сложнее, так как префикс и методы параметризуют тип роутера для проверок на этапе компиляции.

В свою очередь RouterOptions имеет следующий интерфейс:

ts
interface IRouterOptions {
  prefix?: string | undefined;     // Префикс
  methods?: string[] | undefined;  // Методы
  sensitive?: boolean | undefined; // Чувствительность к регистру
  strict?: boolean | undefined;    // Строгая проверка (с учётом количества слэшэй)
}

Также роутер имплементирует интерфейс IBaseRouter из библиотеки koa-router и в поле koaRouter содержит базовый роутер

ts
const router = createRouter()...  // satisfies IBaseRouter
type T = typeof router.koaRouter  // KoaRouter

Принципы мидлвейров

В LiteMW мидлвейры используются как для построения хэндлеров, так и роутеров с помощью метода use(...). Каждый мидлвейр наполняет состояние контекста во время обработки запроса. Схематично можем представить тип метода следующим образом:

ts
function use<NewState extends State, Return>(
  mw: (ctx {..., state: NewState}, next: NextFunction) => Return
): RouterOrHandler<State & Return>;

Где State - текущий тип состояния роутера или хэндлер, NewState - тип состояния, которое ожидает мидлвейр, Return - возвращаемый тип мидлвейра. После выполнения мидлвейра в цепочке обработки возвращённое значение добавится в состояние контекста (с перезаписью существующих свойств). Применение метода use вернёт нам тот же самый роутер или хэндлер, но параметризованный другим типом.

Примеры

Рассмотрим несколько примеров использования мидлвейров. Предположим мы хотим распарсить тело запроса в мидлвейре parseBody и провалидировать в validateBody:

ts
const parseBody: Middleware<any, { someBody: string }> = async (ctx) => {
  const someBody = '';
  await new Promise(resolve => {
     ctx.req.on('data', (chunk) => {/* collect body to string */});
     ctx.req.on('end', resolve)
  })
  return { someBody };
};

const validateBody: Middleware<{ someBody: string }> = (ctx) => {
  // do validation
};

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

Попробуем их использовать:

ts
const router = createRouter('/api');

router.get('/endpoint')
  .use(parseBody)     // Ok
  .use(validateBody)  // Ok
  .use((ctx) => {
    console.log(ctx.state.someBody); // string
  })

При обработке запроса тело будет получено, провалидировано и распечатано. Но что будет если поменять их порядок местами?

ts
const router = createRouter('/api');

router.get('/endpoint')
  .use(validateBody)  // TS2345: Argument of type ... is not assignable to ...
  .use(parseBody)     
  .use((ctx) => {
    console.log(ctx.state.someBody); // string
  })

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

Вложенность роутеров

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

ts
const v1Router = createRouter('/v1')
... // some definitions
const latestRouter = createRouter()
... // some definitions

const apiRouter = createRouter('/api')
apiRouter.use(v1Router)
apiRouter.use('/latest', latestRouter)

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

Состояние по-умолчанию

В состоянии контекста даже при отсутсвии каких-либо мидлвейров уже будут поля router, который доступен в мидвейрах как роутера, так и хэндлера; и handler, который доступен только в мидлвейрах хэндлера. Из них можно получить информацию о префиксе, маршруте и всём что есть в роутере и хэндлере соответственно.

ts
const router = createRouter('/api')
  .use(ctx => {
    ctx.router // <Router>
  });

router.get('/endpoint', (ctx) => {
  ctx.router // <Router>
  ctx.handler // <RouteHandler>
})

Аугментация состояния

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

ts
declare module '@litemw/router' {
  interface DefaultState {
    someString: string;
    someNumber: number;
  }
}

Указанные поля будут доступны во всех обработчиках.

ts
const router = createRouter('/api')
  .use(ctx => {
    ctx.someString // string
    ctx.someNumber // string
  });

router.get('/endpoint', (ctx) => {
  ctx.someString // string
  ctx.someNumber // string
})

Завершение обработки

В некоторых случаях может возникнуть потребность в прерывании цепочки обработки запроса. Сделать это можно с помощью вызова метода cancel() у объекта next.

Рассмотрим пример:

ts
const router = createRouter('/api')
  .use(async (ctx, next) => {
    console.log('First middleware')
    await next()
    console.log('After first middleware')
  })
  .use((ctx, next) => {
    console.log("Interrupted")
    ctx.body = "Interrupted"
    next.cancel()
  })
  .use(ctx => {
    ... // do some work
  });

router.get('/endpoint', (ctx) => {
  ...
})

При обращении по адресу /api/endpoint будет возвращена строка 'Interrupted', то есть после вызова next.cancel() последующие мидлвейры выполнены не будут, а управление вернётся к предыдущим мидлвейрам где был вызван next.

В консоли будет выведено:

First midddleware

Interrupted

After first middleware

Данный механизм может быть полезен, когда требуется прервать обработку запроса, но при этом выполнить предудущие мидлвейры в цепочке. Альтернатива этому - выброс исключения, которое потребует блока try/catch в одном из предыдущих мидлвейров.