Monolit-IT - Blog - szczegóły

Baza wiedzy

Synchroniczne wywołanie XHR z Anglular 2.x z użyciem rxjs

13.03.2018

Wyobraźmy sobie sytuację, gdy musimy wysyłać żądania do backendu w sposób synchroniczny, tj. wysyłamy żądanie nr 1 i dopiero po nadejściu odpowiedzi z serwera wysyłamy kolejne żądanie nr 2. Nie znalazłem żadnej opcji w Angularze lub rxjs, która by w prosty sposób realizowała taki scenariusz. Trzeba więc zbudować takie rozwiązanie samodzielnie.

Zanim zacznę przedstawiać poszczególne etapy wraz z fragmentami kodu, od razu wspomnę o potrzebnych importach:

import 'rxjs/add/operator/mergeMap';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';
import 'rxjs/add/observable/of';

Brak tych importów nie będzie przeszkadzał podczas kompilacji, dopiero uruchomienie kodu będzie skutkować błędami typu "nieznana metoda X".

Na początek spróbujemy sprawdzić możliwości biblioteki rxjs. Jak wiemy z poprzedniego wpisu biblioteka ta ma wiele przydatnych klas, z których jedna przyda nam się do rozwiązania naszego problemu. To klasaSubject, która dostarcza przede wszystkim metodęnext, za pomocą której będziemy wysyłać poszczególne żądania.

Spróbujmy napisać kawałek kodu z metodą, która ma za zadanie wysyłać żądania GET w sposób opisany na początku. Metoda powinna na liście parametrów url żądania i zwracać obiektObservable, który można "zasubskrybować":

private syncSubject = new Subject<any>();

constructor(private http: HttpClient) {
this.syncSubject.subscribe(url => {
http.get(url);
});
}

public sendGetSync(url: string): Observable {
this.syncSubject.next(url);
}

Widzimy tworzenie prywatnego pola oraz jego subskrypcję w konstruktorze, w którym będziemy wysyłać kolejno pojawiające się URL-e. Widzimy też metodęsendGetSync, która wywołujenext. Jak na razie ten kod się przede wszystkim nie kompiluje, bo metoda nie zwraca wartości. Dodatkowo, gdybyśmy tam jednak wstawili zwracanie np. null-a, to i tak to rozwiązanie nie spełnia naszego wymagania – przede wszystkim kolejne wywołania będą wywoływane natychmiastowo, ponieważ funkcja będąca argumentem metodysubscribejest wywoływana jak najbardziej asynchronicznie – nie jest to w żaden sposób skolejkowane. Rozwiążmy najpierw ten drugi problem.

Użyjmy metodymergeMap. Przyjmuje ona 3 parametry, z czego interesują nas tylko pierwszy i trzeci. Pierwszy to "projekcja" czyli funkcja, która zamienia wchodzącą wartość na obiektObservable(dokładnie rzecz biorąc spodziewa się typu ogólniejszego:ObservableInput). Trzeci parametr to liczba jednoczesnych przetworzeń – tutaj damy wartość 1, co spowoduje kolejkowanie naszych żądań.

constructor(private http: HttpClient) {
this.syncSubject.mergeMap(url => {
return http.get(url);
}, null, 1).subscribe();
}

Zwróć uwagę, że metodasubscribepozostała pusta, a faktyczne wywołanie żądania zostało przeniesione domergeMap, co dodatkowo jest dla nas korzystne, bo wywołaniehttp.getzwracaObservable, czyli wymagany typ zwracanej wartości funkcji z pierwszego parametru.

Pozostaje nam jeszcze problem wartości zwracanej przez metodęsendGetSync. Chcielibyśmy w odpowiedzi otrzymać obiektObservable, który zawiera odpowiedź z rzeczywistego wywołania http.get. Nie mamy do tego dostępu, bo wywołanienextnie zwraca żadnej wartości.

Utwórzmy pomocniczy interfejs:

interface GetRequestData {
url: string;
resultSubject: Subject<any>;
}

Przeróbmy definicję naszego pola:

private syncSubject = new Subject<GetRequestData>();

Przeróbmy metodęsendGetSync:

public sendGetSync(url: string): Observable<any> {
const resultSubject = new Subject<any>();
this.syncSubject.next({url, resultSubject});
return resultSubject;
}

Widzimy tutaj, że tworzymy nowySubject, następnie konstruujemy obiekt typuGetRequestData, do którego przekazujemy URL-a oraz stworzony przed chwiląSubject. I dopiero tak stworzony obiekt podajemy do wywołania metodynext.

Przeróbmy ostatecznie wywołaniemergeMap:

constructor(private http: HttpClient) {
this.syncSubject.mergeMap(getRequestData => {
return http.get(getRequestData.url).do(result => {
getRequestData.resultSubject.next(result)
});
}, null, 1).subscribe();
}

Zmiana przede wszystkim polega na tym, że wynik wywołania http.getużywamy jako parametr do wywołania metodynextna obiekcieSubject, który stworzyliśmy w metodziesendGetSync. Wywołanie to wykonujemy w ramach wywołania metodydo– dlaczego? Metodadodziała trochę jak peek w kolejkach, czyli sprawdzamy wartość w kolejce, ale jej nie wyjmujemy. "Wyjęcie wartości z kolejki" następuje w widocznym na końcu wywołaniusubscribe.

Voilà – to powinno już działać. Dla porządku powinniśmy jeszcze uwzględnić obsługę błędów, metoda doma taką możliwość:

constructor(private http: HttpClient) {
this.syncSubject.mergeMap(getRequestData => {
return http.get(getRequestData.url).do(
result => getRequestData.resultSubject.next(result),
error => getRequestData.resultSubject.error(error)
).catch(err => Observable.of(null));
}, null, 1).subscribe();
}

Wywołanie catchma zapobiec sytuacji, w której błąd powstający w wywołaniuhttp.getmógłby spowodować "wyłożenie się" naszego obiektu syncSubject. A w konsekwencji nasz mechanizm synchronizacyjny przestałby po prostu działać.

Przedstawiony w tym materiale problem z wysyłaniem żądania do serwera można rzecz jasna rozszerzyć o podobne sytuacje, w których mamy do czynienia z asynchronicznym kodem.

Zainteresował Cię ten wpis?
Chcesz dowiedzieć się więcej?

Michał Gierwatowski

Michał Gierwatowski

Programista wszechstronny, od języka Progress4GL począwszy, przez Javę, na TypeScripcie kończąc. Ponad piętnastoletnie doświadczenie w wytwarzaniu różnego rodzaju systemów informatycznych. Ostatnio interesuje się nowinkami w ekosystemie JavaScript/node.js

Michał.Gierwatowski(at)monolit-it.pl

Zobacz wszystkie artykuły danego autora »