#angular #caching #rxjs #observable #subject
Вопрос:
В моем сервисе angular у меня есть кэшированный http-запрос, который вызывается один раз при инициализации моего компонента
//variables
private readonly URL: string = 'http://makeup-api.herokuapp.com/api/v1/products.json';
private cachingSubject: ReplaySubject<IProduct[]>
private _products: IProduct[] = [];
//method
public getAllProducts(): Observable<IProduct[]> {
if (!this.cachingSubject) {
this.cachingSubject = new ReplaySubject<IProduct[]>(1);
this.http.get<IProduct[]>(this.URL)
.subscribe((productsData: IProduct[]) => {
this._products = productsData;
this.cachingSubject.next(productsData);
});
}
return this.cachingSubject.asObservable();
}
и мой компонент, когда создает метод обслуживания:
public products: IProduct[] = [];
private destroySubject$: Subject<void> = new Subject<void>();
ngOnInit(): void {
this.productService
.getAllProducts()
.pipe(takeUntil(this.destroySubject$))
.subscribe((products: IProduct[]) => {
this.products = products;
})
}
public onForceReload(): void {
//need to reload onClick
}
Вопрос в том, как я могу принудительно перезагрузить (повторно запустить кэш) из моего кэшированного запроса ? Сделайте GetAllProducts снова
Ответ №1:
Я думаю, что самый простой способ достичь того, что вы ищете, — это использовать один объект в качестве триггера для извлечения данных. Затем определите allProducts$
наблюдаемое в службе (вместо метода), которое зависит от этого «объекта выборки».
Предоставьте простой refreshProducts()
метод, который вызывает .next()
объект выборки, что приведет allProduct$
к повторной выборке данных.
Потребители могут просто подписаться на allProducts$
самые свежие продукты и получать их. Они могут позвонить refreshProducts()
, чтобы перезагрузить данные. Не будет необходимости переназначать ссылки на объекты или объекты наблюдения.
export class ProductService {
private fetch$ = new BehaviorSubject<void>(undefined);
public allProducts$: Observable<IProduct[]> = this.fetch$.pipe(
exhaustMap(() => this.http.get<IProduct[]>('url')),
shareReplay(),
);
public refreshProducts() {
this.fetch$.next();
}
}
Вот рабочая демонстрация StackBlitz.
- Начинается с
fetch$
, что означает, что он будет выполняться всякийfetch$
раз, когда испускает exhaustMap
подпишется на «внутреннюю наблюдаемую» и будет испускать ее выбросы. В данном случае эта внутренняя наблюдаемая функция является вызовом http.shareReplay
используется для отправки предыдущего сообщения новым подписчикам, поэтому http-вызов не выполняется доrefreshProducts()
тех пор, пока не будет вызван (это не требуется, если у вас будет только 1 подписчик одновременно, но, как правило, в службах рекомендуется его использовать)
Причина, по которой мы определяем fetch$
как a BehaviorSubject
, заключается в том, что allProducts$
не будет излучать, пока fetch$
не будет излучаться. A BehaviorSubject
изначально выдаст значение по умолчанию, поэтому оно приведет allProducts$
к выполнению our без необходимости нажатия пользователем кнопки перезагрузки.
Обратите внимание, что в сервисе отсутствует подписка. Это хорошо, потому что это позволяет нашим данным быть ленивыми, а это означает, что мы не просто получаем данные только потому, что какой-то компонент внедрил услугу, мы получаем их только тогда, когда есть подписчик.
Кроме того, это означает, что наш сервис имеет однонаправленный поток данных, что значительно облегчает отладку. Потребители получают данные только путем подписки на общедоступные наблюдаемые объекты, и они изменяют данные, вызывая методы в службе… но эти методы НЕ возвращают данные, а только заставляют их проходить через наблюдаемые объекты. На эту тему есть действительно хорошее видео с участием Томаса Берлесона (бывшего члена команды Angular).
Вы видите, что я использовал имя allProducts
для открытого наблюдаемого вместо getAllProducts
. Поскольку allProducts$
это наблюдаемо, акт подписки подразумевает «получение».
Мне нравится думать о наблюдаемых как о небольших источниках данных, которые всегда будут передавать последние значения. Когда вы подписываетесь, вы прислушиваетесь к будущим ценностям, но потребитель их не «получает».
Я знаю, что это было много, но у меня есть еще один маленький совет, который, по-моему, немного очищает код.
Это и есть использование AsyncPipe
.
Таким образом, код вашего компонента может быть упрощен до этого:
export class AppComponent {
public products$: Observable<IProduct[]> = this.productService.allProducts$;
constructor(private productService: ProductService) { }
public onForceReload(): void {
this.productService.refreshProducts();
}
}
И ваш шаблон:
<ul>
<li *ngFor="let product of products$ | async">{{ product }}</li>
</ul>
Обратите внимание, что здесь нет необходимости подписываться в шаблоне только для того, чтобы сделать this.products = products;
. Асинхронный канал подписывается за вас и, что более важно, отписывается за вас. Это означает, что вам больше не нужна тема уничтожения!
Вот вилка StackBlitz предыдущей версии, обновленной для использования асинхронного канала.
Ответ №2:
Вы можете добиться этого следующим образом:
- Создайте объект наблюдения
cachedRequest$
, чтобы назначить ему HTTP-запрос иpipe
его сshareReplay
помощью, который будет кэшировать ответ и возвращать его каждый раз, когда вы подписываетесь на объект наблюдения. - Верните значение
cachedRequest$
if или назначьте HTTP-запрос (сshareReplay
), если нет. - Передайте
forceReload
параметр своей функции для принудительной перезагрузки, и если это правда, просто установите значениеcachedRequest$
null, чтобы снова инициализироваться.
Попробуйте выполнить следующие действия:
cachedRequest$: Observable<IProduct[]>;
public getAllProducts(forceReload = false): Observable<IProduct[]> {
if (forceReload) this.cachedRequest$ = null;
if (!this.cachedRequest$) {
this.cachedRequest$ = this.http.get<IProduct[]>(this.URL).pipe(
tap(productsData => (this._products = productsData)),
shareReplay(1)
);
}
return this.cachedRequest$;
}
Затем в вашем компоненте лучше обработать запрос другим способом, чтобы избежать многократной подписки на наблюдаемый источник, поэтому вы можете сделать что-то вроде следующего:
products$: Observable<IProduct[]>;
ngOnInit(): void {
this.loadProducts();
}
public onForceReload(): void {
//need to reload onClick
this.loadProducts(true);
}
private loadProducts(forceReload = false) {
this.products$ = this.productService.getAllProducts(forceReload);
}
Затем в вашем шаблоне компонента используйте его следующим образом:
<ng-container *ngIf="products$ | async as products">
<!-- YOUR CONTENT HERE -->
</ng-container>
Комментарии:
1. Можно ли снова подписаться в onForceReloadMethod ? Не будет ли это утечкой памяти или чем-то в этом роде ?
2. Это зависит от того, как вы хотите его использовать. вы можете использовать его с
take(1)
оператором каждый раз, когда захотите подписаться на него, поэтому он будет завершен сразу после получения прямого результата. Или вы можете назначить его наблюдаемому и использовать его в шаблоне сasync
каналом. Поэтому вы должны позаботиться об этой части в соответствии с вашими требованиями.3. У меня будут флажки, и когда флажок установлен, я хочу перезагрузить. Какая стратегия подходит для этого требования ?
4. Я обновил свой ответ предпочтительным решением. Надеюсь, вы найдете это полезным.