Baza wiedzy
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 metodysubscribe
jest 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 metodasubscribe
pozostała pusta, a faktyczne wywołanie żądania zostało przeniesione domergeMap
, co dodatkowo jest dla nas korzystne, bo wywołaniehttp.get
zwracaObservable
, 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łanienext
nie 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.get
używamy jako parametr do wywołania metodynext
na obiekcieSubject
, który stworzyliśmy w metodziesendGetSync
. Wywołanie to wykonujemy w ramach wywołania metodydo
– dlaczego? Metodado
dział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 do
ma 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 catch
ma zapobiec sytuacji, w której błąd powstający w wywołaniuhttp.get
mó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
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 »