Wprowadzenie do funkcjonalnego programowania reaktywnego w JavaScript z RxJS

Maciej Woźnica | Rozwój oprogramowania | 26.10.2022

Współczesne aplikacje internetowe wymagają od programistów coraz wydajniejszych rozwiązań. Czasy, kiedy główną funkcjonalnością strony WWW było wyświetlenie statycznego tekstu, obsługa kliknięcia czy wysłanie wiadomości za pomocą formularza, mamy dawno za sobą.
Bardzo często jako twórcy aplikacji musimy mierzyć się z coraz większymi wymaganiami użytkowników oraz rosnącą ilością danych, co znacząco wpływa na wydajność naszych witryn. Z pomocą przychodzi programowanie reaktywne – w artykule omawiam jego możliwości na przykładzie biblioteki RxJS.

Wyzwania współczesnych aplikacji internetowych

Tworząc nowoczesne i wydajne aplikacje internetowe, mierzymy się z wieloma wyzwaniami, o czym wspominał m.in. Paweł Adamowicz w swoim materiale Zarządzanie stanem aplikacji frontendowej za pomocą NgRx. Jako programiści powinniśmy między innymi brać pod uwagę to, że nie można blokować użytkownika i kazać mu czekać, aż nasza witryna obsłuży kilka zdarzeń. Co zatem zrobić w momencie, kiedy aplikacja jednocześnie musi uruchomić animację, załadować loader czy zareagować na działanie użytkownika? Jak obsłużyć te wszystkie sytuacje naraz? Tutaj z pomocą przychodzi nam paradygmat programowania reaktywnego wraz z biblioteką RxJS.

RXJS - programowanie reaktywne

Czym jest programowanie reaktywne?

Programowanie reaktywne to paradygmat służący do pisania kodu opierającego się na asynchronicznych strumieniach danych.

Jest to sposób tworzenia aplikacji, które reagują na zachodzące zmiany, w przeciwieństwie do typowego, imperatywnego sposobu pisania oprogramowania, w którym jawnie tworzymy instrukcję krok po kroku, aby te zmiany obsłużyć.

Brzmi to dość skomplikowanie i faktycznie takie podejście wymaga zmiany sposobu myślenia. Jednak istnieją rozwiązania, które zdecydowanie ułatwią zmianę podejścia na reaktywne – w przypadku JavaScript jest nim właśnie biblioteka RxJS.

Strumienie danych

Angular cover 1 - Wprowadzenie do funkcjonalnego programowania reaktywnego w JavaScript z RxJS

Angular – zróbmy swoją PWA.

PRZECZYTAJ ARTYKUŁ

Strumień to sekwencja danych uporządkowanych w czasie. Strumienie danych mogą być tworzone z obiektów, zmiennych, struktur danych, za pomocą zdarzeń (kliknięć), odpowiedzi żądań HTTP itd.

Na swojej osi czasu strumień może również emitować error (gdy wystąpi nieoczekiwane zdarzenie powodujące zakończenie strumienia) lub status completed, gdy strumień wyemitował wszystkie dane. Możemy przechwycić te metody (error i complete) i wykonać na nich odpowiednie dla nas operacje, o czym więcej piszę w dalszej części.

Co to jest observable?

RxJS udostępnia nam obiekt observable, który wykorzystuje wzorzec projektowy – obserwator.

Możemy to zilustrować na przykładzie subskrybcji do newslettera. Zapewne nieraz zdarzyło ci się kliknąć przycisk „subskrybuj”, czy to na YouTube, czy np. jakimś blogu. W momencie kiedy pojawi się nowy artykuł lub inny materiał, dostajesz od razu powiadomienie o pojawieniu się nowych zasobów. W każdym momencie możesz także zrezygnować z subskrypcji.

Tak samo działają observable i strumienie.

  • Klikając przycisk subscribe, stajesz się obserwatorem danych zasobów,
  • observable jako obiekt obserwowany serwuje nam strumień danych z nowościami,
  • subskrypcja do strumienia zapewnia nam metodę „subscribe”,
  • możemy również wybrać, które dane są dla nas wartościowe jako nowości.

Observable vs promise w JavaScript

Observable to nic innego jak strumień z wartościami (absolutnie dowolnego typu). Ale zaraz, zaraz… Po co nam ten cały observable, skoro mamy już promise w JavaScript? Observable daje nam dużo większe możliwości, poniżej krótkie porównanie:

Promise

  • za każdym razem jest asynchroniczny, 
  • jest „eager” – przetwarzanie rozpoczyna się natychmiastowo po jego zdefiniowaniu, 
  • może zwracać tylko jedną wartość, 
  • nie posiada operatorów, 
  • nie może być anulowany.

Observable

  • może być zarówno synchroniczny, jak i asynchroniczny,
  • jest „lazy” – będzie wykonany nie w momencie zdefiniowania strumienia, ale w momencie, w którym utworzona zostanie subskrypcja,
  • zwraca jedną lub wiele wartości,
  • do dyspozycji mamy całą gamę operatorów,
  • możemy go anulować w dowolnym momencie,
  • daje nam więcej możliwości przy obsłudze błędów (m.in. operator retry()).

Hot vs cold observables

Observable dzielimy na hot i cold.

Cold observables:

  • emitowanie wartości zaczyna się w momencie pojawienia się pierwszego subskrybenta (gdy pojawi się pierwszy subscribe() – o tym w dalszej części artykułu),
  • dla każdego nowego subskrybenta zwracają nowe wartości,
  • przykładem może być np. serwis do pobierania danych z backendu, gdzie żeby wykonać zapytanie, musimy użyć subscribe.

Hot observables:

  • wysyłają wartości pomimo braku subskrybenta,
  • dzielą te same dane pomiędzy wszystkich subskrybentów,
  • przykład: observable stworzony z eventu click.

Tworzenie observable

Istnieje wiele sposobów na stworzenie observable. Często spotykamy się z tworzeniem ich z innych struktur danych – tutaj z pomocą przychodzą nam operatory of lub from. Dla RxJS nie jest również problemem stworzenie jednego observable z kilku innych. Poniżej kilka przykładów.

Operator from przyjmuje tylko jeden argument, po którym można iterować (np. tablice, elementy tablicopodobne, weźmy za przykład tablicę obiektów).

 

import { from, Observable } from 'rxjs'; 

  

const to ReadBooks = [  

{  bookId: 1, title: The psychology of money', author: 'Morgan Housel' 

publicationYear: 2020 }, 

{  bookId: 2, title: 'The subtle art of not giving a f*ck', author: 'Mark Manson', 			publicationYear: 2016 }, 

{ bookId: 3, title: How to talk to anyone', author: 'Leil Lowndes', publicationYear: 1999 }, 

{ bookId: 3, title: 'Invent and wander', author: Jeff Bezos', publicationYear: 2020 }, 

  

];  

  

let source2$ = from(toReadBooks); 

source2$.subscribe(book => console.log(book.title)); 

W ten sposób nasza tablica obiektów zamieniła się w observable. Proste, prawda?

A co w sytuacji, gdy mamy kilka strumieni, a potrzebujemy jednego observable? Żaden problem! RxJS udostępnia kilka operatorów, które nam w tym pomogą. Jednym z nich jest concat.

import { concat, from, Observable, of } from 'rxjs'; 

  

const toReadBooks = [ 

  

  

{  bookId: 1, title: The psychology of money', author: 'Morgan Housel'  

publicationYear: 2020 },  

{  bookId: 2, title: 'The subtle art of not giving a f*ck', author: 'Mark Manson', 			publicationYear: 2016 },  

{ bookId: 3, title: How to talk to anyone', author: 'Leil Lowndes', publicationYear: 1999 },  

{ bookId: 3, title: 'Invent and wander', author: Jeff Bezos', publicationYear: 2020 }, 

  

];  

  

let source1$ = of('hello', 10, true, toReadBooks[0].title); 

source1$.subscribe(value => console.log(value)); 

  

let source2$ = from(toReadBooks); 

source2$.subscribe(book => console.log(book.title)); 

  

// combine the 2 sources  

concat(source1$, source2$) 

.subscribe(value => console.log(value)); 

Voilà! RxJS naprawdę ułatwia życie!

Subskrybcje

Powyżej wspomniałem o możliwości subskrypcji do strumienia. Kolejny termin, który brzmi skomplikowanie (ale wcale taki skomplikowany nie jest!). Subskrybcja to po prostu podłączenie się do strumienia. Każdy observable posiada metodę subscribe(), do której możemy przekazać parametry na dwa sposoby: jako obiekt z metodami lub jako zestaw callbacków. Obiekt przekazany do metody subscribe nazywamy observerem.

Observer

Observer posiada trzy składowe:

  • pierwsza metoda (next) jest wykonana, jeśli uda nam się odebrać wartość ze strumienia. Każda nowa wartość powoduje wywołanie tej metody,
  • druga metoda (error) jest wykonana, jeśli w strumieniu wystąpi błąd, na przykład w zapytaniu HTTP dostaniemy status 500. W przypadku wystąpienia błędu nasz observer nie przejdzie dalej i nie wykona metody complete, a tym samym zostanie zaznaczony jako closed i przestanie emitować wartości,
  • trzecia metoda (complete) jest wykonana w momencie, gdy strumień wyemituje ostatnią wartość.

RxJS – zalety

Biblioteka RxJS niewątpliwie ułatwia rozwój aplikacji. Możemy ją zintegrować właściwie z dowolnym frontendowym frameworkiem (Angular, React). Również użycie z czystym JavaScriptem nie stanowi problemu. Do dyspozycji mamy ogromną liczbę operatorów, które pozwalają nam na dowolną modyfikację naszych strumieni. Stale pojawiające się aktualizacje sprawiają, że kolejne wydania biblioteki są coraz prostsze w użyciu. Dzięki RxJS zdecydowanie możemy przyspieszyć naszą pracę, jak również poprawić jakość wytwarzanego oprogramowania.

RxJS – wady

Tutaj zdecydowanie trudniej jest mi coś napisać niż w przypadku zalet. Pracując na co dzień z Angularem, nie wyobrażam sobie napisania choćby jednego komponentu bez użycia biblioteki RxJS. Na pewno rzeczą, która sprawia trudność – zwłaszcza początkującym programistom – jest testowanie kodu wykorzystującego RxJS. Testowanie takiego kodu wymaga znajomości wielu dodatkowych technik oraz narzędzi. Na szczęście RxJS ma na to swoje rozwiązanie i tu z pomocą przychodzi nam RxJS Marbles (umożliwia testowanie asynchronicznego kodu RxJS synchronicznie i krok po kroku za pomocą narzędzia testowego RxJS TestScheduler oraz przy użyciu wirtualnych kroków czasowych), jednak jest to dość obszerny temat, któremu można by poświęcić nowy wpis.

RXJS - programowanie reaktywne

Podsumowanie

Powyższe informacje stanowią bardzo ogólny opis programowania reaktywnego oraz biblioteki RxJS. O każdym z powyższych tematów śmiało mógłby powstać oddzielny artykuł, a mój to zaledwie kropla w morzu – wiele aspektów zostało pominiętych.

Jednak bez dwóch zdań używanie RxJS stało się standardem w tworzeniu nowoczesnych i wydajnych aplikacji. Ciągły rozwój i wsparcie dla najpopularniejszych frameworków sprawia, że RxJS jest zdecydowanie najczęściej używaną biblioteką ułatwiającą zapanowanie nad asynchronicznością.

TOPICS