Wzbogać język o możliwości programowania asynchronicznego. Pokaz możliwości Angular + RxJS
Angular, czyli framework JavaScriptowy staje się coraz popularniejszy, a wytwarzane w nim aplikacje są ciekawsze i bardziej rozbudowane. Aby aplikacja była atrakcyjna dla użytkownika musi być jemu przyjazna. Osiągnąć można to przez różnego rodzaju interakcje. W tym artykule chciałbym pokazać czym jest RxJS, co potrafi i jakie daje korzyści.
Mateusz Pacholec. Senior Software Developer w Objectivity. Pasjonat nowych rozwiązań i technologii. Na co dzień pracuje przy aplikacji finansowej. Poza projektem dzieli się wiedzą prowadząc Lightning Talki i wykłady. Można go znaleźć na LinkedIn pod tym adresem.
Spis treści
Czym jest Angular?
Postanowiłem zaprezentować rozwiązanie RxJS wraz z Angularem. Dlatego zanim przejdę do omówienia najważniejszego to kilka słów o samym Angularze. Angular jest to framework frontendowy rozwijany przez Google. Charakteryzuje się tym, że dostarcza on wszystko co potrzebne do wytworzenia pełnej aplikacji, bez konieczności szukania zewnętrznych rozszerzeń.
Główne cechy frameworka:
- Wieloplatformowość → za pomocą Angulara możemy tworzyć aplikacje na każde urządzenie, np. responsywne aplikacje webowe, progressive web apps, natywne aplikacje mobilne (Cordova, Ionic).
- Szybkość i wydajność → wysoka wydajność generowanego kodu, uniwersalny framework (dowolny język jako backend), przemyślany podział kodu aplikacji.
- Produktywność → świetny silnik do budowy layoutu, który pozwala na łatwe manipulowanie wyglądem, interakcją z użytkownikiem oraz dostarcza dwukierunkowego bindingu danych. Ponadto dostajemy świetne narzędzie jakim jest Angular CLI, za pomocą którego łatwiej wejdziemy w nowy projekt, będziemy mogli go szybciej rozwijać i co najważniejsze — ułatwi utrzymanie projektu.
- Pełen proces wytwarzania → na koniec nie mniej ważny pełen proces wytwarzania oprogramowania — Angular dostarcza dodatkowo możliwość testowania własnych komponentów, dostarcza zoptymalizowanych animacji, a także zapewnia wysoką dostępność, poprzez wsparcie ARIA.
Taka wiedza wystarczy, aby przejść do tematu głównego. Po więcej informacji odsyłam do oficjalnej dokumentacji, która znajduje się pod adresem angular.io/docs.
Czym jest RxJS?
RxJS czyli Reactive Extensions for JavaScript to zestaw rozszerzeń do języka JavaScript, wzbogacający ten język o możliwość programowania reaktywnego, czyli programowania asynchronicznego opartego na zdarzeniach. Dzięki zastosowaniu takich rozszerzeń zyskujemy ogromne możliwości w interakcji aplikacji z użytkownikiem. Ponadto samo czytanie i utrzymanie kodu napisanego z użyciem RxJS jest nieporównywalnie lepsze niż np. w klasycznym zastosowaniu jQuery z manipulacją interfejsu, która wykonywana jest “na piechotę”.
Cały koncept RxJS oparty jest na klasach opisanych poniżej.
- Observable → najważniejszy obiekt w RxJS. Reprezentuje on strumień wartości w czasie. Observable wysyła powiadomienia, a także można go subskrybować, czyli po prostu z niego skorzystać.
- Observer → obiekt, który otrzymuje powiadomienia z nowymi wartościami. Jest przekazywany do metody subscribe -> posiada metody next, error, complete.
- Operatos → operacje jakie możemy wykonać na observable, o czym w dalszej części artykułu.
- Subscription → typ Subskrypcji umożliwia przypisanie subskrypcji observable do zmiennej. Co nam to daje – możliwość jawnego odsubskrybowania danego źródła. Daje to lepsze możliwości zarządzania cyklem życia takiej subskrypcji.
- Subject → jest to typ, który jest jednocześnie typu observable i observer. Subject pozwala na ręczne wywołanie metody next() w dowolnym czasie. Jest to bardziej aktywna forma Observable, który pozwalał na pasywne tworzenie strumieni (zbiór, event, itp.). W skrócie jest to taki event emiter z możliwością subskrybowania. Co ważne — nowe wartości są rozsyłane do wszystkich obserwatorów.
- Schedulers → pozwalają na zaplanowanie wykonania, kontrolują kiedy subskrypcja się rozpoczyna i kiedy powiadomienia są publikowane do subskrybentów. Możliwe jest wykonanie zadania w przyszłości, bez wywołania z kodu.
Możliwości RxJS — Subject i Subscription
Subject jest to specyficzny typ, który jest jednocześnie Observablem i Observerem. Czyli możemy jednocześnie emitować nowe wartości, ale ponadto możemy subskrybować ten obiekt. Zapewnia przejrzystość rozwiązania, lepszą organizację kodu i co najważniejsze – komunikację między komponentami.
Subject z Subscription można zastosować w wielu przypadkach. Dwa z nich jakie chciałbym przedstawić to powiadomienia i koszyk internetowy. Wyobraźmy sobie sytuację, w której z wielu miejsc naszej aplikacji musimy powiadomić użytkownikowi o jakieś akcji. Dodatkowo mamy kilka różnych sposobów na wyświetlenie takiego powiadomienia, w zależności od konfiguracji interfejsu. W takiej sytuacji ciężko byłoby tym sterować poprzez ręczną manipulację interfejsu, co by miało miejsce w jQuery. Stosując najnowsze trendy, w tym tytułowy RxJS, możemy taki scenariusz rozwiązać w banalny sposób, zachowując przy tym najwyższą jakość, czytelność i skuteczność. Oto przykład realizacji takiego problemu w kodzie.
export class MessageService { private subject = new Subject<MessageModel>(); sendMessage(message: string): void { const me = this; me.subject.next({ message: message, status: 200 }); } clearMessage(): void { const me = this; me.subject.next(); } getMessage(): Observable<MessageModel> { const me = this; return me.subject.asObservable(); } }
Jest to prosty serwis w Angularze, który zawiera jedynie obsługę przesyłania powiadomienia — obiekt typu MessageModel, który zawiera wiadomość i status. Posiadając taki serwis możemy go teraz użyć w wielu miejscach. Zarówno w miejscach, które będą emitować nową wiadomość (wysyłać powiadomienie), a także w miejscach, które będą obsługiwać (wyświetlać) powiadomienie. Oto przykład.
export class SubjectSubscriptionComponent { message: MessageModel; subscription: Subscription; constructor( private messageService: MessageService ) { const me = this; me.subscription = me.messageService .getMessage() .subscribe((message: MessageModel) => { me.message = message; }); } }
Na powyższym przykładzie przedstawiono odbieranie nowych powiadomień i przypisywanie ich do propercji message w komponencie. Ta propercja została podpięta pod template.
<div class="alert alert-success mt-5" *ngIf="message"> <h4 class="alert-heading">Information</h4> {{message.message}} </div>
Jak widać — wystarczyły trzy proste kroki, aby zrealizować obsługę wyświetlania powiadomienia. Ale dobrze – a co z wysłaniem? Otóż wygląda to następująco.
sendMessage(message: string): void { const me = this; me.messageService.sendMessage(message); }
Powyżej przedstawiłem metodę z komponentu, który wysyła powiadomienia. Wykonanie messageService.sendMessage spowoduje, że we wszystkich miejscach, które zasubskrybowały powiadomienia zostanie ono wyświetlone.
W drugim przykładzie — koszyk internetowy — sytuacja przedstawia się bardzo podobnie. Różnica polega na tym, że wysyłany obiekt będzie większy (nazwa produktu, właściwości). Użytkownik dodaje produkt do koszyka, komponent wysyła powiadomienie do serwisu obsługującego koszyk, że pojawił się nowy produkt, komponent koszyka odbiera wiadomość, aktualizuje koszyk i odświeża widok. Z opisu możemy wywnioskować, że odpowiedzialności komponentów są tutaj bardzo dobrze wyznaczone. Komponent produktu obsługujący dodanie produktu do koszyka wysyła jedynie wiadomość, a sama logika aktualizacji koszyka realizowana jest już w koszyku.
Przegląd najbardziej przydatnych metod/operatorów
- switchMap – świetny operator, który przełącza wartości do kolejnego typu observable. Co ważne – dany typ observable może mieć jakieś subskrypcje wewnętrzne. I tu właśnie przychodzi switchMap – dzięki niemu, wszystkie wewnętrzne aktywne strumienie zostaną anulowane. Co więcej – nawet żądania http zostaną anulowane, co można zaobserwować na zakładce network poprzez status „canceled”. Ta metoda znajduje zastosowanie np. w wyszukiwarkach lub miejscach gdzie bardzo często jest uruchamiana ta sama metoda i być może nie zostanie jej działanie dokończone do czasu nowej wartości.
- retryWhen – metoda, która pozwala na ponowienie sekwencji akcji na podstawie jakichś kryteriów, np. przy wystąpieniu błędu http.
- scan/reduce — obie metody służą do zebrania wyniku na podstawie strumienia. Metoda scan emituje każdy wynik cząstkowy, reduce jedynie wynik końcowy.
- debounceTime – odczekuje określony czas przed wyemitowaniem wartości – przydatne np. przy formularzach — auto wypełnianiu, wyszukiwarkach.
- combineLatest – pozwala połączyć kilka obiektów observable i kiedy każdy z nich wyemituje wartość combineLatest zwraca nam ostatnią z każdego observable. Tutaj zastosowanie znajdą bardziej zaawansowane procesy, które wymagają zakończenia kilku akcji przed przejściem do następnego etapu w aplikacji.
Przykłady dla wybranych operatorów
Wyszukiwarka z opóźnieniem — zastosowano tutaj połączenie debounceTime, aby ograniczyć liczbę wywołań wyszukiwania, a także switchMap, który wykonuje przeszukiwanie i dzięki użyciu switchMap, gdy wyszukiwanie nie zdąży się zakończyć do nowej emisji wartości to wtedy zostaje ono przerwane automatycznie. Co za tym idzie — w sytuacji, gdy wyszukiwanie odbywa się z wykorzystaniem API, takie żądania zostaną anulowane, a nie wykonane do końca — pozwala to ograniczyć obciążenie serwera.
Takie rozwiązanie można zastosować w dynamicznych wyszukiwarkach, dużych polach autocomplete, itp.
search(texts: Observable<string>): any { const me = this; return texts .debounceTime(300) .distinctUntilChanged() .switchMap(text => me.searchRecords(text)); }
W powyższej metodzie jako parametr przekazywany jest strumień Observable — np. input ze zdarzeniem change. W ciele metody zastosowano opisane operatory, a zadaniem metody searchRecords jest zwrócenie wyników — może to być wynik z tablicy, wynik z API — jest tutaj dowolność. Nie ma to wpływu na przykład. Taki kod powoduje, że wyszukiwanie zostanie “uruchomione” dopiero po 300 milisekundach od momentu zaprzestania wprowadzania znaków.
Scan — operator, który umożliwia akumulowanie kolejnych wartości w kolekcji Observable, emitując kolejne, np. sumy pośrednie. Po raz kolejny przykładem może być tutaj koszyk internetowy i wyświetlanie sumy zamówienia. Przykład takiego zastosowania poniżej.
getCartSumSubscription(): Subscription { const me = this; return me.subscription = me.cart$ .startWith(0) .scan((sum: number, product: Product) => sum + product.price) .subscribe(sum => { me.cartSum = sum; }); }
Powyższy przykład nieco bardziej złożony jednak podejście jest to samo. Mamy strumień koszyka, czyli produkty. Suma zaczyna się od 0 – startWith(0). Następnie sumujemy wszystkie produkty w koszyku (produkt ma w sobie cenę — price). Na końcu po obliczeniu sumy koszyka przekazujemy wynik do cartSum, które jest propercją komponentu i jest ona wyświetlana na interfejsie użytkownika.
Podsumowanie
RxJS to zbiór rozszerzeń JavaScriptu, dzięki którym wzbogacamy język o możliwości bardziej rozbudowanego programowania asynchronicznego. Dostarczane jest wiele metod, które realizują zawiłe kiedyś funkcjonalności (takich jak łączenie wielu eventów, timeout, itp.). Moim zdaniem możliwości jakie niesie ze sobą RxJS usprawniają na tyle wytwarzanie aplikacji, że coraz częściej będziemy te rozwiązania spotykać w kolejnych projektach.
Przydatne linki
Dla osób chętnych poszerzyć swoją wiedzę w zagadnieniach poruszanych w tym artykule zachęcam do odwiedzenia poniższych adresów:
- Angular doc — angular.io
- RxJS doc — rxjs-dev.firebaseapp.com/api
- Alligator — alligator.io/angular
- LearnRxJS — www.learnrxjs.io
Wszystkie zamieszczone przykłady znajdziesz w repozytorium na GitHubie pod adresem: https://github.com/mati4455/Angular5-RxJS.
Gdyby ktoś chciał zobaczyć mojego LightningTalka na temat RxJS zapraszam na YouTube.
https://youtu.be/E9E0-Xjc3VE