RxJS: реактивне розширення фронтенд для розробки

Про реактивне програмування вже написано сотні статей. Фронтенд не зміг уникнути цього тренда, але інтерес до теми досі дуже і дуже високий. Тому ми просто не могли не взяти інтерв'ю у одного з наших майбутніх доповідачів.
Отже, прошу любити і жалувати, Віктор Русакович. Родом з Мінська, працює в компанії GP Software.travel.
Віктор останні п'ять років займається (в основному) фронт-енд розробкою. Ну а починав, як і більшість з нас, з jQuery.
Потім був backbone, angular v1. Останні півроку він працює в проекті на Redux/React.js (часто їх плутають з RxJS, але це трохи інше).


В твоєму проекті активно використовується реактив. Можеш пояснити, що це таке і з чого це рух почалося? Зараз у всіх на слуху RxJS, RxJava, RxPython, RxBasic, ну і хіба що RxBrainfuck немає.


Дійсно, один з моїх попередніх проектів був наскрізь пронизаний використанням бібліотеки RxJS. Всі AJAX-запити, робота з DOM-подіями, часом просто обробка статичних даних — все це проходило через RxJS.

Для початку пара слів про «реактивному програмуванні» як такому. Наприклад, в житті з «реактивністю» ви стикаєтеся в Excel:


Як бачите, для автоматичного обчислення суми були створені комірки з формулами. На мові реактивного програмування це можна зобразити у вигляді двох потоків даних, gross та taxes, і третього потоку net, який буде збирати дані з net і gross та аггрегировать їх по нашій формулі, перетворюючи на підсумкове значення.
Я буду користуватися дуже зручним онлайн-редактором, щоб показувати всі свої приклади. Цей редактор хороший тим, що прямо в брузере рендерить результат роботи програми. Ну і сама класна фіча радактора в тому, що потім всі фрагменти залишаться доступними по прямой ссылке. Мій перший приклад тут.

var gross = Rx.Вами.just(5000)
var taxes = Rx.Вами.just(13)
var net = gross.combineLatest(taxes, (g, t) => g - t / 100 * g)

net.subscribe(money => document.getElementById('out').value = money)


В інтернеті можна знайти багато варіантів визначення «реактивного програмування»: у Вікіпедії, тут, на Хабре. Тема серйозна, навіть існує спеціальний маніфест, який можна підписати, якщо ви згодні з його ідеями — тільки навіщо? Особисто мені всі ці визначення не подобаються, і я придумав своє:
Реактивне програмування — це коли ти замість обробки подій по одному объединяешь їх в потік і потім вже працюєш тільки з ним.
(Дата, Підпис, Печатка).




Якщо ж говорити про те, як все це зародилося, то історія появи RxJS така.
Році в 2010-2011 хлопці з Microsoft, які працювали c .NET, вирішили, що непогано було б і для JS зробити реактивну бібліотеку. Справа в тому, що в .NET вже досить давно був популярний LINQ. Наприклад, ось так з допомогою LINQ можна підрахувати кількість входжень певного слова в рядку.

string searchTerm = "data";
//Convert the string into an array of words
string[] source = text.Split(new char[] { '.', '?', '!', '', ';', ':', ',' }, StringSplitOptions.RemoveEmptyEntries);
// Create the query. Use ToLowerInvariant to match "data" and "Data" 
var matchQuery = from word in source
where word.ToLowerInvariant() == searchTerm.ToLowerInvariant()
select word;
// Count the matches, which executes the query.
int wordCount = matchQuery.Count();


Нам посчастливлось почати працювати з самої першої версії бібліотеки. Код тоді ще був не в GitHub, а в якомусь власному сховищі від Microsoft. Крім того, особливості ліцензії не дозволяли включати в проект неминифицированную версію. Величезною проблемою була документація — доводилося читати статті, написані для .NET, і намагатися розуміти на рівні концепцій, незважаючи на відмінності в мовах. Саме тоді я зрозумів, що можу писати на будь-якій мові програмування. :)



Як взагалі ви прийшли до ідеї використовувати RxJS?


В кожному проекті є свої особливості, які впливають на вибір тієї чи іншої технології. Рішення може бути обсуловленно складністю інтерфейсу, наявністю або відсутністю чіткого Roadmap'a, який показує, що проект буде розвиватися і ускладнюватися як мінімум кілька років. А ще в 2012 ми не змогли знайти нічого іншого, схожого на RxJS. Нам була важлива хороша технічна підтримка, і ми піддалися бренду Microsoft.

Навіщо ж нам знадобився RxJS в той момент?
Уявіть собі додаток, в якому багато незалежних UI компонентів.
Тепер додаємо сюди штук 10 API запитів (від входу до перевірки ціни продукту). Так, є.
Потім групуємо компонети під View так, що деякі використовуються в різних View, а деякі — ні. Добре виходить.
Тепер нам треба навчити компоненти змінювати свій стан між запитами. Десь показати шар з спиннером, десь сховати дані, коли повернеться помилковий запит. Причому, компонент може залежати від декількох API і, більш того, іноді має значення не поодинокий запит, а серія запитів і їх відповіді. І вишеньку на торт — на вулиці 2012 рік, Angular немає, Backbone не підходить, значить, будемо працювати тільки з jQuery. Без фреймворка.

RxJS підійшов ідеально. Ми перестали ловити окремі події і будувати величезні піраміди callback'ів. Що таке піраміда з callback не потрібно пояснювати, сподіваюся? :)
Замість цього між компонентами у нас з'явилися потоки з даними, з яких ми могли створювати нові потоки, що потрібні в конкретній ситуації. Наприклад, є у нас потік з вибором якогось продукту. Якщо користувач додає щось у кошик, то в цьому потоці з'являється подія з додатковим продуктом. Якщо користувач видаляє продукт — в потоці з'являється подія з порожнім масивом. Припустимо, ми хочемо, щоб таблиця моргала червоним кожен раз, коли видаляється щось з кошика:
var cart = require('streams/cart')
var removalsFromCart = cart
.bufferWithCount(2,1) 
.where(history => history[1].length < history[0].length)

removalsFromCart.subscribe(() => $('table').setClassFor1Sec('warning))


Посилання на сніппет.

.bufferWithCount() буде брати попереднє та поточне події та передавати їх масивом далі. В .where() ми перевіряємо, що поточна кошик (вона буде другим у масиві після .bufferWithCount) містить менше елементів, ніж попередня. Якщо умова правдиве, то дані передаються далі в метод .subscribe(), потрапити в який є метою кожної події в потоці. Якщо б не використання методу .where(), то кожна зміна кошика викликало б моргання.

Значить, причини були саме такі. Як ти вважаєш, а з якихось причин ще має сенс починати використовувати реактивний підхід у проектах?


На самому початку ми всі-всі події перетворювали в Rx.Вами. З часом ми зрозуміли, що це не завжди виправдано. Наприклад, якщо ми на 100% впевнені, що клік по цій кнопці викликає перезавантаження сторінки, то ніяких додаткових змін на сторінці не потрібно. Тепер такі прості і не пов'язані з іншими події ми обробляємо без RxJS.

Але як тільки вам треба стежити за багатьма подіями, тут без RxJS не обійтися.
Наприклад, це можуть бути вебсокеты, що створюють величезні потоки даних.
Це можуть бути анімації, які починаються при отриманні одного запиту, а при закінченні повинні запустити наступний запит.

Але ось за що ми особливо полюбили RxJS, так це за те, що при написанні всіх цих .where(), .buffer(), .map() та ін. методів нам абсолютно не важливо, що є джерелом даних. Це може бути масив або ж вебсокет, який може надіслати за 100 об'єктів в секунду, а потім раптом зупинитися на хвилину. Всі ці події рівні для нас і обробляються однаково.

А це означає, що, розібравшись з асинхронними речами, ми можемо почати використовувати RxJS для обробки абсолютно синхронних даних, наприклад, позначити жирним всі зовнішні посилання:
Rx.Вами.from(document.querySelectorAll('a'))
.where(el => el.getAttribute('href').indexOf('http') === 0)
.subscribe(el => el.style.fontWeight = 'bold')


Посилання на сніппет.
Резюмуючи, скажу, що, як мені здається, RxJS ідеально підійде для проектів з великими потоками даних, на які треба реагувати — ігри, чати і прості сайти, у яких є якісь складні оновлення на сторінці. Не дарма, думаю, RxJS включили в Angular 2.

А які ще рішення є для JS?


Відразу згадується кілька альтернатив. В першу чергу це BaconJS. Бібліотека створена приблизно в той самий час, коли і RxJS. Що цікаво, в одному з проектів ми навіть намагалися вибрати між RxJS і Bacon — це було 4 роки тому, коли обидві бібліотеки тільки-тільки вийшли. Але тоді схилився в бік RxJS, так як Bacon програвав за кількістю стандартних методів і більш простому API в цілому. Ну, і ще одним важливим фактором, як я вже сказав, було те, що розвитком і підтримкою займався лише Juha Paananen, а за RxJS стояла Microsoft. Сьогодні користуватися BaconJS можна без будь-яких побоювань, тому що гарне спільнота вже сформувалося, API добре задокументовано, і можна знайти багато чудових прикладів.

Наступна альтернатива — це KefirJS (у мові ще залишилися слова, до яких не додали JS? :). Чудова бібліотека для реактивного програмування, підтримувана нашим співвітчизником Романом Поминовым. Роман створював KefirJS, намагаючись взяти простоту API від BaconJS (порівняно з RxJS) і відразу виправляючи помилки в продуктивності. І знаєте, вийшло добре! Ми, наприклад, користуємося кефіром кожен день в одному з проектів.

Невже все так райдужно? Можо просто взяти і використовувати ці фреймворки?


Є нюанси.
Року 3 назад після доповіді про RxJS на конференції в мене запитали: «чи Правильно я зрозумів, що для того, щоб використовувати реактивне програмування, потрібно змінити спосіб мислення?» Це був самий правильний питання за всі мої доповіді! Дуже часто він читався в очах слухачів, але поставили його тільки раз. І відповідь на нього: «Так!»

Реактивний підхід дуже відрізняється від того, як ми звикли писати програми. Тепер треба спочатку написати код, який буде описувати шляху проходження подій, а вже потім запускати ці події в програму, де вони автоматично будуть викликати необхідні дії.
Це перша проблема.

Друга проблема полягає в тому, що часом важко зрозуміти, який саме метод можна застосувати в конкретному випадку. Наприклад, ви перетворили якісь скалярні дані запити. Як тепер отримати доступ до відповіді сервера? Припустимо, ви прочитали опис усіх методів в RxJS і навіть запам'ятали .switch(): «Transforms an вами sequence of вами sequences into an вами sequence producing values only from the most recent вами sequence»…
Ну ви зрозуміли — без jsfiddle не розібратися.

З цим важко боротися, треба буде пережити момент, поки знань API недостатньо для невпинного кодинга. Тут стане в нагоді проект rxmarbles.com — його автор (André Staltz) створив велику кількість динамічних часових діаграм, за якими можна простежити, як саме впливає порядок даних на підсумковий потік. Однак, методу .switch() там не знайшлося. :(

На щастя є reactivex.io/documentation/operators/switch.html, де можна знайти докладний і людське опис основних методів та їх часових діаграм, і switch() у тому числі:


Видно, що метод .switch() повертає дані з вкладених потоків. Причому, що важливо, дані з вкладеного потоку надходять на вихід до тих пір, поки не з'являється новий вкладений потік, який незабаром почне виробляти дані: зверніть увагу на те, що жовтий круг не пройшов, хоча жовтий трикутник ще не з'явився.
Саме цей метод ми зазвичай використовуємо, коли працюємо з ajax — як тільки відправлено новий запит, дані, які можуть повернутися з попереднього запиту, нам не цікаві. Тепер ми ніколи не побачимо застарілий відповідь сервера.

Виходить, що при належному умінні і бажанні цілком можна з цим працювати і використовувати в продакшн. З чого почати і які ресурси можна почитати?


Сьогодні ресурсів дуже багато. Тільки ресурсів з посиланнями на ресурси десятки, на всі смаки: хтось любить відео-курси, хтось- читання документації з прикладами, комусь книги подавай. Я б почав з цього списку:

  • egghead.io/series/introduction-to-reactive-programming — відмінний вступний курс від автора RxMarbles;
  • reactivex.io/intro.html — багато теорії від авторів RxJS;
  • www.reactivemanifesto.org — маніфест (куди без нього?);
  • xgrommx.github.io/rx-book — багато документації з репозиторію RxJS, але є й багато своїх хороших статей, наприклад про Backpresure в RxJS (а це, між іншим, частину маніфесту!);
  • www.introtorx.com — збірник статей, які увійшли в однойменну книгу (на сайті є безкоштовно mobi версія для Kindle). Не лякайтеся, що все .NET — відмінне читання, щоб зрозуміти концепцію. Так і .NET дуже схожий на TypeScript;
  • Статті по тегу FRP на Хабре;
  • А я раджу почати з підключення проект rx.lite.js і перекладі ваших Ajax (Fetch) запитів на Rx.Вами.




Сподіваюся, що у вас прокинулося бажання викинути імперативний підхід і почати нарешті слідувати реактивної парадигмі! Вистачить тягнути дані! Нехай дані самі пройдуть за созданым ланцюжках!
До речі, доповідь для holyjs.ru вже готовий, залишилися невеликі доопрацювання. У доповіді я розповім на прикладах про практичне застосування RxJS в ваших проектах. Так що побачимось в Петербурзі на конференції 5-ого червня!
Джерело: Хабрахабр

0 коментарів

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