Використання 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 — серверна реалізація авторизації з використанням 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.

Сніппети запозичив тут.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.