Как поделиться HttpClient GET с BehaviorSubject?

Итак, я использовал shareReplay (1) для кеширования массива элементов в памяти, которые возвращаются из вызова HttpClient GET.

if (!this.getItems$) {
    this.getItems$ = this.httpClient
            .get<Item[]>(url, options)
            .pipe(shareReplay(1));
}
return this.getItems$;

Я делаю это, потому что у меня на странице много компонентов, которым нужен массив Items, и я не хочу делать http-вызов для каждого из них.

Я также создаю элемент на странице. Итак, после того, как я вызываю службу для вызова API, который создает элемент в базе данных и возвращает его, я хотел бы добавить его в массив элементов в памяти и предупредить все компоненты, которые подписываются на обновленный массив. . Итак, я попробовал это:

private items = new BehaviorSubject<Item[]>(null);

...

if (!this.getItems$) {
this.getItems$ = this.httpClient
    .get<Item[]>(url, options)
    .pipe(
      tap({
        next: (items) => {
          this.items.next(items);
        },
      })
    )
    .pipe(multicast(() => this.items));
}
return this.getItems$;

Затем в методе, вызывающем API:

return this.httpClient.post<Item>(url, item, options).pipe(
      tap({
        next: (item) => {
          this.items.next(this.items.value.push(item));
        },
      })
    );

Проблема в том, что все, что подписывается на метод getItems, всегда возвращает null. В базе данных есть элементы, поэтому даже при первом вызове они должны быть возвращены. Есть, как я тестировал с помощью shareReplay (1), и он работает.

Как я могу поделиться с помощью BehaviorSubject вместо ReplaySubject?


person ScubaSteve    schedule 12.05.2021    source источник
comment
все, что подписывается на метод getItems, всегда возвращает null - сначала подписчики получают null, затем результаты от httpClient.get() или только null?   -  person BizzyBob    schedule 12.05.2021
comment
Только null. В массиве элементов никогда не бывает элементов.   -  person ScubaSteve    schedule 12.05.2021
comment
Судя по всему, API не вызывается с помощью multicast (). Глядя на документацию, мне нужно каким-то образом вызвать connect (), чтобы подписать BehaviorSubject на источник, то есть на get, что приведет к вызову API.   -  person ScubaSteve    schedule 12.05.2021


Ответы (2)


Для этого кода:

if (!this.getItems$) {                 
    this.getItems$ = this.httpClient 
            .get<Item[]>(url, options)
            .pipe(shareReplay(1));
}
return this.getItems$;

Это: this.httpClient.get() асинхронно. Таким образом, если getItems$ еще не установлен, он еще не будет установлен оператором return.

Код выполняется в следующем порядке:

  • Строка 1.
  • Строки 2 и 3
  • Строка 5. (возвращается null)
  • Строка 4. (фактически обработка возвращенного ответа)

Кроме того, добавление if избыточно с shareReplay, которое автоматически получит данные, только если Observable еще не установлен.

Попробуйте что-то вроде этого:

getItems$ = this.httpClient
            .get<Item[]>(url, options)
            .pipe(shareReplay(1));

РЕДАКТИРОВАТЬ: если приведенный выше код является свойством, а не методом.

Что касается второй части вопроса, касающейся создания новых элементов, вы можете сделать что-то вроде этого, что взято из одного из моих примеров. Вы можете переименовать свойства / интерфейсы в соответствии с вашим сценарием.

  // Action Stream
  private productInsertedSubject = new Subject<Product>();
  productInsertedAction$ = this.productInsertedSubject.asObservable();

  // Merge the streams
  productsWithAdd$ = merge(
    this.productsWithCategory$,  // would be your getItems$
    this.productInsertedAction$
  )
    .pipe(
      scan((acc: Product[], value: Product) => [...acc, value]),
      catchError(err => {
        console.error(err);
        return throwError(err);
      })
    );

Вы можете найти полный пример с приведенным выше кодом здесь: https://github.com/DeborahK/Angular-RxJS/tree/master/APM-Final

РЕДАКТИРОВАТЬ:

Для справки, вот ключевой код из stackblitz:

Сервис

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { merge, Observable, Subject, throwError } from 'rxjs';
import { catchError, scan, shareReplay, tap } from 'rxjs/operators';
import { Item } from './item.model';

@Injectable({
  providedIn: 'root'
})
export class ItemsService {
  url = 'https://jsonplaceholder.typicode.com/users';

  // This is a property
  // It executes the http request when subscribed to the first time
  // And emits the returned array of items.
  // All other times, it replays (and emits) the items due to the shareReplay.
  getItems$ = this.httpClient.get<Item[]>(this.url).pipe(
    tap(x => console.log("I'm only getting the data once", JSON.stringify(x))),
    tap(x => {
      // You can write any other code here, inside the tap
      // to perform any other operations
    }),
    shareReplay(1)
  );
  
    // Action Stream
  private itemInsertedSubject = new Subject<Item>();
  productInsertedAction$ = this.itemInsertedSubject.asObservable();

  // Merge the action stream that emits every time an item is added
  // with the data stream
  allItems$ = merge(
    this.getItems$,
    this.productInsertedAction$
  )
    .pipe(
      scan((acc: Item[], value: Item) => [...acc, value]),
      catchError(err => {
        console.error(err);
        return throwError(err);
      })
    );

  constructor(private httpClient: HttpClient) {}

  addItem(item) {
    this.itemInsertedSubject.next(item);
  }
}

Компонент

import { Component, OnInit } from '@angular/core';
import { Item } from './item.model';
import { ItemsService } from './items.service';

@Component({
  selector: 'app-item',
  templateUrl: './item.component.html'
})
export class ItemComponent {

  // Recommended technique using the async pipe in the template
  // With this technique, no ngOnInit is required.
  items$ = this.itemService.allItems$;

  constructor(private itemService: ItemsService) {}

  addItem() {
    this.itemService.addItem({id: 42, name: 'Deborah Kurata'})
  }
}

Шаблон

<h1>My Component</h1>
<button (click)="addItem()">Add Item</button>
<div *ngFor="let item of items$ | async">
  <div>{{item.name}}</div>
</div>
person DeborahK    schedule 12.05.2021
comment
Я пробовал не использовать if, и он звонил на каждого абонента. this.getItems$ = this.httpClient.get<Item[]>(url, options).pipe(shareReplay(1)); return this.getItems$; - person ScubaSteve; 12.05.2021
comment
Можете ли вы создать маленький стек, демонстрирующий вашу технику и вашу проблему (а не все ваше приложение). Это не должно делать звонков на одного абонента. И обратите внимание, что оператор return не вернет результат, потому что .get является асинхронным. - person DeborahK; 12.05.2021
comment
Да, я возвращаю Observable ‹Item []›. Get не должен срабатывать, пока не подпишется первый подписчик. Попробую получить проект Stackblitz. - person ScubaSteve; 12.05.2021
comment
Большое спасибо! Как свойство, он не сработает, пока на свойство не будет подписана подписка. У меня есть полный пример в упомянутом выше репозитории GitHub. (У меня есть несколько видеороликов на YouTube и курс Pluralsight по этим темам, если вам интересны ссылки.) - person DeborahK; 12.05.2021
comment
Чтобы убедиться, какое поведение вы хотите от sharereplay, я бы использовал новый синтаксис sharereplay ({refcount: false, buffercount: 1}) refcount = false сохранит результаты, даже если подписок не осталось. Зависит от того, какое поведение вы хотите от курса - person rekna; 12.05.2021
comment
Вот пример проекта. stackblitz.com/edit/angular-sharereplay- пример? file = src / app / Я указываю на тестовый API, который возвращает пользователей вместо элементов, но выполняет свою работу. Если вы посмотрите на вкладку Network в Dev Tools, вы увидите, что она выполняет 3 вызова, по одному для каждого ItemComponent. - person ScubaSteve; 12.05.2021
comment
Я внес изменение, описанное выше, и оно работает должным образом, получая данные только один раз. Обновленный stackblitz здесь: stackblitz.com/edit/angular-sharereplay-example-g77pay - person DeborahK; 12.05.2021
comment
Вот видео, в котором рассказывается об этой технике: youtube.com/watch?v=Z76QlSpYcck - person DeborahK; 12.05.2021
comment
Хорошо, я вижу, что он звонит не несколько раз, а два вопроса. Во-первых, как передать параметры этому вызову и / или добавить логику внутри? Логический пример: мне нужно показывать или не показывать счетчик загрузки в зависимости от флага, отправленного методу, и его нужно добавить к параметрам. Во-вторых, я пробовал ваш пример слияния. (Я обновил свой пример Stackblitz.) Но он не предупреждает подписчиков о том, что он изменился. - person ScubaSteve; 12.05.2021
comment
Для слияния см. Мой обновленный stackblitz: stackblitz.com/edit/angular-sharereplay-example -g77pay Я также добавил комментарий, показывающий, где вы должны добавить любой другой код / ​​логику, которые вам нужно добавить. Чтобы эти потоки работали так, как вы хотите, они должны быть активными как свойства, а не выполняться внутри метода. - person DeborahK; 12.05.2021
comment
Как бы вы использовали параметры с вызовами GET? Единственный способ, который я могу придумать, - это вызвать метод, который установит свойства или сделает свойства общедоступными, которые, как предполагается, вызов GET были установлены до подписки. - person ScubaSteve; 13.05.2021
comment
Не могли бы вы создать это как новый вопрос, так как меня просят избегать длительных обсуждений в комментариях. Тогда я могу ответить на него как ответ, а не как комментарий. Спасибо! - person DeborahK; 13.05.2021
comment
Конечно, я создал его здесь: stackoverflow.com/q/67512084/787958 - person ScubaSteve; 13.05.2021

Чтобы ответить на свой вопрос, я смог найти решение.

Сначала мне нужно было получить последнюю версию rxjs:

npm install rxjs@latest

Затем я создал класс shareBehavior:

export function shareBehavior<T>(
  behaviorSubject: BehaviorSubject<T>
): MonoTypeOperatorFunction<T> {
  return share<T>({
    connector: () => behaviorSubject,
    resetOnError: true,
    resetOnComplete: false,
    resetOnRefCountZero: false,
  });
}

Затем в сервисе:

private items = new BehaviorSubject<Item[]>([]);

...

if (!this.getItems$) {
  this.getItems$ = this.httpClient
  .get<Item[]>(url, options)
  .pipe(shareBehavior(this.items));
}
return this.getItems$;

Это работает очень хорошо. Но проблема заключалась в том, что, поскольку поведение имеет значение по умолчанию, всем подписчикам отправляется начальное значение []. Они обновляются, как только завершается вызов API. Но я бы предпочел использовать ReplaySubject, чтобы подписчики получали значение только после возврата http. Сделайте это, см. https://stackoverflow.com/a/67508863/787958

person ScubaSteve    schedule 12.05.2021