Автор: Александр Трищенко, 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 — все они должны содержать свой итератор. Теперь мы можем получить доступ непосредственно к самому итератору. Также появилась возможность заставить перебираться неперебираемое, и порой это может быть очень удобно.
Что такое итератор? Как вы можете видеть, существует простейший массив. У простейшего массива есть ключ — символ итератора, по сути, фабрика, которая возвращает итератор. Каждый вызов этой фабрики вернет новый экземпляр итератора, мы можем перебирать их независимо друг от друга. В итоге мы получили переменную, хранящую ссылку на итератор. Далее, с помощью единственного метода 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.
Сниппеты заимствовал тут.
Комментарии (24)
mrjj
14.10.2016 00:13+2То есть коллбэк хелл, который обычно банально означает малый опыт работы с JS, мы теперь заменяем семантическим адом.
sugadu
14.10.2016 01:24Поделитесь, пожалуста, примером как избежать колбэк-хелла без промисов и их производных.
Large
14.10.2016 02:01+2Создавайте больше методов с небольшим количеством вложенных колбеков или используйте библиотеку асинк. Это не рекомендация промисы с асинк/евейт это здорово, но избежать ада можно было и без этого =)
mrjj
14.10.2016 14:28Модуляризация, наличие конвенции о размещении коллбэков в сигнатурах, наличие центральных оркестраторов приложения вроде стейт машин или механизма эвентов, хотя у последнего есть множество своих минусов.
И почему без промисов? Они по сути просто удобная обертка над двумя-тремя коллбэками. Стоит познакомиться с FRP это развитие идее, когда мы вместе с коллбэками упаковываем контекст выполнения, и по возможности оформляем код как мэппинг входного потока данных на выходной.
Еще очень полезно познакомиться с другими функциональными языками программирования, например LISP/clojure или Haskell, в них многие вещи вроде мэппингов, функторов, делегатов, монад и т.п. более наглядны без шелухи си-подобного синтаксиса.
homm
14.10.2016 02:29+3Вы переворачиваете все с ног на голову и запутываете новичков. Вы начинаете с того, что «генераторы позволяют писать синхронный код». Бог с ней, с формулировкой (правильно было сказать «генераторы позволяют писать асинхронный код в синхронном стиле»). Но ведь это откровенная неправда. Генераторы знать ничего не знают о синхронности или асинхронности вашего кода. Они — простой способ записи итераторов. Удобный способ описания и работы с последовательностями данных. Это фреймворки, использующие генераторы, позволяют что-то там делать с кодом.
kurtov
14.10.2016 10:51+1Согласен, возьмем пример из статьи
function* renderUserData(render) { let user = yield getUser(); let paymentDetails = yield getUserDetails(user.payment.id); render(paymentDetails); }
первый yield вернет user? Нет, он вернет promise, которого где то снаружи функции нужно дождаться, чтобы снова вызвать next(), тоже самое для getUserDetails(). Код не полный, а приводится как довод в пользе генераторов.tryshchenko
14.10.2016 13:24В том примере опущен вызов функции rendrer для случаем с промисами в том числе, это не ошибка. Основной идеей было описать принцип работы без контекста его вызова.
kurtov
15.10.2016 13:49+1Сверху и снизу вы видите абсолютно идентичные по функциональности сниппеты.
Но это не так, в случае с промисами вам достаточно вызвать функцию. В случае с генераторами вам нужно вызвать функцию и построить цепочку промисов самостоятельно, попутно возвращая в next() полученное значение. Поправьте если я ошибаюсь.
// Вызов варианта с промисом renderUserData(); // Вызов варианта с генератором let generator = renderUserData(); generator.next().value .then(user => generator.next(user).value) .then(paymentDetails => generator.next(paymentDetails ).value)
Очень похоже на то, что мы использовали генератор ради использования генератора. Возможно вы скажете, что внешний вызов генератора можно отдать на откуп сопрограммам типа co.js — соглашусь, что это позволяет получать некий профит.
Смысл в том, что генераторами нельзя заменить промисы (для асинхронных вызовов). Вообще никак. Async\await — да, киллер фича, нас ждет светлое будущее (заглянуть в будущее уже можно с помощью babel и подобных)tryshchenko
17.10.2016 19:17Как по мне — идея «заменить генераторы промисами» в корне не верна. Мы вполне можем использовать промис внутри нашей сопрограммы и в результате выполнения промиса передавать управление генератору. В то же время, ничего нам не мешает использовать для асинхронного действия в сопрограмме функцию обратного вызова и решить задачу вообще без промисов.
tryshchenko
14.10.2016 13:21Спасибо за комментарий. В статье я раскрывал конкретно реальное использование генераторов в качестве сопрограмм. В частности раскрывается способность генераторов приостанавливать свое выполнение, которую мы и используем для нового способа реализации асинхронных действий.
Я везде писал «псевдосинхронный» вместо «синхронный» код. И почему же генераторы этого не позволяют? Вполне себе позволяют, хоть, как вы и говорите, ничего об асинхронном коде сами по себе знать не должны.
Ничего не мешает объявить генератор как переменную, например, myGenerator и внутри сопрограммы продолжить выполнение родительского генератора с помощью myGenerator.next().
kurtov
14.10.2016 10:15+1такие как Map, Set, WeakMap, WeakSet — все они должны содержать свой итератор
WeakMap, WeakSet — не содержат и не должны, иначе они бы не были Weak
kurtov
14.10.2016 10:35+4Если использовать промисы как коллбэки, то зачем их вообще использовать?
// Пример из статьи (render) => { getUser().then((user) => { getUserPaymentDetails(user.payment_id).then((paymentDetails) => { render(paymentDetails); }); }); }; // Без лишних скобок (render) => { getUser() .then(user => user.payment_id) .then(getUserPaymentDetails) .then(render); };
tryshchenko
14.10.2016 13:12Спасибо, дельное замечание, Ваш вариант выглядит лучше моего. Но возвращаясь к сути вопроса — у нас остается цепочка и необходимость определять новую функцию / метод для последующих действий в чейне.
baka_cirno
14.10.2016 11:08+1Promises можно делать плоскими, не обязательно вкладывать друг в друга.
tryshchenko
14.10.2016 13:09Вы имеете ввиду образование цепочек с помощью последовательного вызова .then() на примере выше?
ilfroloff
14.10.2016 13:59Может немного оффтоп, но все же.
Странная мания пошла популяризировать фичи, которые еще не скоро появится. Да, у async-await — stage 3, но официальную публикацию спецификации планируют "аж" в 2017, а поддержка в браузерах и того боле. Все-таки, в мире javascript решения плодятся быстрее, чем дрожжи, и полгода-год — это очень много.
Вся эта катавасия, с новыми фишками, вынуждает использовать всякие
babel
ы-транспайлеры, которые фаршируют код в непредсказуемую кашу с возможными багами. Выполнение кода на серверной стороне должно быть максимально безопасным и предсказуемый, а транспайлеры убивают все это в пух и прах.
Все ИМХО
BoryaMogila
14.10.2016 19:31По поводу async-await, и babel, отлично себя показали в продакшене, по всем показателям, неговоря про удобство.
А на koa 2.x можно писать и на промисах, тут дело выбора написать так:
(render) => { getUser() .then(user => user.payment_id) .then(getUserPaymentDetails) .then(render); };
или так
async function (render) { let user = await getUser() let paymentDetails = await getUserPaymentDetails(user.payment_id) await render(paymentDetails) };
Посути async-await транспилится в тотже промис и возращает тоже промис.
BoryaMogila
16.10.2016 10:14Т. е., если мы говорим о маршрутизации, о различных полезных HTTP-методах, о системах безопасности, защите от CSRF-атак, о кроссдоменных запросах, шаблонизаторах и т. д. — в koa ничего этого мы не найдем.
В ядре нет, но все это есть в виде библиотек, заточеных именно под коа. Большенство сдесь. А остальное можна найти в npm
Igelko
17.10.2016 17:09А почему бы в случае уже существующего express-проекта не взять bluebird и использовать его
Promise.coroutine
для обёртывания генераторов? Снаружи это выглядит, как обычный promise, который можно скормить в express, а внутри это выглядящий линейным код.
В принципе текущая реализация async-функций совместима с Promise и можно использовать её as is в свежих версиях node (6+?), как справедливо заметили выше.tryshchenko
17.10.2016 19:24Вариантов много, я привел лишь некоторые из них.
В хроме, кстати, тоже работает async await: https://www.chromestatus.com/feature/5643236399906816
Проблема async / await в том, что спецификация еще не утверждена. Маловероятно, но можно столкнуться с тем, что спецификация к окончательному утверждению документации может измениться (хоть это и маловероятно). В любом случае, тот же koa2 прекрасно работает с async / await.
blare
17.10.2016 23:49Значит, в итоге может получиться так же, как с захватом экрана в WebRTC: появились приложения использующие эту функциональность, а в результате от нее отказались.
А можно про это подробнее?
Strate
Но ведь и для express можно написать обработчик роута в async/await стиле: