Пишем микросервис на 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-сервиса. Мы этим непременно займемся во второй части статьи, если, конечно, это кому-то интересно кроме меня.