Использование ES6 генераторов на примере koa.js
Автор: Александр Трищенко, Senior Front-end Developer, DataArt
Содержание
• Итераторы. Генераторы.
• Использование генераторов (Redux, Koa)
• Зачем нам использовать koa.js
• Будущее. Async Await и koa.js 2.x
Генераторы — новая спецификация, новая возможность, которую мы можем использовать в ECMAScript 6. Статью я начну с рассказа об итераторах, без которых понять генераторы не получится, расскажу непосредственно про спецификацию и о том, что такое генераторы вообще, про их использование в реальных кейсах. Рассмотрим два примера: React + Redux как фронтненд-случай и koa.js в качестве бэкенда. Затем подробнее остановлюсь на koa.js, будущем JavaScript, на асинхронных функциях и koa.js 2.
В статье использованы, в том числе, и заимствованные сниппеты (ссылки на источник приведены в конце), и я сразу прошу прощения, что части кода выложены в виде картинок.
ECMAScript 6 (2015) поддерживается достаточно хорошо, чтобы его использовать. На диаграмме видно, что в принципе все неплохо даже у Microsoft в Edge, большие проблемы наблюдаются только у Internet Explorer (вертикальная ось координат — поддержка функциональности, в %). Приятно удивляет Safari 10, по заявлениям команды WebKit, работает все.
Итераторы
• Теперь все, что можно перебрать, — итерируемый объект (iterable).
• Все, что не перебирается само по себе, — можно заставить с помощью своего Symbol.iterator.
Каждый перебираемый, итерируемый тип данных, каждая итерируемая структура данных, получает итератор и становится iterable. Можно перебрать строку, массив, можно перебрать новые структуры данных, такие как Map, Set, WeakMap, WeakSet — все они должны содержать свой итератор. Теперь мы можем получить доступ непосредственно к самому итератору. Также появилась возможность заставить перебираться неперебираемое, и порой это может быть очень удобно.
Что такое итератор? Как вы можете видеть, существует простейший массив. У простейшего массива есть ключ — символ итератора, по сути, фабрика, которая возвращает итератор. Каждый вызов этой фабрики вернет новый экземпляр итератора, мы можем перебирать их независимо друг от друга. В итоге мы получили переменную, хранящую ссылку на итератор. Далее, с помощью единственного метода next, мы его перебираем. Метод next возвращает нам объект, который содержит два ключа, первый — value — непосредственно значение итератора, второй — состояние итератора — done: false.
Описать итератор мы можем самостоятельно. В принципе, он представляет собой фабрику, обычную функцию. Предположим, что у нас есть функция endlessNumbers, есть индекс и метод next. Объект с единственным методом next, который возвращает итерируемое значение и статус. Этот итератор никогда не дойдет до конца, потому что мы никогда не будем присваивать ключу done значение true.
Применяются итераторы довольно широко, особенно в приложениях, которые реализуют нестандартные подходы к работе с информацией. В свободное время я занимаюсь написанием секвенсора на JavaScript с использованием Web Audio API. У меня есть задача: проигрывать с определенными интервалами какую-то ноту. Укладывать это в какой-то цикл было бы неудобно, поэтому я использую итератор, который просто «плюется» нотами в медиаплеер.
Предпосылки для появления генераторов возникли уже давно. Вы можете увидеть динамику популярности Node.js за последние пять лет, она косвенно отражает популярность JavaScript в целом. Также на график отражает частоту запроса Callback Hell — он пропорционально зависит от распространения JavaScript. То есть, чем популярнее становился JavaScript, тем больше страдали разработчики и клиенты.
Лапша, представленная на изображении — это структурная визуализация кода, написанного без генераторов. То есть это то, с чем всем нам приходится работать — мы к этому привыкаем и, не имея выбора, воспринимаем как данность. Когда разработчики начали попытки борьбы с этим явлением, появилась такая штука как promise. Идея была в том, чтобы взять все наши Callback (функции обратного вызова) и «размазать» их по всему коду, объявляя там, где нам удобнее. Однако на самом деле у нас остались те же самые функции обратного вызова, просто представленные в немного другом виде. Разработчики продолжили борьбу — так появились генераторы и асинхронные функции.
Генераторы
Применение
• Написание синхронного кода.
• Написание приостанавливаемых функций.
• Написание комплексных итераторов.
Генераторы позволяют писать синхронный код. Раньше я говорил, что достоинство JavaScript как раз в его асинхронности, теперь поговорим, как писать на JavaScript синхронный код. На самом деле, это код будет псевдосинхронным, поскольку Event Loop не будет останавливаться, если у вас есть какие-то тайм-ауты в фоне, пока вы будете ожидать выполнения приостановленного генератора. Вы вполне можете выполнять в фоновом режиме любые другие нужные операции. Мы можем написать приостанавливаемые функции, применение которых достаточно узко, в отличие от асинхронных действий, применяемых очень широко. Появилась и возможность написания комплексных итераторов. Например, мы можем написать итератор, который будет каждый раз ходить в базу и по очереди итерировать одно значение из нее.
Сверху и снизу вы видите абсолютно идентичные по функциональности сниппеты. У нас есть некая функция, которая должна сходить в базу данных и забрать user.payment_id. После чего она должна сходить на внешнюю API и забрать payment details user, актуальные на текущий день. У нас возникает явление The Pyramid Of Doom, когда функция обратного вызова находится внутри другой функции обратного вызова, и степень вложенности может увеличиваться бесконечно. Естественно, результат не всегда получается приемлемым, даже инкапсулировав все операции в отдельные функции, на выходе мы все равно получаем «лапшу».
Есть решение, которое позволяет нам сделать то же самое с помощью генератора: у генератора немного другой синтаксис — function* (со звездочкой). Генератор может быть именованный и неименованный. Как вы можете видеть, у нас появилось ключевое слово yield. Оператор yield, который приостанавливает выполнение генератора, позволяет нам дождаться выполнения метода GetUser. Когда мы получим данные, положим их в user, после чего продолжим выполнение, таким же образом получим paymentDetails, и тогда сможем отрисовать всю информацию для нашего пользователя.
Рассмотрим возможность реализации генераторов — как мы можем их перебирать. Здесь мы видим уже описанную ранее конструкцию. Как было показано на итераторе, здесь тоже есть итератор числа, который будет нам возвращать значение от 0 до 3, и который мы будем перебирать. То есть, мы можем использовать метод next.
Метод next ()
• Может принимать в качестве аргумента значение, которое будет проброшено в генератор
• Возвращаемое значение — объект с двумя ключами:
value — часть выражения, получаемая из генератора.
done — состояние генератора.
Метод next ничем не отличается от аналогичного в итераторах, мы можем получить value и done, как два параметра, можем пробросить значение в генератор и получить значение из генератора.
Следующий вопрос — производительность. Насколько имеет смысл использовать то, о чем мы говорили? На момент доклада средств тестирования в моем распоряжении не было, поэтому я написал свое. В результате тысячи итераций удалось добиться среднего значения по различным технологиям. В Chrome обещания и генераторы не сильно отличаются друг от друга, причем в большую сторону отличаются то одни, то другие. Если учесть, что время, затраченное на выполнение одной итерации с помощью Callback, Promise или генераторов, исчисляется в миллисекундах, в реальности особенной разницы нет. Я думаю, что заморачиваться, экономя на спичках, не стоит. А значит, можно свободно использовать то, что вам больше по душе.
Ни один современный доклад о JavaScript не может обойтись без React. Я в частности буду говорить о Redux.
redux-saga
• Это библиотека.
• Это библиотека, написанная на генераторах.
• Это библиотека, которая прячет impure-функции с глаз долой.
• Это библиотека, которая позволяет вам писать синхронный код.
В функциональном программировании есть очень важный принцип — мы должны использовать «настоящие функции». Наша функция не должна влиять на окружение, должна работать с теми аргументами, которые мы ей передаем. В том или ином виде наши функции обратного вызова часто превращаются в impure function, и этого, конечно, хочется избежать. Отсюда и основное назначение redux-saga — возможность писать синхронный (псевдосинхронный) код.
Суть заключается в том, что таким же образом мы можем с помощью генераторов останавливать выполнение нашей саги. Можно сказать, что saga — своеобразный аналог action, который вызывает другой action. Дождавшись ответа, мы с помощью диспетчера инициируем нужное событие в нашем reducer и передаем необходимую информацию.
Суть достаточно проста: в результате мы выполнили асинхронное действие очень просто и быстро. Собственно, минимальная saga выглядит так: есть генератор, который обращается к нашей saga, вызывает takeEvery — один из методов saga, который позволяет нам инициировать событие «USER_FETCH_REQUESTED» внутри нашего редюсера. Возможно, вы обратили внимание, что yield здесь идет со звездочкой. Это является делегацией операции генератора, мы можем делегировать наш генератор другому генератору.
redux-saga: послесловие
• Саги (Sagas) не декларируются, как обычные Actions, их необходимо внедрять через sagaMiddleware.
• Очевидно, что сам sagaMiddleware — нечто иное, как middleware вашего store в Redux.
Мы поговорили про фронтенд, теперь пришло время рассказать про бэкенд, т. е. о koa. Я сталкивался со многими фреймворками на бэкенде, наиболее интересными для меня показались kraken.js и koa.js, на втором остановлюсь подробнее.
koa.js
В двух словах это:
• node.js-фреймворк для серверной разработки.
• node.js-фреймворк, который использует ES6-генераторы, асинхронные функции ES2016.
• node.js-фреймворк, написанный командой express.js.
Учитывая авторитетность команды express.js, ресурсы компании, фреймворк вызывает доверие и быстро развивается. На данный момент вокруг него образовалось солидное сообщество, он оброс кучей библиотек — зачастую найти какое-то решение для middleware koa очень просто.
• «Фреймворк нового поколения»
Что такое koa? По сути, это фреймворк, который предоставляет нам движок для посредников (middleware), а их архитектурная диаграмма очень похожа на хорошо знакомую всем игру в испорченный телефон. Здесь имеется состояние, которое по очереди передается между middleware, каждый из которых влияет или не влияет на это состояние (я дальше покажу пример логера, который влияние не оказывает). С этим middleware мы и будем работать. Напомним, что koa.js — фреймворк middleware на генераторах. Т. е., если мы говорим о маршрутизации, о различных полезных HTTP-методах, о системах безопасности, защите от CSRF-атак, о кроссдоменных запросах, шаблонизаторах и т. д. — в koa ничего этого мы не найдем. В koa есть только движок для middleware, причем множество из них написано самой командой koa.js.
Так выглядит максимально простое приложение на koa.js. Есть реализация логирования — простейшая имплементация middleware на koa.js. Это генератор, который возвращает свое состояние и перед тем, как его вернуть, и после того, как оно вернется, что позволяет подсчитать время, затраченное на выполнение нашего приложения. Обратите внимание, что выполняются они в порядке объявления: то, что объявили выше, начнет работать прежде всего.
koa.js
Преимущества:
• Наличие огромного количества библиотек, обернутых в co.js.
• Модульность и легковесность.
• Возможность писать более понятный код.
• Возможность писать меньше кода.
• Высокая активность сообщества.
Казалось бы, koa.js — бедный фреймворк, в котором нет почти ничего. В то же время, существует множество библиотек, и большая часть стандартного сервисного функционала представлена в виде middleware. Нужны кроссдоменные запросы — просто подключаете пакет и пробрасываете middleware. Если требуются настройки — необходимо просто передать параметры, и у вас будут кроссдоменные запросы. Необходима авторизация с помощью jwt- token — то же самое: понадобятся три строчки кода. Если необходимо работать с базой данных — пожалуйста.
Таких случаев много — работа с фреймворком становится похожа на игру с конструктором: от вас требуется только пробовать разные пакеты, и все будет работать. Таких возможностей инкапсуляции все очень ждали, и теперь они в нашем распоряжении. Как результат отсутствия функциональности внутри фреймворка, он стал легче, также отсутствуют какие-то стандартные компоненты, которые потом надо допиливать. Появилась возможность писать более понятный код. Генераторы позволяют писать псевдосинхронный код, таким образом, можно сократить количество непонятных и ненужных вещей в приложении. Вследствие этого появилась и возможность писать меньше кода. Присутствует активная поддержка сообщества, множество плагинов, которые начинают конкурировать между собой. Выигрывают лучшие, многие при этом отсеиваются, что в целом, конечно, полезно.
В таблице представлено сравнение поставки Koa, Express и Connect фреймворков. Как вы можете видеть, в koa нет ничего, кроме middleware ядра.
Стоит сказать пару слов о самом co.js:
Co.js — это обертка вокруг генераторов и обещаний, которая позволяет нам упростить работу с асинхронными операциями. Более корректно обозначить co как «Сопрограммы (coroutines) в JavaScript». Идея сопрограмм не нова, и существует в других языках программирования очень давно.
Основная идея заключается в передаче управления из основной программы в сопрограмму, которая в свою очередь может вернуть управление основной программе. Собственно часть этого процесса и реализуют генераторы в JavaScript.
Если привести все к более привычным для JS-разработчика материям — co.js выполняет генератор, избавляя нас от необходимости последовательного вызова next () генератора. В свою очередь co возвращает другое обещание, что позволяет нам отследить его завершение и отловить ошибки (для этого можно использовать метод catch). Самое крутое — в co можно выполнить yield для массива промисов (а ля Promise.all). Стоит заметить, что co прекрасно справляется с делегацией генераторов.
koa.js
Пара полезных пакетов для старта:
• koa-cors — разрешаем кроссдоменные запросы одной строкой.
• koa-route — полноценный роутинг.
• koa-jwt — cерверная реализация авторизации с использованием jwt-токена.
• koa-bodyparse — парсер тела приходящих запросов.
• koa-send — управление статикой.
Выше в качестве примера приведены несколько middleware, которые вы можете использовать в реальном приложении. koa-cors пакет позволяет обеспечить кроссдоменные запросы, koa-route обеспечивает роутинг, аналогичный тому, что есть в Express, и т. д.
Основные недостатки koa.js — обратная стороной того, что фреймворк поставляется голым, необходимость постоянно контролировать качество пакетов, которые при этом не обещают быть зависимыми друг от друга, и иногда избавляться от багов становится сложно. Вторая проблема — подбор команды, потому что на данный момент, к сожалению, с koa.js работает не так много людей. Из-за этого увеличивается время на введение нового человека в проект. И если проект маленький, это может оказаться нерентабельным. Т. ч. использовать koa.js в работе нужно с умом.
koa.js 2
Фреймворк нового поколения?
Koa.js 2 — очень хитрый фреймворк. Он работает на спецификации, которой нет. То есть вы можете найти статьи об асинхронных функциях, где сказано, что это ECMAScript 7 или ECMAScript 2016. Но самом деле несмотря на то, что Babel, Google Chrome и Microsoft Edge поддерживают асинхронные функции, их не существует. Многие ожидали, что асинхронные функции войдут в официальный релиз ECMAScript 7 (2016), но в итоге тот вышел с исправлениями дефектов и двумя новыми возможностями, чем новшества и ограничились. А тем временем koa.js 2 на асинхронных функциях работает, разработчики на них пишутся. И все это позиционируется как фреймворк нового поколения.
Async functions
Обзор
• Async — это Promise.
• Await — это Promise.
Асинхронные функции — и Async, и Await — это Promise.
Допустим, у нас есть такой код. Если убрать async и await, поставить возле функции звездочку и поставить yield перед conquer, получится генератор. Казалось бы, в чем разница? А она в том, что мы ожидаем в conquer обычный Promise, не надо оборачивать наши асинхронные функции ни в какие генераторы, это просто не требуется — мы можем взять обычный новый метод для получения запроса сервера fetch. Потом необходимо дождаться результата, а когда мы его получим, положим в state и таким образом вернем состояние.
Async functions
Послесловие
• Асинхронные функции удобнее генераторов (меньше кода, нет необходимости оборачивать промисы для генераторов).
• Асинхронные функции пока еще не часть стандарта и не ясно, станут ли они его частью.
Асинхронные функции, безусловно, удобнее генераторов, они позволяют нам писать меньше кода. В этом случае нет необходимости писать обвязочный код, можно взять любую библиотеку, которая возвращает нам promise (а это почти все современные библиотеки). Это позволяет сэкономить много времени и денег. Минус — асинхронная функция — все еще черновик спецификации. Значит, в итоге может получиться так же, как с захватом экрана в WebRTC: появились приложения использующие эту функциональность, а в результате от нее отказались.
Мораль всего, о чем я рассказывал в статье, довольно проста: я не говорю, что генераторы —замена Promise или Callback. Я не утверждаю, что асинхронные функции могут заменить генераторы, функции обратного вызова и обещания. Но у нас появились новые инструменты для написания кода, позволяющие делать его красивым и структурированным. Но их использование остается на вашем усмотрение. Решать стоит с точки зрения рациональности и применимости каждого инструмента в конкретно вашем проекте.
Список используемых ресурсов
Рисунки заимствовал тут: 1, 2, 3.
Сниппеты заимствовал тут.
Комментарии (2)
13 октября 2016 в 22:35
0↑
↓
Но ведь и для express можно написать обработчик роута в async/await стиле:
app.get("/url", async function(req, res, next) { let list = await getListFromDb(); lst processedList = await processList(list); res.send(processedList) })
14 октября 2016 в 00:13
0↑
↓
То есть коллбэк хелл, который обычно банально означает малый опыт работы с JS, мы теперь заменяем семантическим адом.