Пишем микросервис на KoaJS 2 в стиле ES2017. Часть I: Такая разная ассинхронность

Пишем микросервис на KoaJS 2 в стиле ES2017. Часть I: Такая разная ассинхронность

У Вас никогда не возникало желания переписать все с чистого листа, «забить» на совместимость и сделать все «по уму»? Скорее всего KoaJS создавался именно так. Этот фреймворк уже несколько лет разрабатывает команда Express. Экспресовцы про эти 2 фреймворка пишут так: Philosophically, Koa aims to «fix and replace node», whereas Express «augments node» [С филосовской точки зрения Koa стремится «пофиксить и заменить ноду» в то время как Express «расширяет ноду»].

Koa не обременен поддержкой legacy-кода, с первой строчки вы погружаетесь в мир современного ES6 (ES2015), а в версии 2 уже есть конструкции из будущего стандарта ES2017. В моей компании этот фреймворк в продакшене уже 2 года, один из проектов (AUTO.RIA) работает на нагрузке полмиллиона посетителей в день. Несмотря на свой уклон в сторону современных/экспериментальных стандартов фреймворк работает стабильнее Express и многих других фреймворков с CallBack-style подходом. Это обусловлено не самим фреймворком, а современными конструкциями JS, которые в нем применяются.

В этой статье я хочу поделиться своим опытом разработки на koa. В первой части будет описан сам фреймворк и немного теории по организации кода на нем, во второй мы создадим небольшой рест-сервис на koa2 и обойдем все грабли, на которые я уже наступил.

Немного теории

Давайте возьмем простой пример, напишем функцию, которая читает данные в объект из JSON-файла. Для наглядности будем обходиться без «reqiure('my.json')»:

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

Попробуем решить эту задачу в nodejs style с помощью callback-функций:

Тут с ассинхронностью все хорошо, а вот удобство работы с кодом пострадало. Есть еще вероятность, что мы забудем проверить наличие ошибки 'if (err) return callback(err)' и при возникновении исключения при чтении файла все «вывалится», второе неудобство заключается в том, что мы уже погрузились на одну ступеньку в, так-называемый, callback hell. Если ассинхронных функций будет много, то вложенность будет расти и код будет читаться очень тяжело.

Что же, попробуем решить эту задачу более современным способом, оформим функцию readJSON промисом:

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

Это ситуацию, пока что, ощутимо не меняет, есть косметическое улучшение красоты кода, возможно, стало понятнее что за чем выполняется. Кардинально ситуацию изменило появление генераторов и библиотеки co, которые стали основой движка koa v1. Пример:

В месте, где используется директива yield, происходит ожидание выполнения ассихронного readJSON. readJSON при этом необходимо немного переделать. Такое оформление кода получило название thunk-функция. Есть специальная библиотека, которая делает из функции, написанной в nodejs-style в thunk-функцию thunkify. Что это нам дает? Самое главное — код в той части, где мы вызываем yield, выполняется последовательно, мы можем написать

и получить последовательное выполнение сначала чтения 'my.json' потом 'my2.json'. А вот это уже «callback до свидания». Тут «некрасивость» в том, что мы используем особенность работы генераторов не по прямому назначению, thunk-функция это нечто нестандартное и переписывать все для koa в такой формат «не айс». Оказалось, не все так плохо, yield можно делать не только для thunk-функции, но и промису или даже масиву промисов или объекту с промисами. Пример:

Казалось, лучше уже не придумаешь, но придумали. Сделали так, чтоб все было «по прямому» назаначению. Знакомьтесь, Async Funtions:

Не спешите запускать, без babel этот синтаксис ваша нода не поймет. Koa 2 работатет именно в таком стиле. Вы еще не поразбегались?

Давайте разберемся как работает этот «убийца колбеков»:

с промисамы уже знакомы.

() => — так обозначается «стрелочная функция», аналогична записи function () . У стрелочной функции есть небольшое отличие — контекст: this ссылается на объект, в котром инициализируется стрелочная функция.

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

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

Теория закончилась — можем приступать к первому запуску KoaJS.

Знакомьтесь, koa

«Hello world» для koa:

  • common function
  • async function
  • generatorFunction

Также с точки зрения фазы выполнения кода, middleware делится на две фазы: до (upstream) обработки запроса и после (downstream). Эти фазы разделяются функцией next, которая передается в middleware.

common function async function (работает с транспайлером babel) generatorFunction

В случае такого подхода необходимо подключить библиотеку co, которая начиная с версии 2.0 уже не является частью фреймворка:

Поддерживаются также legacy middleware от koa v1. Надеюсь, в вышестоящих примерах понятно, где upstream/downstream. (Если нет — пишите в комменты)

В контексте запроса ctx есть 2 важных для нас объекта request и response. В процессе написания middleware мы разберем некоторые свойства этих объектов, по указанных ссылкам вы можете получить полный перечень свойств и методов, которые можно использовать в своем приложении.

Пора переходить к практике, пока я не процитировал всю документацию по ECMAScript

Пишем свой первый middleware

В первом примере мы расширим функционал нашего «Hello world» и добавим в ответ дополнительный заголовок, в котором будет указано время обработки запроса, еще один middleware будет писать в лог все запросы к нашему приложению. Поехали:

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

Стоит отметить, что если в middleware не вызывается метод next, то все middleware, которые подключены после текущего, принимать участие в обработке запросов не будут.

При тестировании примера не забывайте подключить babel

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

C этим заданием koa справляется шикарно. Например, мы хотим в случае любой ошибки отвечать пользвателю в json-формате 500 ошибку и свойство message с информацией про ошибку.

Самым первым middleware пишем следующее:

Все, можете попробовать в любом middleware бросить исключение с помощью 'throw new Error(«My error»)' или спровоцировать ошибку другим способом, она «всплывет» по цепочке к нашему обработчику и приложение ответит корректно.

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