Общая картина, которую я пытаюсь достичь, - это набор фильтров моего заголовка страницы, которые контролируют входные параметры для нескольких страниц аналитики в моем приложении. Я инкапсулировал функциональность фильтров в службу Angular, которая предоставляет наблюдаемый объект, который генерируется, когда происходят изменения в фильтрах.
Я хочу, чтобы службы, которые используют эти значения фильтра в запросах HttpClient, подписывались на изменения в фильтрах и повторно запускали свои запросы HttpClient при изменении фильтров (например, если диапазон дат изменяется, любые элементы на моей странице, которые управляются к этому диапазону дат автоматически обновляются).
Типичная служба данных в моем приложении будет выглядеть так, как показано ниже. Кажется, то, что я пытаюсь сделать, должно быть достаточно простым, но я изо всех сил пытаюсь понять библиотеку RxJS настолько, чтобы комбинировать наблюдаемые так, как я стремлюсь.
export class DashboardDataService {
constructor(
private readonly http: HttpClient,
private readonly globalFiltersService: GlobalFiltersService
) { }
public getDashboard(): Observable<DashboardDto> {
const filtersSubscription = globalFiltersService.filters$.subscribe(...);
const observable = this.http.get<DashboardDto>(`${environment.apiBase}network/dashboard`, {
params: this.globalFiltersService.getHttpParams()
});
// TODO: when filtersSubscription receives new data, make observable re-run it's HTTP request and emit a new response
return observable; // Make this observable emit new data
}
}
Я использую Angular 8 и RxJS 6, поэтому предпочтительнее использовать самый современный способ.
ОБНОВЛЕНИЕ: Рабочая реализация
export class GlobalFiltersService {
private readonly _httpParams$: BehaviorSubject<{ [param: string]: string | string[]; }>;
private start: Moment;
private end: Moment;
constructor() {
this._httpParams$ = new BehaviorSubject(this.getHttpParams());
}
public setDateFilter(start: Moment, end: Moment) {
this.start = start;
this.end = end;
this._httpParams$.next(this.getHttpParams());
}
public get httpParams$() {
return this._httpParams$.asObservable();
}
public getHttpParams() {
return {
start: this.start.toISOString(),
end: this.end.toISOString()
};
}
}
export class DashboardDataService {
private _dashboard$: Observable<DashboardDto>;
constructor(
private readonly http: HttpClient,
private readonly globalFiltersService: GlobalFiltersService
) { }
public getDashboard(): Observable<DashboardDto> {
if (!this._dashboard$) {
// Update the dashboard observable whenever global filters are changed
this._dashboard$ = this.globalFiltersService.httpParams$.pipe(
distinctUntilChanged(isEqual), // Lodash deep comparison. Only replay when filters actually change.
switchMap(params => this.http.get<DashboardDto>(`${environment.apiBase}network/dashboard`, { params })),
shareReplay(1),
take(1)
);
}
return this._dashboard$;
}
}
export class DashboardResolver implements Resolve<DashboardDto> {
constructor(private readonly dashboardDataService: DashboardDataService, private readonly router: Router) {}
public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<DashboardDto> {
return this.dashboardDataService.getDashboard();
}
}
Попробуйте следующее:
import {map, switchMap, shareReplay } from 'rxjs/operators';
export class FooComponent {
readonly dashboard$: Observable<DashboardDto>;
ctor(...){
this.dashboard$ = this.globalFiltersService.filters$.pipe(
// map filter event to the result of invoking `GlobalFiltersService#getParams`
map(_ => this.globalFiltersService.getHttpParams()),
// maps the params to a new "inner observable" and flatten the result.
// `switchMap` will cancel the "inner observable" whenever a new event is
// emitted by the "source observable"
switchMap(params => this.http.get<DashboardDto>(`${environment.apiBase}network/dashboard`, { params })),
// avoid retrigering the HTTP request whenever a new subscriber is registered
// by sharing the last value of this stream
shareReplay(1)
);
}
}
Обновление: теперь я хочу обновить свою панель инструментов, когда любой из двух других наблюдаемых выдает новые значения. Не понял, как расширить это решение, чтобы справиться с этим. Мы ценим любые предложения.
Хороший вопрос! Мне пришлось синхронизировать параметры URL, формы и результаты запросов. Это привело меня в кроличью нору архитектуры вплоть до управления государством.
TL;DR Когда есть много элементов, зависящих от самых последних данных, вам нужно иметь высокодоступное состояние для этих данных. Решение больше зависит от архитектуры, чем от того, какой метод RXJS использовать.
Вот сервис, который я построил в качестве примера stackblitz.com/edit/state-with-simple-service.
Вот были мои требования. (напрямую применимо к вопросу)
Вот суть:
export class SearchService {
// 1. results object from the endpoint called with the current options
private _currentResults = new BehaviorSubject<any[]>([]);
// 2. current state of URL parameters and Form values
private _currentOptions = new BehaviorSubject<Params>({});
// 3. private options object to manipulate
private _options: Params = {};
Затем получите доступ к ним с помощью геттеров:
// Any component can subscribe to the current results from options query
public get results(): Observable<any[]> {
return this._currentResults.asObservable();
}
// Any component can subscribe to the current options
public get options(): Observable<Params> {
return this._currentOptions.asObservable();
}
Обновляйте личные темы в любое время с помощью next()
this._currentOptions.next(this._options);
Теперь у вас есть управление состоянием без внедрения огромного фреймворка вроде redux.
Спасибо, Бен, на самом деле это было довольно близко к тому, что я уже реализовал в созданном мною сервисе GlobalFiltersService. Я действительно хотел избежать избыточности - это довольно небольшое требование в более крупном приложении, и у меня сложилось впечатление, что избыточность в последнее время, как правило, используется во многих угловых приложениях, которым это действительно не нужно (по крайней мере, это то, что я читал). Простые наблюдаемые предоставляют некоторые довольно крутые возможности управления состоянием - мне просто по какой-то причине трудно понять библиотеку.
О, спасибо большое - это было именно то, что мне было нужно. Я немного подправил решение из этого (через мгновение опубликую его в вопросе), но это привело меня туда, куда мне нужно было идти. В итоге мне не понадобился первый оператор map(), потому что filter$ сам по себе возвращает результат getHttpParams(), и мне пришлось добавить оператор take(1) после общего повтора, чтобы заставить его играть в мяч (я не полностью понятно почему) но работает отлично.