Was ist der richtige Weg, um das Ergebnis eines Angular Http-Netzwerkaufrufs in RxJs 5 zu teilen?


303

Mit Http rufen wir eine Methode auf, die einen Netzwerkaufruf ausführt und eine beobachtbare http zurückgibt:

getCustomer() {
    return this.http.get('/someUrl').map(res => res.json());
}

Wenn wir dies beobachten und mehrere Abonnenten hinzufügen:

let network$ = getCustomer();

let subscriber1 = network$.subscribe(...);
let subscriber2 = network$.subscribe(...);

Wir möchten sicherstellen, dass dies nicht zu mehreren Netzwerkanforderungen führt.

Dies mag wie ein ungewöhnliches Szenario erscheinen, ist aber tatsächlich recht häufig: Wenn der Anrufer beispielsweise das Observable abonniert, um eine Fehlermeldung anzuzeigen, und es mithilfe der asynchronen Pipe an die Vorlage weiterleitet, haben wir bereits zwei Abonnenten.

Wie geht das in RxJs 5 richtig?

Das scheint nämlich gut zu funktionieren:

getCustomer() {
    return this.http.get('/someUrl').map(res => res.json()).share();
}

Aber ist dies die idiomatische Art, dies in RxJs 5 zu tun, oder sollten wir stattdessen etwas anderes tun?

Hinweis: Gemäß Angular 5 new HttpClientist der .map(res => res.json())Teil in allen Beispielen jetzt unbrauchbar, da das JSON-Ergebnis jetzt standardmäßig angenommen wird.


1
> share ist identisch mit publish (). refCount (). Eigentlich ist es nicht. Siehe die folgende Diskussion: github.com/ReactiveX/rxjs/issues/1363
Christian

1
Die bearbeitete Frage sieht laut Problem so aus, als müssten die Dokumente im Code aktualisiert werden -> github.com/ReactiveX/rxjs/blob/master/src/operator/share.ts
Angular University

Ich denke, es kommt darauf an. Bei Aufrufen, bei denen die Daten nicht lokal zwischengespeichert werden können, ist dies möglicherweise aufgrund von Parameteränderungen / -kombinationen nicht sinnvoll .share () scheint absolut richtig zu sein. Wenn Sie die Dinge jedoch lokal zwischenspeichern können, sind einige der anderen Antworten zu ReplaySubject / BehaviorSubject ebenfalls gute Lösungen.
JimB

Ich denke, wir müssen nicht nur die Daten zwischenspeichern, sondern auch die zwischengespeicherten Daten aktualisieren / ändern. Es ist ein häufiger Fall. Zum Beispiel, wenn ich dem zwischengespeicherten Modell ein neues Feld hinzufügen oder den Wert des Feldes aktualisieren möchte. Vielleicht ist es besser , einen Singleton- DataCacheService mit der CRUD- Methode zu erstellen ? Wie Laden von Redux . Was denken Sie?
Slideshowp2

Sie könnten einfach ngx-cacheable verwenden ! Es passt besser zu Ihrem Szenario. Siehe meine Antwort unten
Tushar Walzade

Antworten:


230

Cache die Daten und wenn verfügbar zwischengespeichert, gib dies zurück, andernfalls mache die HTTP-Anfrage.

import {Injectable} from '@angular/core';
import {Http, Headers} from '@angular/http';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/observable/of'; //proper way to import the 'of' operator
import 'rxjs/add/operator/share';
import 'rxjs/add/operator/map';
import {Data} from './data';

@Injectable()
export class DataService {
  private url: string = 'https://cors-test.appspot.com/test';

  private data: Data;
  private observable: Observable<any>;

  constructor(private http: Http) {}

  getData() {
    if(this.data) {
      // if `data` is available just return it as `Observable`
      return Observable.of(this.data); 
    } else if(this.observable) {
      // if `this.observable` is set then the request is in progress
      // return the `Observable` for the ongoing request
      return this.observable;
    } else {
      // example header (not necessary)
      let headers = new Headers();
      headers.append('Content-Type', 'application/json');
      // create the request, store the `Observable` for subsequent subscribers
      this.observable = this.http.get(this.url, {
        headers: headers
      })
      .map(response =>  {
        // when the cached data is available we don't need the `Observable` reference anymore
        this.observable = null;

        if(response.status == 400) {
          return "FAILURE";
        } else if(response.status == 200) {
          this.data = new Data(response.json());
          return this.data;
        }
        // make it shared so more than one subscriber can get the result
      })
      .share();
      return this.observable;
    }
  }
}

Plunker Beispiel

Dieser Artikel https://blog.ehowtram.io/angular/2018/03/05/advanced-caching-with-rxjs.html ist eine großartige Erklärung, wie man mit Cache arbeitet shareReplay.


3
do()im Gegensatz zu map()ändert das Ereignis nicht. Sie könnten auch verwenden map(), mussten dann aber sicherstellen, dass am Ende des Rückrufs der richtige Wert zurückgegeben wird.
Günter Zöchbauer

3
Wenn die Call-Site, die das .subscribe()tut, den Wert nicht benötigt, können Sie das tun, weil es möglicherweise nur wird null(abhängig davon, was this.extractDatazurückgibt), aber meiner Meinung nach drückt dies die Absicht des Codes nicht gut aus.
Günter Zöchbauer

2
Wenn this.extraDatawie extraData() { if(foo) { doSomething();}}sonst endet, wird das Ergebnis des letzten Ausdrucks zurückgegeben, der möglicherweise nicht Ihren Wünschen entspricht.
Günter Zöchbauer

9
@ Günter, danke für den Code, es funktioniert. Ich versuche jedoch zu verstehen, warum Sie Daten und Observable getrennt verfolgen. Würden Sie nicht effektiv den gleichen Effekt erzielen, wenn Sie nur Observable <Data> wie folgt zwischenspeichern? if (this.observable) { return this.observable; } else { this.observable = this.http.get(url) .map(res => res.json().data); return this.observable; }
Juli.Tech

3
@HarleenKaur Es ist eine Klasse, für die der empfangene JSON deserialisiert wurde, um eine starke Typprüfung und automatische Vervollständigung zu erhalten. Es ist nicht nötig, es zu benutzen, aber es ist üblich.
Günter Zöchbauer

44

Laut @ Christian-Vorschlag ist dies eine Möglichkeit, die für HTTP-Observables gut funktioniert, die nur einmal emittieren und dann abgeschlossen werden:

getCustomer() {
    return this.http.get('/someUrl')
        .map(res => res.json()).publishLast().refCount();
}

Es gibt einige Probleme bei der Verwendung dieses Ansatzes - das zurückgegebene Observable kann nicht abgebrochen oder erneut versucht werden. Dies ist möglicherweise kein Problem für Sie, aber es könnte auch sein. Wenn dies ein Problem ist, ist der shareBediener möglicherweise eine vernünftige Wahl (wenn auch mit einigen bösen Randfällen). Für eine ausführliche
Christian

1
Kleine Klarstellung ... Obwohl streng genommen die Quelle, die von geteilt wird, publishLast().refCount()nicht gekündigt werden kann refCount, wird der Nettoeffekt , dass alle beobachtbaren Quellen abgemeldet werden, wenn alle Abonnements der beobachtbaren Quelle, die von zurückgegeben werden, storniert werden, wenn sie "Inflight"
Christian sind

@Christian Hey, kannst du erklären, was du meinst, indem du sagst "kann nicht storniert oder wiederholt werden"? Vielen Dank.
undefiniert

37

UPDATE: Ben Lesh sagt, dass Sie in der nächsten Nebenversion nach 5.2.0 einfach shareReplay () aufrufen können, um wirklich zwischenzuspeichern.

VORHER.....

Verwenden Sie erstens nicht share () oder PublishReplay (1) .refCount (), sie sind identisch und das Problem dabei ist, dass sie nur freigegeben werden, wenn Verbindungen hergestellt werden, während das Observable aktiv ist, wenn Sie nach Abschluss eine Verbindung herstellen , es schafft wieder eine neue beobachtbare Übersetzung, nicht wirklich Caching.

Birowski hat oben die richtige Lösung angegeben, nämlich ReplaySubject zu verwenden. ReplaySubject speichert die von Ihnen angegebenen Werte (bufferSize) in unserem Fall 1 zwischen. Sobald refCount Null erreicht und Sie eine neue Verbindung herstellen, wird kein neues beobachtbares like share () erstellt. Dies ist das richtige Verhalten für das Caching.

Hier ist eine wiederverwendbare Funktion

export function cacheable<T>(o: Observable<T>): Observable<T> {
  let replay = new ReplaySubject<T>(1);
  o.subscribe(
    x => replay.next(x),
    x => replay.error(x),
    () => replay.complete()
  );
  return replay.asObservable();
}

Hier erfahren Sie, wie Sie es verwenden

import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import { cacheable } from '../utils/rxjs-functions';

@Injectable()
export class SettingsService {
  _cache: Observable<any>;
  constructor(private _http: Http, ) { }

  refresh = () => {
    if (this._cache) {
      return this._cache;
    }
    return this._cache = cacheable<any>(this._http.get('YOUR URL'));
  }
}

Im Folgenden finden Sie eine erweiterte Version der zwischenspeicherbaren Funktion. Diese ermöglicht eine eigene Nachschlagetabelle + die Möglichkeit, eine benutzerdefinierte Nachschlagetabelle bereitzustellen. Auf diese Weise müssen Sie this._cache nicht wie im obigen Beispiel überprüfen. Beachten Sie auch, dass Sie anstelle der Übergabe der Observable als erstes Argument eine Funktion übergeben, die die Observables zurückgibt. Dies liegt daran, dass Angulars HTTP sofort ausgeführt wird. Wenn Sie also eine verzögert ausgeführte Funktion zurückgeben, können Sie entscheiden, sie nicht aufzurufen, wenn sie bereits vorhanden ist unser Cache.

let cacheableCache: { [key: string]: Observable<any> } = {};
export function cacheable<T>(returnObservable: () => Observable<T>, key?: string, customCache?: { [key: string]: Observable<T> }): Observable<T> {
  if (!!key && (customCache || cacheableCache)[key]) {
    return (customCache || cacheableCache)[key] as Observable<T>;
  }
  let replay = new ReplaySubject<T>(1);
  returnObservable().subscribe(
    x => replay.next(x),
    x => replay.error(x),
    () => replay.complete()
  );
  let observable = replay.asObservable();
  if (!!key) {
    if (!!customCache) {
      customCache[key] = observable;
    } else {
      cacheableCache[key] = observable;
    }
  }
  return observable;
}

Verwendungszweck:

getData() => cacheable(this._http.get("YOUR URL"), "this is key for my cache")

Gibt es einen Grund, diese Lösung nicht als RxJs-Operator zu verwenden : const data$ = this._http.get('url').pipe(cacheable()); /*1st subscribe*/ data$.subscribe(); /*2nd subscribe*/ data$.subscribe();? So verhält es sich mehr wie jeder andere Operator ..
Felix

31

rxjs 5.4.0 verfügt über eine neue shareReplay- Methode.

Der Autor sagt ausdrücklich "ideal für den Umgang mit Dingen wie dem Zwischenspeichern von AJAX-Ergebnissen"

rxjs PR # 2443 feat (shareReplay): fügt eine shareReplayVariante von hinzupublishReplay

shareReplay gibt eine Observable zurück, die die Quelle ist, die über ein ReplaySubject multicasted ist. Dieses Wiederholungsobjekt wird bei einem Fehler aus der Quelle wiederverwendet, jedoch nicht nach Abschluss der Quelle. Dies macht shareReplay ideal für die Verarbeitung von AJAX-Ergebnissen, da es erneut versucht werden kann. Das Wiederholungsverhalten unterscheidet sich jedoch vom Anteil darin, dass es die beobachtbare Quelle nicht wiederholt, sondern die Werte der beobachtbaren Quelle wiederholt.


Hat es damit zu tun? Diese Dokumente stammen jedoch aus dem Jahr 2014. github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/…
Aaron Hoffman

4
Ich habe versucht, .shareReplay (1, 10000) zu einem Observable hinzuzufügen, aber ich habe keine Zwischenspeicherung oder Verhaltensänderung bemerkt. Gibt es ein funktionierendes Beispiel?
Aydus-Matthew

Ein Blick auf das Änderungsprotokoll github.com/ReactiveX/rxjs/blob/… Es erschien früher, wurde in Version 5 entfernt und in Version 5.4 hinzugefügt. Dieser RX-Book-Link bezieht sich auf Version 4, ist jedoch in der aktuellen Version LTS Version 5.5.6 und vorhanden es ist in v6. Ich stelle mir vor, dass der RX-Book-Link dort veraltet ist.
Jason Awbrey

25

nach diesem Artikel

Es stellt sich heraus, dass wir dem Observable leicht Caching hinzufügen können, indem wir PublishReplay (1) und RefCount hinzufügen.

also drinnen, wenn Anweisungen nur angehängt werden

.publishReplay(1)
.refCount();

zu .map(...)


11

rxjs Version 5.4.0 (2017-05-09) bietet Unterstützung für shareReplay .

Warum shareReplay verwenden?

Sie möchten shareReplay im Allgemeinen verwenden, wenn Sie Nebenwirkungen haben oder Berechnungen belasten, die nicht unter mehreren Abonnenten ausgeführt werden sollen. Dies kann auch in Situationen hilfreich sein, in denen Sie wissen, dass Sie späte Abonnenten eines Streams haben, die Zugriff auf zuvor ausgegebene Werte benötigen. Diese Fähigkeit, Werte im Abonnement wiederzugeben, unterscheidet share und shareReplay.

Sie können einen Winkeldienst leicht ändern, um dies zu verwenden, und ein Observable mit einem zwischengespeicherten Ergebnis zurückgeben, das den http-Aufruf immer nur einmal ausführt (vorausgesetzt, der erste Aufruf war erfolgreich).

Beispiel Angular Service

Hier ist ein sehr einfacher Kundenservice, der verwendet shareReplay.

customer.service.ts

import { shareReplay } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';

@Injectable()
export class CustomerService {

    private readonly _getCustomers: Observable<ICustomer[]>;

    constructor(private readonly http: HttpClient) {
        this._getCustomers = this.http.get<ICustomer[]>('/api/customers/').pipe(shareReplay());
    }

    getCustomers() : Observable<ICustomer[]> {
        return this._getCustomers;
    }
}

export interface ICustomer {
  /* ICustomer interface fields defined here */
}

Beachten Sie, dass die Zuweisung im Konstruktor in die Methode verschoben werden kann. Da getCustomersjedoch die von zurückgegebenen Observablen HttpClient"kalt" sind , ist dies im Konstruktor akzeptabel, da der http-Aufruf nur bei jedem ersten Aufruf von ausgeführt wird subscribe.

Hierbei wird auch davon ausgegangen, dass die ursprünglich zurückgegebenen Daten während der Lebensdauer der Anwendungsinstanz nicht veraltet sind.


Ich mag dieses Muster sehr und möchte es in einer gemeinsam genutzten Bibliothek von API-Diensten implementieren, die ich in einer Reihe von Anwendungen verwende. Ein Beispiel ist ein UserService, und überall außer an einigen Stellen muss der Cache während der Lebensdauer der App nicht ungültig gemacht werden. Wie kann ich ihn in diesen Fällen ungültig machen, ohne dass frühere Abonnements verwaist sind?
SirTophamHatt

10

Ich habe die Frage mit einem Stern versehen, aber ich werde versuchen, es zu versuchen.

//this will be the shared observable that 
//anyone can subscribe to, get the value, 
//but not cause an api request
let customer$ = new Rx.ReplaySubject(1);

getCustomer().subscribe(customer$);

//here's the first subscriber
customer$.subscribe(val => console.log('subscriber 1: ' + val));

//here's the second subscriber
setTimeout(() => {
  customer$.subscribe(val => console.log('subscriber 2: ' + val));  
}, 1000);

function getCustomer() {
  return new Rx.Observable(observer => {
    console.log('api request');
    setTimeout(() => {
      console.log('api response');
      observer.next('customer object');
      observer.complete();
    }, 500);
  });
}

Hier ist der Beweis :)

Es gibt nur einen Imbiss: getCustomer().subscribe(customer$)

Wir abonnieren nicht die API-Antwort von getCustomer(), wir abonnieren ein beobachtbares ReplaySubject, das auch ein anderes Observable abonnieren kann, und (und das ist wichtig) halten Sie den zuletzt ausgegebenen Wert und veröffentlichen Sie ihn erneut auf einem seiner (ReplaySubjects) ) Abonnenten.


1
Ich mag diesen Ansatz, da er rxjs gut nutzt und keine benutzerdefinierte Logik hinzufügen muss, danke
Thibs

7

Ich habe eine Möglichkeit gefunden, das http get-Ergebnis in sessionStorage zu speichern und für die Sitzung zu verwenden, damit der Server nie wieder aufgerufen wird.

Ich habe es verwendet, um die Github-API aufzurufen, um Nutzungsbeschränkungen zu vermeiden.

@Injectable()
export class HttpCache {
  constructor(private http: Http) {}

  get(url: string): Observable<any> {
    let cached: any;
    if (cached === sessionStorage.getItem(url)) {
      return Observable.of(JSON.parse(cached));
    } else {
      return this.http.get(url)
        .map(resp => {
          sessionStorage.setItem(url, resp.text());
          return resp.json();
        });
    }
  }
}

Zu Ihrer Information, das SessionStorage-Limit beträgt 5 Millionen (oder 4,75 Millionen). Daher sollte es für große Datenmengen nicht so verwendet werden.

------ edit -------------
Wenn Sie Daten mit F5 aktualisieren möchten, das Speicherdaten anstelle von sessionStorage verwendet;

@Injectable()
export class HttpCache {
  cached: any = {};  // this will store data
  constructor(private http: Http) {}

  get(url: string): Observable<any> {
    if (this.cached[url]) {
      return Observable.of(this.cached[url]));
    } else {
      return this.http.get(url)
        .map(resp => {
          this.cached[url] = resp.text();
          return resp.json();
        });
    }
  }
}

Wenn Sie im Sitzungsspeicher speichern, wie stellen Sie dann sicher, dass der Sitzungsspeicher beim Verlassen der App zerstört wird?
Gags

Dies führt jedoch zu unerwartetem Verhalten für den Benutzer. Wenn der Benutzer F5 oder die Schaltfläche "Aktualisieren" des Browsers drückt, erwartet er neue Daten vom Server. Tatsächlich erhält er veraltete Daten von localStorage. Fehlerberichte, Support-Tickets usw. eingehen ... Wie der Name schon sessionStoragesagt, würde ich es nur für Daten verwenden, von denen erwartet wird , dass sie für die gesamte Sitzung konsistent sind.
Martin Schneider

@ MA-Maddin als ich sagte "Ich habe es verwendet, um Nutzungsbeschränkungen zu vermeiden". Wenn Sie möchten, dass Daten mit F5 aktualisiert werden, müssen Sie anstelle von sessionStorage Speicher verwenden. Die Antwort wurde mit diesem Ansatz bearbeitet.
Allenhwkim

Ja, das könnte ein Anwendungsfall sein. Ich wurde gerade ausgelöst, da alle über Cache sprechen und OP getCustomerin seinem Beispiel hat. ;) Also wollte nur ein paar Leute warnen, die die Risiken vielleicht nicht sehen :)
Martin Schneider

5

Die von Ihnen gewählte Implementierung hängt davon ab, ob unsubscribe () Ihre HTTP-Anfrage abbrechen soll oder nicht.

In jedem Fall sind TypeScript-Dekoratoren eine gute Möglichkeit, das Verhalten zu standardisieren. Dies ist der, den ich geschrieben habe:

  @CacheObservableArgsKey
  getMyThing(id: string): Observable<any> {
    return this.http.get('things/'+id);
  }

Dekorateur Definition:

/**
 * Decorator that replays and connects to the Observable returned from the function.
 * Caches the result using all arguments to form a key.
 * @param target
 * @param name
 * @param descriptor
 * @returns {PropertyDescriptor}
 */
export function CacheObservableArgsKey(target: Object, name: string, descriptor: PropertyDescriptor) {
  const originalFunc = descriptor.value;
  const cacheMap = new Map<string, any>();
  descriptor.value = function(this: any, ...args: any[]): any {
    const key = args.join('::');

    let returnValue = cacheMap.get(key);
    if (returnValue !== undefined) {
      console.log(`${name} cache-hit ${key}`, returnValue);
      return returnValue;
    }

    returnValue = originalFunc.apply(this, args);
    console.log(`${name} cache-miss ${key} new`, returnValue);
    if (returnValue instanceof Observable) {
      returnValue = returnValue.publishReplay(1);
      returnValue.connect();
    }
    else {
      console.warn('CacheHttpArgsKey: value not an Observable cannot publishReplay and connect', returnValue);
    }
    cacheMap.set(key, returnValue);
    return returnValue;
  };

  return descriptor;
}

Hallo @Arlo - das obige Beispiel wird nicht kompiliert. Property 'connect' does not exist on type '{}'.von der Linie returnValue.connect();. Können Sie das näher erläutern?
Huf

4

Zwischenspeicherbare HTTP-Antwortdaten mit Rxjs Observer / Observable + Caching + Subscription

Siehe Code unten

* Haftungsausschluss: Ich bin neu bei rxjs. Denken Sie also daran, dass ich den Observable / Observer-Ansatz möglicherweise missbrauche. Meine Lösung ist lediglich ein Konglomerat anderer Lösungen, die ich gefunden habe, und ist die Folge davon, dass ich keine einfache, gut dokumentierte Lösung gefunden habe. Daher biete ich meine vollständige Codelösung (wie ich sie gerne gefunden hätte) in der Hoffnung an, dass sie anderen hilft.

* Beachten Sie, dass dieser Ansatz lose auf GoogleFirebaseObservables basiert. Leider fehlt mir die richtige Erfahrung / Zeit, um zu wiederholen, was sie unter der Haube getan haben. Das Folgende ist jedoch eine vereinfachte Methode, um einen asynchronen Zugriff auf einige cachefähige Daten bereitzustellen.

Situation : Eine 'Produktlisten'-Komponente hat die Aufgabe, eine Produktliste anzuzeigen. Die Website ist eine einseitige Web-App mit einigen Menüschaltflächen, mit denen die auf der Seite angezeigten Produkte "gefiltert" werden.

Lösung : Die Komponente "abonniert" eine Servicemethode. Die Servicemethode gibt ein Array von Produktobjekten zurück, auf die die Komponente über den Abonnement-Rückruf zugreift. Die Servicemethode packt ihre Aktivität in einen neu erstellten Observer und gibt den Observer zurück. Innerhalb dieses Beobachters sucht er nach zwischengespeicherten Daten und gibt sie an den Abonnenten (die Komponente) zurück und gibt sie zurück. Andernfalls wird ein http-Aufruf zum Abrufen der Daten ausgegeben, die Antwort abonniert, in der Sie diese Daten verarbeiten (z. B. die Daten Ihrem eigenen Modell zuordnen) und die Daten dann an den Abonnenten zurückgeben können.

Der Code

product-list.component.ts

import { Component, OnInit, Input } from '@angular/core';
import { ProductService } from '../../../services/product.service';
import { Product, ProductResponse } from '../../../models/Product';

@Component({
  selector: 'app-product-list',
  templateUrl: './product-list.component.html',
  styleUrls: ['./product-list.component.scss']
})
export class ProductListComponent implements OnInit {
  products: Product[];

  constructor(
    private productService: ProductService
  ) { }

  ngOnInit() {
    console.log('product-list init...');
    this.productService.getProducts().subscribe(products => {
      console.log('product-list received updated products');
      this.products = products;
    });
  }
}

product.service.ts

import { Injectable } from '@angular/core';
import { Http, Headers } from '@angular/http';
import { Observable, Observer } from 'rxjs';
import 'rxjs/add/operator/map';
import { Product, ProductResponse } from '../models/Product';

@Injectable()
export class ProductService {
  products: Product[];

  constructor(
    private http:Http
  ) {
    console.log('product service init.  calling http to get products...');

  }

  getProducts():Observable<Product[]>{
    //wrap getProducts around an Observable to make it async.
    let productsObservable$ = Observable.create((observer: Observer<Product[]>) => {
      //return products if it was previously fetched
      if(this.products){
        console.log('## returning existing products');
        observer.next(this.products);
        return observer.complete();

      }
      //Fetch products from REST API
      console.log('** products do not yet exist; fetching from rest api...');
      let headers = new Headers();
      this.http.get('http://localhost:3000/products/',  {headers: headers})
      .map(res => res.json()).subscribe((response:ProductResponse) => {
        console.log('productResponse: ', response);
        let productlist = Product.fromJsonList(response.products); //convert service observable to product[]
        this.products = productlist;
        observer.next(productlist);
      });
    }); 
    return productsObservable$;
  }
}

product.ts (das Modell)

export interface ProductResponse {
  success: boolean;
  msg: string;
  products: Product[];
}

export class Product {
  product_id: number;
  sku: string;
  product_title: string;
  ..etc...

  constructor(product_id: number,
    sku: string,
    product_title: string,
    ...etc...
  ){
    //typescript will not autoassign the formal parameters to related properties for exported classes.
    this.product_id = product_id;
    this.sku = sku;
    this.product_title = product_title;
    ...etc...
  }



  //Class method to convert products within http response to pure array of Product objects.
  //Caller: product.service:getProducts()
  static fromJsonList(products:any): Product[] {
    let mappedArray = products.map(Product.fromJson);
    return mappedArray;
  }

  //add more parameters depending on your database entries and constructor
  static fromJson({ 
      product_id,
      sku,
      product_title,
      ...etc...
  }): Product {
    return new Product(
      product_id,
      sku,
      product_title,
      ...etc...
    );
  }
}

Hier ist ein Beispiel für die Ausgabe, die ich beim Laden der Seite in Chrome sehe. Beachten Sie, dass beim ersten Laden die Produkte von http abgerufen werden (Aufruf meines Node Rest-Dienstes, der lokal auf Port 3000 ausgeführt wird). Wenn ich dann auf klicke, um zu einer gefilterten Ansicht der Produkte zu navigieren, werden die Produkte im Cache gefunden.

Mein Chrome-Protokoll (Konsole):

core.es5.js:2925 Angular is running in the development mode. Call enableProdMode() to enable the production mode.
app.component.ts:19 app.component url: /products
product.service.ts:15 product service init.  calling http to get products...
product-list.component.ts:18 product-list init...
product.service.ts:29 ** products do not yet exist; fetching from rest api...
product.service.ts:33 productResponse:  {success: true, msg: "Products found", products: Array(23)}
product-list.component.ts:20 product-list received updated products

... [klickte auf eine Menüschaltfläche, um die Produkte zu filtern] ...

app.component.ts:19 app.component url: /products/chocolatechip
product-list.component.ts:18 product-list init...
product.service.ts:24 ## returning existing products
product-list.component.ts:20 product-list received updated products

Fazit: Dies ist der einfachste Weg, den ich (bisher) gefunden habe, um zwischengespeicherte http-Antwortdaten zu implementieren. In meiner eckigen App wird die Produktlistenkomponente jedes Mal neu geladen, wenn ich zu einer anderen Ansicht der Produkte navigiere. ProductService scheint eine gemeinsam genutzte Instanz zu sein, daher bleibt der lokale Cache von 'products: Product []' im ProductService während der Navigation erhalten, und nachfolgende Aufrufe von "GetProducts ()" geben den zwischengespeicherten Wert zurück. Ein letzter Hinweis: Ich habe Kommentare darüber gelesen, wie Observables / Abonnements geschlossen werden müssen, wenn Sie fertig sind, um Speicherverluste zu vermeiden. Ich habe dies hier nicht aufgenommen, aber es ist etwas zu beachten.


1
Hinweis - Ich habe seitdem eine leistungsstärkere Lösung gefunden, die RxJS BehaviorSubjects umfasst, die den Code vereinfacht und den Overhead drastisch reduziert. Importieren Sie in products.service.ts {BehaviorSubject} aus 'rxjs'; 2. Ändern Sie 'products: Product []' in 'product $: BehaviorSubject <Product []> = new BehaviorSubject <Product []> ([]);' 3. Jetzt können Sie einfach die http aufrufen, ohne etwas zurückzugeben. http_getProducts () {this.http.get (...). map (res => res.json ()). subscribe (products => this.product $ .next (products))};
ObjectiveTC

1
Die lokale Variable 'product $' ist ein behaviourSubject, das sowohl die neuesten Produkte emittiert als auch speichert (aus dem Aufruf von product $ .next (..) in Teil 3). Injizieren Sie nun in Ihren Komponenten den Service wie gewohnt. Sie erhalten den zuletzt zugewiesenen Wert von product $ mit productService.product $ .value. Oder abonnieren Sie Produkt $, wenn Sie eine Aktion ausführen möchten, wenn Produkt $ einen neuen Wert erhält (dh die Funktion Produkt $ .next (...) wird in Teil 3 aufgerufen).
ObjectiveTC

1
ZB in products.component.ts ... this.productService.product $ .takeUntil (this.ngUnsubscribe) .subscribe ((products) => {this.category); let filteredProducts = this.productService.getProductsByCategory (this.category); this.products = filteredProducts; });
ObjectiveTC

1
Ein wichtiger Hinweis zum Abbestellen von Observables: ".takeUntil (this.ngUnsubscribe)". Sehen Sie sich diese Frage / Antwort zum Stapelüberlauf an, die den de facto empfohlenen Weg zum Abbestellen von Ereignissen zu zeigen scheint: stackoverflow.com/questions/38008334/…
ObjectiveTC

1
Die Alternative ist .first () oder .take (1), wenn das Observable nur einmal Daten empfangen soll. Alle anderen "unendlichen Streams" von Observablen sollten in "ngOnDestroy ()" abgemeldet werden. Wenn Sie dies nicht tun, erhalten Sie möglicherweise doppelte "Observable" -Rückrufe. stackoverflow.com/questions/28007777/…
ObjectiveTC

3

Ich gehe davon aus, dass @ ngx-cache / core nützlich sein könnte, um die Caching-Funktionen für die http-Aufrufe beizubehalten, insbesondere wenn der HTTP-Aufruf sowohl auf Browser- als auch auf Serverplattformen erfolgt .

Angenommen, wir haben die folgende Methode:

getCustomer() {
  return this.http.get('/someUrl').map(res => res.json());
}

Sie können die Verwendung CachedDekorateur von @ NGX-Cache / Kern den zurückgegebenen Wert aus dem Verfahren zum Speichern der HTTP - Aufruf an die Herstellung cache storage( die storagekonfigurierbar sein kann, überprüfen Sie bitte die Umsetzung bei ng-Samen / universal ) - direkt auf der ersten Ausführung. Beim nächsten Aufruf der Methode (unabhängig von Browser oder Serverplattform ) wird der Wert von der abgerufen cache storage.

import { Cached } from '@ngx-cache/core';

...

@Cached('get-customer') // the cache key/identifier
getCustomer() {
  return this.http.get('/someUrl').map(res => res.json());
}

Es gibt auch die Möglichkeit zur Verwendung Caching - Methoden ( has, get, set) mit dem Caching API .

anyclass.ts

...
import { CacheService } from '@ngx-cache/core';

@Injectable()
export class AnyClass {
  constructor(private readonly cache: CacheService) {
    // note that CacheService is injected into a private property of AnyClass
  }

  // will retrieve 'some string value'
  getSomeStringValue(): string {
    if (this.cache.has('some-string'))
      return this.cache.get('some-string');

    this.cache.set('some-string', 'some string value');
    return 'some string value';
  }
}

Hier ist die Liste der Pakete, sowohl für das clientseitige als auch für das serverseitige Caching:


1

rxjs 5.3.0

Ich war nicht glücklich mit .map(myFunction).publishReplay(1).refCount()

Mit mehreren Teilnehmern, .map()führt myFunctionzweimal in einigen Fällen (Ich erwarte , dass es nur einmal ausführen). Ein Fix scheint zu seinpublishReplay(1).refCount().take(1)

Eine andere Sache, die Sie tun können, ist refCount(), das Observable nicht sofort zu verwenden und heiß zu machen:

let obs = this.http.get('my/data.json').publishReplay(1);
obs.connect();
return obs;

Dadurch wird die HTTP-Anforderung unabhängig von den Abonnenten gestartet. Ich bin nicht sicher, ob das Abbestellen vor Abschluss des HTTP-GET das Programm abbricht oder nicht.


1

Wir möchten sicherstellen, dass dies nicht zu mehreren Netzwerkanforderungen führt.

Mein persönlicher Favorit ist die Verwendung von asyncMethoden für Anrufe, die Netzwerkanforderungen stellen. Die Methoden selbst geben keinen Wert zurück, sondern aktualisieren einen BehaviorSubjectinnerhalb desselben Dienstes, den die Komponenten abonnieren.

Warum nun ein BehaviorSubjectstatt eines verwenden Observable? Weil,

  • Beim Abonnement gibt BehaviorSubject den letzten Wert zurück, während ein reguläres Observable nur ausgelöst wird, wenn es einen empfängt onnext.
  • Wenn Sie den letzten Wert des BehaviorSubject in einem nicht beobachtbaren Code (ohne Abonnement) abrufen möchten, können Sie die getValue()Methode verwenden.

Beispiel:

customer.service.ts

public customers$: BehaviorSubject<Customer[]> = new BehaviorSubject([]);

public async getCustomers(): Promise<void> {
    let customers = await this.httpClient.post<LogEntry[]>(this.endPoint, criteria).toPromise();
    if (customers) 
        this.customers$.next(customers);
}

Dann können wir, wo immer erforderlich, einfach abonnieren customers$.

public ngOnInit(): void {
    this.customerService.customers$
    .subscribe((customers: Customer[]) => this.customerList = customers);
}

Oder vielleicht möchten Sie es direkt in einer Vorlage verwenden

<li *ngFor="let customer of customerService.customers$ | async"> ... </li>

Bis Sie einen weiteren Aufruf tätigen getCustomers, bleiben die Daten im customers$BehaviorSubject erhalten.

Was ist, wenn Sie diese Daten aktualisieren möchten? Rufen Sie einfach angetCustomers()

public async refresh(): Promise<void> {
    try {
      await this.customerService.getCustomers();
    } 
    catch (e) {
      // request failed, handle exception
      console.error(e);
    }
}

Mit dieser Methode müssen wir die Daten zwischen nachfolgenden Netzwerkaufrufen nicht explizit beibehalten, da sie von der verarbeitet werden BehaviorSubject.

PS: Wenn eine Komponente zerstört wird, empfiehlt es sich normalerweise, die Abonnements zu entfernen. Dazu können Sie die in dieser Antwort vorgeschlagene Methode verwenden .


1

Tolle Antworten.

Oder Sie könnten dies tun:

Dies ist aus der neuesten Version von rxjs. Ich verwende die Version 5.5.7 von RxJS

import {share} from "rxjs/operators";

this.http.get('/someUrl').pipe(share());

0

Rufen Sie einfach share () nach der Karte und vor dem Abonnieren auf .

In meinem Fall habe ich einen generischen Dienst (RestClientService.ts), der den Restaufruf ausführt, Daten extrahiert, auf Fehler prüft und beobachtbar an einen konkreten Implementierungsdienst (z. B. ContractClientService.ts) zurückgibt, schließlich diese konkrete Implementierung Gibt beobachtbar an de ContractComponent.ts zurück, und dieser abonniert, um die Ansicht zu aktualisieren.

RestClientService.ts:

export abstract class RestClientService<T extends BaseModel> {

      public GetAll = (path: string, property: string): Observable<T[]> => {
        let fullPath = this.actionUrl + path;
        let observable = this._http.get(fullPath).map(res => this.extractData(res, property));
        observable = observable.share();  //allows multiple subscribers without making again the http request
        observable.subscribe(
          (res) => {},
          error => this.handleError2(error, "GetAll", fullPath),
          () => {}
        );
        return observable;
      }

  private extractData(res: Response, property: string) {
    ...
  }
  private handleError2(error: any, method: string, path: string) {
    ...
  }

}

ContractService.ts:

export class ContractService extends RestClientService<Contract> {
  private GET_ALL_ITEMS_REST_URI_PATH = "search";
  private GET_ALL_ITEMS_PROPERTY_PATH = "contract";
  public getAllItems(): Observable<Contract[]> {
    return this.GetAll(this.GET_ALL_ITEMS_REST_URI_PATH, this.GET_ALL_ITEMS_PROPERTY_PATH);
  }

}

ContractComponent.ts:

export class ContractComponent implements OnInit {

  getAllItems() {
    this.rcService.getAllItems().subscribe((data) => {
      this.items = data;
   });
  }

}

0

Ich habe eine Cache-Klasse geschrieben,

/**
 * Caches results returned from given fetcher callback for given key,
 * up to maxItems results, deletes the oldest results when full (FIFO).
 */
export class StaticCache
{
    static cachedData: Map<string, any> = new Map<string, any>();
    static maxItems: number = 400;

    static get(key: string){
        return this.cachedData.get(key);
    }

    static getOrFetch(key: string, fetcher: (string) => any): any {
        let value = this.cachedData.get(key);

        if (value != null){
            console.log("Cache HIT! (fetcher)");
            return value;
        }

        console.log("Cache MISS... (fetcher)");
        value = fetcher(key);
        this.add(key, value);
        return value;
    }

    static add(key, value){
        this.cachedData.set(key, value);
        this.deleteOverflowing();
    }

    static deleteOverflowing(): void {
        if (this.cachedData.size > this.maxItems) {
            this.deleteOldest(this.cachedData.size - this.maxItems);
        }
    }

    /// A Map object iterates its elements in insertion order — a for...of loop returns an array of [key, value] for each iteration.
    /// However that seems not to work. Trying with forEach.
    static deleteOldest(howMany: number): void {
        //console.debug("Deleting oldest " + howMany + " of " + this.cachedData.size);
        let iterKeys = this.cachedData.keys();
        let item: IteratorResult<string>;
        while (howMany-- > 0 && (item = iterKeys.next(), !item.done)){
            //console.debug("    Deleting: " + item.value);
            this.cachedData.delete(item.value); // Deleting while iterating should be ok in JS.
        }
    }

    static clear(): void {
        this.cachedData = new Map<string, any>();
    }

}

Es ist alles statisch, weil wir es verwenden, aber Sie können es zu einer normalen Klasse und einem Service machen. Ich bin mir nicht sicher, ob Angular eine einzelne Instanz für die gesamte Zeit behält (neu in Angular2).

Und so benutze ich es:

            let httpService: Http = this.http;
            function fetcher(url: string): Observable<any> {
                console.log("    Fetching URL: " + url);
                return httpService.get(url).map((response: Response) => {
                    if (!response) return null;
                    if (typeof response.json() !== "array")
                        throw new Error("Graph REST should return an array of vertices.");
                    let items: any[] = graphService.fromJSONarray(response.json(), httpService);
                    return array ? items : items[0];
                });
            }

            // If data is a link, return a result of a service call.
            if (this.data[verticesLabel][name]["link"] || this.data[verticesLabel][name]["_type"] == "link")
            {
                // Make an HTTP call.
                let url = this.data[verticesLabel][name]["link"];
                let cachedObservable: Observable<any> = StaticCache.getOrFetch(url, fetcher);
                if (!cachedObservable)
                    throw new Error("Failed loading link: " + url);
                return cachedObservable;
            }

Ich nehme an, es könnte einen klügeren Weg geben, der einige ObservableTricks verwendet, aber das war für meine Zwecke in Ordnung.


0

Verwenden Sie einfach diese Cache-Ebene, sie erledigt alles, was Sie benötigen, und verwaltet sogar den Cache für Ajax-Anforderungen.

http://www.ravinderpayal.com/blogs/12Jan2017-Ajax-Cache-Mangement-Angular2-Service.html

Es ist so einfach zu bedienen

@Component({
    selector: 'home',
    templateUrl: './html/home.component.html',
    styleUrls: ['./css/home.component.css'],
})
export class HomeComponent {
    constructor(AjaxService:AjaxService){
        AjaxService.postCache("/api/home/articles").subscribe(values=>{console.log(values);this.articles=values;});
    }

    articles={1:[{data:[{title:"first",sort_text:"description"},{title:"second",sort_text:"description"}],type:"Open Source Works"}]};
}

Die Schicht (als injizierbarer Winkeldienst) ist

import { Injectable }     from '@angular/core';
import { Http, Response} from '@angular/http';
import { Observable }     from 'rxjs/Observable';
import './../rxjs/operator'
@Injectable()
export class AjaxService {
    public data:Object={};
    /*
    private dataObservable:Observable<boolean>;
     */
    private dataObserver:Array<any>=[];
    private loading:Object={};
    private links:Object={};
    counter:number=-1;
    constructor (private http: Http) {
    }
    private loadPostCache(link:string){
     if(!this.loading[link]){
               this.loading[link]=true;
               this.links[link].forEach(a=>this.dataObserver[a].next(false));
               this.http.get(link)
                   .map(this.setValue)
                   .catch(this.handleError).subscribe(
                   values => {
                       this.data[link] = values;
                       delete this.loading[link];
                       this.links[link].forEach(a=>this.dataObserver[a].next(false));
                   },
                   error => {
                       delete this.loading[link];
                   }
               );
           }
    }

    private setValue(res: Response) {
        return res.json() || { };
    }

    private handleError (error: Response | any) {
        // In a real world app, we might use a remote logging infrastructure
        let errMsg: string;
        if (error instanceof Response) {
            const body = error.json() || '';
            const err = body.error || JSON.stringify(body);
            errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
        } else {
            errMsg = error.message ? error.message : error.toString();
        }
        console.error(errMsg);
        return Observable.throw(errMsg);
    }

    postCache(link:string): Observable<Object>{

         return Observable.create(observer=> {
             if(this.data.hasOwnProperty(link)){
                 observer.next(this.data[link]);
             }
             else{
                 let _observable=Observable.create(_observer=>{
                     this.counter=this.counter+1;
                     this.dataObserver[this.counter]=_observer;
                     this.links.hasOwnProperty(link)?this.links[link].push(this.counter):(this.links[link]=[this.counter]);
                     _observer.next(false);
                 });
                 this.loadPostCache(link);
                 _observable.subscribe(status=>{
                     if(status){
                         observer.next(this.data[link]);
                     }
                     }
                 );
             }
            });
        }
}

0

Es ist .publishReplay(1).refCount();oder .publishLast().refCount();seit Angular Http Observables nach Anfrage abgeschlossen.

Diese einfache Klasse speichert das Ergebnis zwischen, sodass Sie .value viele Male abonnieren können, und stellt nur eine Anfrage. Sie können auch .reload () verwenden, um neue Anforderungen zu stellen und Daten zu veröffentlichen.

Sie können es verwenden wie:

let res = new RestResource(() => this.http.get('inline.bundleo.js'));

res.status.subscribe((loading)=>{
    console.log('STATUS=',loading);
});

res.value.subscribe((value) => {
  console.log('VALUE=', value);
});

und die Quelle:

export class RestResource {

  static readonly LOADING: string = 'RestResource_Loading';
  static readonly ERROR: string = 'RestResource_Error';
  static readonly IDLE: string = 'RestResource_Idle';

  public value: Observable<any>;
  public status: Observable<string>;
  private loadStatus: Observer<any>;

  private reloader: Observable<any>;
  private reloadTrigger: Observer<any>;

  constructor(requestObservableFn: () => Observable<any>) {
    this.status = Observable.create((o) => {
      this.loadStatus = o;
    });

    this.reloader = Observable.create((o: Observer<any>) => {
      this.reloadTrigger = o;
    });

    this.value = this.reloader.startWith(null).switchMap(() => {
      if (this.loadStatus) {
        this.loadStatus.next(RestResource.LOADING);
      }
      return requestObservableFn()
        .map((res) => {
          if (this.loadStatus) {
            this.loadStatus.next(RestResource.IDLE);
          }
          return res;
        }).catch((err)=>{
          if (this.loadStatus) {
            this.loadStatus.next(RestResource.ERROR);
          }
          return Observable.of(null);
        });
    }).publishReplay(1).refCount();
  }

  reload() {
    this.reloadTrigger.next(null);
  }

}

0

Sie können eine einfache Klasse Cacheable <> erstellen, mit der Sie Daten verwalten können, die vom HTTP-Server mit mehreren Abonnenten abgerufen wurden:

declare type GetDataHandler<T> = () => Observable<T>;

export class Cacheable<T> {

    protected data: T;
    protected subjectData: Subject<T>;
    protected observableData: Observable<T>;
    public getHandler: GetDataHandler<T>;

    constructor() {
      this.subjectData = new ReplaySubject(1);
      this.observableData = this.subjectData.asObservable();
    }

    public getData(): Observable<T> {
      if (!this.getHandler) {
        throw new Error("getHandler is not defined");
      }
      if (!this.data) {
        this.getHandler().map((r: T) => {
          this.data = r;
          return r;
        }).subscribe(
          result => this.subjectData.next(result),
          err => this.subjectData.error(err)
        );
      }
      return this.observableData;
    }

    public resetCache(): void {
      this.data = null;
    }

    public refresh(): void {
      this.resetCache();
      this.getData();
    }

}

Verwendung

Deklarieren Sie das zwischenspeicherbare <> Objekt (vermutlich als Teil des Dienstes):

list: Cacheable<string> = new Cacheable<string>();

und Handler:

this.list.getHandler = () => {
// get data from server
return this.http.get(url)
.map((r: Response) => r.json() as string[]);
}

Aufruf von einer Komponente:

//gets data from server
List.getData().subscribe(…)

Sie können mehrere Komponenten abonnieren lassen.

Weitere Details und ein Codebeispiel finden Sie hier: http://devinstance.net/articles/20171021/rxjs-cacheable


0

Sie könnten einfach ngx-cacheable verwenden ! Es passt besser zu Ihrem Szenario.

Der Vorteil davon

  • Es ruft die Rest-API nur einmal auf, speichert die Antwort zwischen und gibt sie für folgende Anforderungen zurück.
  • Kann die API nach dem Erstellen / Aktualisieren / Löschen nach Bedarf aufrufen.

Ihre Serviceklasse wäre also ungefähr so ​​-

import { Injectable } from '@angular/core';
import { Cacheable, CacheBuster } from 'ngx-cacheable';

const customerNotifier = new Subject();

@Injectable()
export class customersService {

    // relieves all its caches when any new value is emitted in the stream using notifier
    @Cacheable({
        cacheBusterObserver: customerNotifier,
        async: true
    })
    getCustomer() {
        return this.http.get('/someUrl').map(res => res.json());
    }

    // notifies the observer to refresh the data
    @CacheBuster({
        cacheBusterNotifier: customerNotifier
    })
    addCustomer() {
        // some code
    }

    // notifies the observer to refresh the data
    @CacheBuster({
        cacheBusterNotifier: customerNotifier
    })
    updateCustomer() {
        // some code
    }
}

Hier ist der Link für weitere Informationen.


-4

Haben Sie versucht, den bereits vorhandenen Code auszuführen?

Da Sie das Observable aus dem daraus resultierenden Versprechen erstellen getJSON(), wird die Netzwerkanforderung gestellt, bevor sich jemand anmeldet. Das daraus resultierende Versprechen wird von allen Abonnenten geteilt.

var promise = jQuery.getJSON(requestUrl); // network call is executed now
var o = Rx.Observable.fromPromise(promise); // just wraps it in an observable
o.subscribe(...); // does not trigger network call
o.subscribe(...); // does not trigger network call
// ...

Ich habe die Frage bearbeitet, um sie Angular 2 spezifisch zu machen
Angular University
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.