Dynamicznie dodaj operator do Rxjs obserwowalnego w aplikacji Angular
W naszej aplikacji Angular mamy komponent ładujący, który otrzymuje obserwowalny poprzez właściwość ustawiającą dane wejściowe.
W programie ustawiającym najpierw ustawiamy wartość logiczną isLoading na true, która wewnątrz szablonu zaczyna wyświetlać spinner ładujący. Następnie zapisujemy się na obserwowalne, a po otrzymaniu danych wartość logiczna isLoading jest ponownie ustawiana na false, co powoduje zniknięcie pokrętła:
// loading-component template:
<ng-container *ngIf="data; else errorOrLoading">
...
</ng-container>
<ng-template #errorOrLoading>
...
</ng-template>
// loading-component class (short version without unsubscribe):
@Input() set data$(data$: Observable<any>) {
if (data$) { this.data = null; data$
.subscribe(data => {
this.data = data;
});
}
}
Działa to świetnie, jeśli mamy tylko jedno zdarzenie z Observable. Ale gdy Observable emituje wiele zdarzeń, isLoading nie zostanie ponownie ustawiona na wartość true, ponieważ metoda ustawiająca nie jest wywoływana.
Czy istnieje sposób na dynamiczne dodanie dodatkowego operatora tap, który pozwoli ustawić this.data = null, zanim dany łańcuch Observable zacznie emitować nowe zdarzenie?
Więc jeśli na przykład Observable to:
myService.onChanges$.pipe(
switchMap(() => this.getBackendData())
);
Czy możemy dynamicznie dodać operator zaczepu, który zmieni potok na:
myService.onChanges$.pipe(
tap(() => this.data = null),
switchMap(_ => this.getBackendData())
);
Aktualizacja: Postanowiłem uprościć sterowanie programem ładującym i przenieść całą obserwowalną logikę powiązaną do usług, co wydaje się znacznie bardziej skalowalne i elastyczne.
Odpowiedzi
Aktualizacja nr 1
Rozwiązanie nr 1. Posługiwać sięshareReplay()
Zgodnie z odpowiedzią @Reactgular
Wszystko, co musisz zrobić, to użyć operatora shareReplay () :
class MyService {
public data$: Observable<any>; public loaded$: Observable<boolean>;
constructor(private dataService: DataService) {
this.data$ = this.dataService.loadData().pipe( startWith(null), // data$ emit a null before data is ready followed by the API data.
shareReplay(1);
);
this.loaded$ = this.data$.pipe(
mapTo(true),
startWith(false)
);
}
}
Musisz wywołać myService.data $ .subscribe (), aby wywołać pierwszy odczyt strumienia i przygotować dane. Możesz to zrobić w konstruktorze, ale pamiętaj, że Angular nie tworzy usługi, dopóki nie zostanie po raz pierwszy użyta. Jeśli chcesz, aby dane były chętnie ładowane, użyj resolvera w trasie lub wstrzyknij usługę do konstruktora NgModule i tam zasubskrybuj.
Rozwiązanie nr 2. Skorzystaj z dedykowanej usługi
Lepszym rozwiązaniem może być wprowadzenie narzędzia, LoaderServicektóre będzie obsługiwać ładowanie komponentu / przeglądanie danych.
W zależności od potrzeb projektu może być pojedyncza lub współdzielona .
Załóżmy, że nasza usługa obsłuży stan ładowania tylko dla bieżącego widoku (usługa udostępniona)
Usługa ładowarki:
export class LoaderService {
private readonly _loading: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false)
// returns value of `_loading` on the moment of accessing `isLoading`
get isLoading(): boolean {
return this._loading.getValue();
}
// returns value of `_loading` as Observable
get isLoading$(): Observable<boolean> {
return this._loading.asObservable();
}
// pushes `true` as a new value of `_loading` subject and notifies subscribers
start(): void {
this._loading.next(true);
}
// pushes `true` as a new value of `_loading` subject and notifies subscribers
stop(): void {
this._loading.next(false);
}
}
Zakładając, że mamy dwie usługi:
- API - zawiera tylko deklaracje metod, które zwracają czyste (niezmodyfikowane) strumienie http
- ComponentService - usługa przygotowująca dane przed przekazaniem ich do komponentu reprezentacyjnego
MyFruitService
constructor(
...,
private api: MyFruitAPIService,
private loader: LoaderService
) { ... }
getApples(): Observable<T> {
this.loader.start();
return this.api.getApples()
.pipe(
finalize(() => this.loader.stop()) // finalize - will call a function when observable completes or errors
);
}
MyAppleFruitComponent
readonly loading$ = this.loader.isLoading$.pipe(tap((loading) => {
if (loading) {
this.data = null;
}
}));
constructor(private loader: LoaderService) { ... }
<ng-container *ngIf="loading$ | async; else errorOrLoading">
...
</ng-container>
Krótko mówiąc, nie. To nie jest możliwe. Prawdopodobnie też nie jest to dobry pomysł, ponieważ powoduje to zerwanie enkapsulacji.
Nie możesz też dynamicznie zmieniać wywołania funkcji. Jeśli doADance()tańczysz dla Ciebie, nie możesz tak naprawdę dynamicznie zmusić go do dodania również listy numerów. Implementacja funkcji powinna pozostać oddzielona od jej wywołania. Chociaż do twojego punktu, Javascript faktycznie sprawia, że ludzie wykonują dziwne rzeczy dynamicznie, wiążąc różne konteksty i tym podobne.
RxJS oddziela implementację od wywołania (subskrypcji) również dla strumieni. Jeśli biblioteka wykonuje 20 transformacji i zwraca ci ten strumień, tak naprawdę nie dostajesz listy transformacji, to tylko szczegóły implementacji, które autorzy biblioteki mogą zmienić bez wprowadzania istotnych zmian.
Aktualizacja 1:
To prawda, że hermetyzacja jest ważna i nie bez powodu. Oczywiście możemy jednak dynamicznie dodawać listę liczb do funkcji doADance (), przekazując listę jako parametr. Może kontrolowany sposób pozwalający na wypełnianie symboli zastępczych wewnątrz potoku dynamicznie podawanymi operatorami byłby taki sam?
Chociaż symbole zastępcze w tym pipenaprawdę nie mają sensu, ponieważ każdy operator potokowy można łatwo przekształcić w operator statyczny, a każdy zestaw operatorów można łatwo przekształcić w jednego operatora.
To, co możesz zrobić, to coś bardzo bliskiego. Nie działa to na przykład w przypadku strumienia zwracanego z biblioteki, ale można zaprojektować strumienie, aby umożliwić dostosowanie sposobu ich przetwarzania.
W Twojej służbie:
function dataOnChange(): Observable<Data>{
return myService.onChanges$.pipe( switchMap(() => this.getBackendData()) ); } function dataOnChangePlus(op): Observable<Data>{ if(op == null) return dataOnChange(); return myService.onChanges$.pipe(
op,
switchMap(() => this.getBackendData())
);
}
gdzie indziej:
this.service.dataOnChangePlus(
tap(_ => this.data = null)
).subscribe(console.log);
gdzie indziej, robiąc to samo, ale trochę inaczej:
this.service.dataOnChangePlus(
st => st.pipe(
mapTo(null),
tap(val => this.data = val)
)
).subscribe(console.log);
Teraz konsument dataOnChangePlus otrzymuje strumień i może również pomóc w zdefiniowaniu, w jaki sposób ten strumień jest konstruowany. Nie jest to dynamiczne dodawanie operatora, ale pozwala na odroczenie definicji operatora.
Zaletą jest to, że każde wezwanie może definiować coś innego.
Jeśli chcesz, możesz zawęzić zakres możliwości dzwoniącego, dając mu dostęp tylko do określonego typu operatora. Na przykład pozwól im tylko zdefiniować lambdę dla operatora tap:
function dataOnChangePlusTap(tapper): Observable<Data>{
return myService.onChanges$.pipe(
tap(tapper),
switchMap(() => this.getBackendData())
);
}
this.service.dataOnChangePlusTap(
_ => this.data = null
).subscribe(console.log);