Website-Suche

angular - Unerwünschtes Abonnement auf dem Observable-Dienst


Folgend dem zwischengespeicherten Service-Muster verwende ich ein BehaviourSubject, das über ein nur lesbares Observable, das daraus in verschiedenen Komponenten erstellt wurde, abonniert wird

// credential.service.ts
private _credentialList$: BehaviorSubject<Credential[]>
readonly credentialListObv$: Observable<Credential[]>

...
this. credentialListObv$ = this.credentialList$.asObservable()

...
getCredentialList(...): Observable<Credential[]> {
    return this.http.get<Credential[]>(
      ``
      ).pipe(
      tap(credList => {
        ...
        this._credentialList$.next(credList)
      }),
    )
  }

Also löse ich von der Komponente aus die Methode getCredentialList aus, um die abgerufene Liste über das _credentialList$ Subject zu senden, und so sollten alle Abonnenten des credentialListObv$ den Wert erhalten.

Das Problem ist, dass ich beim Erstellen meiner Komponente eine unerwünschte Subscription erhalte, wenn ich die getCredentialList-Methode aufrufe.

// credential-list.component.ts

private _credentials$: Observable<Credential[]>

constructor(
    private credentialService: CredentialService,
  ) {
    this._credentials$ = this.credentialService.credentialListObv$.pipe(
      tap(() => {console.log("sub")}),
      ...
    )
  }

  ngAfterViewInit(): void {
    this.credentialService.getCredentialList(...).subscribe()
  }

  get credentials$() {
    return this._credentials$
  }

// credential-list.component.html
<mat-expansion-panel *ngFor="let credential of credentials$ | async; let i=index">
        ...

Das Ergebnis ist, dass beim Zugriff auf die Seite "sub" zweimal ausgegeben wird. Das erste Abonnement erfolgt über die async-Pipe innerhalb der Komponenten-Vorlage, aber warum gibt es ein weiteres, wenn ich niemals subscribe auf this._credentials$ aus der credential-list-Komponente aufrufen werde?

Antwort #1

Weil Sie BehaviourSubject verwenden, ist die erste Emission der Anfangswert davon. Später, in NgAfterViewInit, abonnieren Sie getCredentialList, was einen neuen Wert an dasselbe BehaviourSubject übergibt.

Versuchen Sie, die Werte zu protokollieren, anstatt eine Zeichenkette einzugeben:

tap((val) => {console.log(val)}),

Der erste sollte der Standard sein, den Sie beim Instanziieren des Subjects verwendet haben (this._credentialList$=new BehaviorSubject(...)

Antwort #2

Das liegt daran, dass Sie auch dem Servicemethode abonnieren, die das HTTP-Observable zurückgibt. Jetzt hat Ihre Komponente also zwei aktive Abonnements für einen HTTP-Aufruf.

HTTP-Methoden in Angular schließen nach dem Senden eines Werts ab, sodass Sie dem Observable innerhalb der Methode bedenkenlos abonnieren können.

getCredentialList(...): Observable<Credential[]> {
    this.http.get<Credential[]>(
      ``
      ).pipe(
      tap(credList => {
        ...
        this._credentialList$.next(credList)
      }),
    ).subscribe();
  }

Dann lassen Sie in Ihrer Komponente Ihr Observable unverändert, aber abonnieren Sie die Methode nicht.

ngAfterViewInit(): void {
  this.credentialService.getCredentialList(...);
}

Das Aufrufen der Servicemethode löst nun die HTTP-Anfrage aus, und Ihr privates _credentials$ Observable erhält weiterhin die Daten vom BehaviorSubject.


Bearbeiten: Da Sie gesagt haben, dass Sie nichts im Dienst abonnieren möchten, kann ich Ihnen eine Lösung anbieten, die nur ein Abonnement im Komponenten-Template erfordert.

// credential.service.ts
private credentialParams$ = new Subject<ApiParameters>();
public credentialListObv$: Observable<Credential[]>;

...
this.credentialListObv$ = this.credentialParams$
.pipe(
  switchMap(urlParameters=>
    this.http.get<Credential[]>(`url with ${urlParameters}`)
  )
);

...
getCredentialList(newParams:ApiParameters): void {
  this.credentialParams$.next(newParams);
}

Verwenden Sie das Subject, um alle Parameter auszugeben, die Sie für Ihren API-Aufruf benötigen. Dann nimmt das Observable diesen ausgegebenen Parameter und gibt direkt das HTTP-Request-Observable zurück.

// credential-list.component.ts

public credentials$: Observable<Credential[]>

constructor(
    private credentialService: CredentialService,
  ) {
    this.credentials$ = this.credentialService.credentialListObv$.pipe(
      tap(() => {console.log("sub")}),
      ...
    );
  }

  ngAfterViewInit(): void {
    this.credentialService.getCredentialList(apiParams);
  }

// credential-list.component.html
<mat-expansion-panel *ngFor="let credential of credentials$ | async; let i=index">

In der Komponente abonnieren wir nur das Service-Observable. Beim Init-Lifecycle rufen wir die Servicemethode auf. Dies bewirkt, dass Ihr Komponenten-Observable auf die HTTP-Anfrage reagiert, die vom Service-Observable zurückgegeben wird.

P.S. Es ist in Ordnung, etwas innerhalb eines Dienstes zu abonnieren, wenn Sie wissen, dass das Observable abgeschlossen wird (wie bei HTTP-Anfragen).