it-swarm.com.de

Wie kann ich das Ergebnis eines Angular-HTTP-Netzwerkaufrufs in RxJs 5 gemeinsam nutzen?

Mit Http rufen wir eine Methode auf, die einen Netzwerkaufruf durchführt und ein http-Observable zurückgibt:

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

Wenn wir dieses Observable nehmen und mehrere Abonnenten hinzufügen:

let network$ = getCustomer();

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

Was wir tun wollen, ist sicherzustellen, dass dies nicht mehrere Netzwerkanfragen verursacht.

Dies mag wie ein ungewöhnliches Szenario erscheinen, ist jedoch durchaus üblich: Wenn der Anrufer beispielsweise das Beobachtbare abonniert, um eine Fehlermeldung anzuzeigen, und es mithilfe der asynchronen Pipe an die Vorlage übermittelt, haben wir bereits zwei Abonnenten.

Was ist der richtige Weg, um dies in RxJs 5 zu tun? 

Das scheint nämlich gut zu funktionieren:

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

Aber ist das der idiomatische Weg, dies in RxJs 5 zu tun, oder sollten wir stattdessen etwas anderes tun? 

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

246

Zwischenspeichern Sie die Daten und geben Sie, falls verfügbar, zwischengespeichert, andernfalls die HTTP-Anforderung ab.

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.thoughtram.io/angular/2018/03/05/advanced-caching-with-rxjs.html ist eine großartige Erklärung zum Zwischenspeichern mit shareReplay.

202

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

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

UPDATE: Ben Lesh sagt, die nächste Minor-Version nach 5.2.0. Sie können einfach shareReplay () aufrufen, um wirklich zwischenzuspeichern.

VORHER.....

Verwenden Sie zunächst nicht share () oder publishReplay (1) .refCount (), da sie identisch sind und das Problem dabei ist, dass es nur dann freigegeben wird, wenn Verbindungen hergestellt werden, während das Observable aktiv ist, wenn Sie nach Abschluss eine Verbindung herstellen , erstellt es ein neues Observable, Übersetzung, nicht wirklich Caching.

Birowski hat oben die richtige Lösung angegeben, nämlich ReplaySubject. ReplaySubject speichert die von Ihnen übergebenen Werte (bufferSize) in unserem Fall 1. Es wird kein neues Observable wie share () erstellt, wenn refCount den Wert Null erreicht und Sie eine neue Verbindung herstellen. 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();
}

So verwenden Sie es

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'));
  }
}

Nachfolgend 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 des Beobachtbaren als erstes Argument eine Funktion übergeben, die die Observablen zurückgibt. Dies ist darauf zurückzuführen, dass Angulars Http sofort ausgeführt wird. Wenn Sie also eine verzögert ausgeführte Funktion zurückgeben, können Sie sie nicht aufrufen, wenn sie bereits in 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")
30

demnach Artikel

Es hat sich herausgestellt, dass wir das beobachtbare Objekt durch Hinzufügen von publishReplay (1) und refCount leicht zwischenspeichern können.

also innerhalb von if-Anweisungen einfach anhängen

.publishReplay(1)
.refCount();

zu .map(...)

23
Ivan

rxjs 5.4.0 hat eine neue shareReplay -Methode.

Der Autor sagt ausdrücklich "ideal für die Handhabung von Ergebnissen wie AJAX - Ergebnisse"

rxjs PR # 2443 feat (shareReplay): fügt shareReplay variante von publishReplay hinzu

shareReplay gibt ein Observable zurück, bei dem die Quelle über .__ multicasted ist. ein ReplaySubject. Dieses Betreff der Wiederholung wird bei einem Fehler von .__ wiederaufbereitet. Quelle, aber nicht nach Abschluss der Quelle. Dies macht shareReplay Ideal für die Handhabung von Ergebnissen wie das Zwischenspeichern von AJAX - Ergebnissen, da es .__ ist. wiederauffindbar. Es ist Wiederholungsverhalten, unterscheidet sich jedoch von teilen, dass es wird die beobachtbare Quelle nicht wiederholen, vielmehr wird .__ wiederholt. Werte der beobachtbaren Quelle.

22
Arlo

Ich hatte die Frage in der Hauptrolle, aber ich werde es 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 Antwort des API von getCustomer(), wir abonnieren ein ReplaySubject, das beobachtbar ist, das auch ein anderes Observable abonnieren kann, und (und das ist wichtig), den zuletzt ausgegebenen Wert halten und erneut veröffentlichen Abonnenten von ReplaySubject).

7
Birowsky

Die von Ihnen gewählte Implementierung hängt davon ab, ob Sie Ihre HTTP-Anforderung durch unsubscribe () abbrechen möchten oder nicht.

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

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

Dekorateurdefinition:

/**
 * 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;
}
4
Arlo

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

Warum shareReplay verwenden?

Im Allgemeinen möchten Sie shareReplay verwenden, wenn Sie Nebenwirkungen haben oder Berechnungen besteuern, die nicht unter mehreren Abonnenten ausgeführt werden sollen. Dies kann auch in Situationen nützlich sein, in denen Sie wissen, dass Sie verspätete Abonnenten für einen Stream haben, die Zugriff auf zuvor ausgegebene Werte benötigen. Diese Möglichkeit, Werte bei einem Abonnement wiederzugeben, unterscheidet share und shareReplay.

Sie können leicht einen angular service ändern, um diesen zu verwenden, und eine beobachtbare Datei mit einem zwischengespeicherten Ergebnis zurückgeben, die den http-Aufruf immer nur ein einziges Mal ausführt (vorausgesetzt, der erste Aufruf war erfolgreich).

Beispiel Angular Service

Hier ist ein sehr einfacher Kundenservice, der shareReplay verwendet.

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 getCustomers verschoben werden kann, aber die von HttpClient zurückgegebenen Observablen "kalt" sind Dies im Konstruktor zu tun ist akzeptabel, da der http-Aufruf nur beim ersten Aufruf von subscribe erfolgt.

Auch hier wird davon ausgegangen, dass die ursprünglich zurückgegebenen Daten in der Lebensdauer der Anwendungsinstanz nicht veraltet sind.

4
Igor

Ich habe einen Weg gefunden, das http-get-Ergebnis in sessionStorage zu speichern und für die Sitzung zu verwenden, sodass es den Server nie mehr anruft.

Ich habe es benutzt, um die github-API aufzurufen, um das Nutzungslimit 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, sessionStorage ist 5M (oder 4.75M). Daher sollte es nicht für große Datenmengen verwendet werden.

------ edit -------------
Wenn Sie Daten mit F5 aktualisieren möchten, werden anstelle von sessionStorage Speicherdaten 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();
        });
    }
  }
}
4
allenhwkim

Cachefähige HTTP-Antwortdaten mit Rxjs Observer/Observable + Caching + Subscription

Siehe Code unten

* disclaimer: Ich bin neu bei rxjs, bedenke also, dass ich den beobachtbaren/beobachtenden Ansatz missbrauchen könnte. Meine Lösung besteht aus einer Ansammlung anderer Lösungen, die ich gefunden habe, und ist die Folge, 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, 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 asynchronen Zugriff auf einige Cache-fähige Daten bereitzustellen.

Situation : Eine 'Produktliste'-Komponente wird mit der Anzeige einer Produktliste beauftragt. 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 das die Komponente über den Abonnementrückruf zugreift. Die Servicemethode fasst ihre Aktivitäten in einem neu erstellten Observer zusammen und gibt den Observer zurück. Innerhalb dieses Beobachters sucht er nach zwischengespeicherten Daten, gibt sie an den Abonnenten (die Komponente) zurück und kehrt zurück. Andernfalls gibt es einen http-Aufruf aus, um die Daten abzurufen, abonniert die Antwort, wobei Sie diese Daten verarbeiten können (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 sehe, wenn ich die Seite in Chrome lade. Beachten Sie, dass die Produkte beim ersten Laden von http abgerufen werden (Aufruf an meinen Node Rest Service, der lokal auf Port 3000 ausgeführt wird). Wenn ich dann klicken, 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

... [auf eine Menüschaltfläche geklickt, 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 bisher einfachste Weg, um zwischengespeicherte http-Antwortdaten zu implementieren. Jedes Mal, wenn ich zu einer anderen Ansicht der Produkte navigiere, wird die Produktlistenkomponente neu geladen. ProductService scheint eine gemeinsam genutzte Instanz zu sein, daher wird der lokale Cache von 'products: Product []' im ProductService während der Navigation beibehalten, und nachfolgende Aufrufe von "GetProducts ()" geben den zwischengespeicherten Wert zurück. Eine letzte Anmerkung, ich habe Kommentare darüber gelesen, wie Observables/Abonnements geschlossen werden müssen, wenn Sie fertig sind, um Speicherlecks zu vermeiden. Ich habe das hier nicht aufgenommen, aber es ist etwas zu bedenken.

3
ObjectiveTC

Ich gehe davon aus, dass @ ngx-cache/core nützlich sein kann, um die Cachefunktionen für die http-Aufrufe aufrechtzuerhalten, insbesondere wenn der HTTP-Aufruf sowohl auf browser - als auch auf server -Plattform erfolgt.

Nehmen wir an, wir haben die folgende Methode:

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

Sie können den Cached-Dekorator von @ ngx-cache/core verwenden, um den zurückgegebenen Wert der Methode zu speichern, die den HTTP-Aufruf am cache storage ausführt (die storage kann konfiguriert werden, überprüfen Sie die Implementierung unter ng.) -seed/universal) - direkt bei der ersten Ausführung. Beim nächsten Aufruf der Methode (unabhängig von browser oder server - Plattform) wird der Wert vom cache storage abgerufen.

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, Caching-Methoden (has, get, set) mithilfe der caching-API zu verwenden.

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 sind die Liste der Pakete, sowohl für das clientseitige als auch für das serverseitige Caching:

2
Burak Tasci

Tolle Antworten.

Oder Sie könnten das tun:

Dies ist von der neuesten Version von RXJS. Ich benutze 5.5.7 Version von RxJS

import {share} from "rxjs/operators";

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

rxjs 5.3.0

Ich war mit .map(myFunction).publishReplay(1).refCount() nicht zufrieden

Bei mehreren Abonnenten führt .map() in einigen Fällen myFunction zweimal aus (ich erwarte, dass es nur einmal ausgeführt wird). Ein Fix scheint publishReplay(1).refCount().take(1) zu sein.

Eine andere Sache, die Sie tun können, ist refCount() einfach nicht zu verwenden und das Observable sofort 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 Abonnenten gestartet. Ich bin mir nicht sicher, ob das Abbestellen vor dem Ende von HTTP GET dies abbricht oder nicht.

1
Arlo

Was wir tun wollen, ist sicherzustellen, dass dies nicht mehrere Netzwerkanfragen verursacht.

Mein persönlicher Favorit ist die Verwendung der async-Methode für Anrufe, die Netzwerkanfragen stellen. Die Methoden selbst geben keinen Wert zurück, sondern aktualisieren ein BehaviorSubject innerhalb desselben Diensts, dessen Komponenten abonniert werden.

Warum sollte ein BehaviorSubject anstelle eines Observable verwendet werden? Weil, 

  • Bei der Subskription gibt BehaviorSubject den letzten Wert zurück, während A regular observable nur ausgelöst wird, wenn es ein onnext empfängt.
  • Wenn Sie den letzten Wert des BehaviorSubject in einem nicht beobachtbaren Code (ohne Subskription) 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);
}

Wo immer dies erforderlich ist, können wir customers$ abonnieren.

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

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

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

Bis zum nächsten Aufruf von getCustomers bleiben die Daten im customers$-Verhaltenssubjekt erhalten.

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

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

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

PS: Wenn eine Komponente zerstört wird, ist es in der Regel ratsam, die Abonnements zu löschen. Dafür können Sie die in this answer vorgeschlagene Methode verwenden.

1
cyberpirate92

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
Ravinder Payal

Sie können eine einfache Klasse Cacheable <> erstellen, die die Verwaltung von Daten unterstützt, die mit mehreren Abonnenten vom http-Server abgerufen werden:

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();
    }

}

Verwendungszweck

Cacheable <> -Objekt deklarieren (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[]);
}

Anruf von einer Komponente:

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

Sie können mehrere Komponenten abonniert haben.

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

0
yfranz

Rufen Sie einfach share () nach map und vor subscribe auf.

In meinem Fall habe ich einen generischen Dienst (RestClientService.ts), der den Rest aufruft, Daten extrahiert, auf Fehler prüft und beobachtbar an einen konkreten Implementierungsdienst (z. B. ContractClientService.ts) zurückkehrt gibt beobachtbare an de ContractComponent.ts zurück, und diese abonnieren die Ansicht.

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
surfealokesea

Genau dafür habe ich die Bibliothek ngx-rxcache erstellt.

Schauen Sie sich das an https://github.com/adriandavidbrand/ngx-rxcache und schauen Sie sich ein Funktionsbeispiel an https://stackblitz.com/edit/angular-jxqaiv

0
Adrian Brand

Es ist .publishReplay(1).refCount(); oder .publishLast().refCount();, da die Beobachtung der Angular-Http-Observables auf Anfrage abgeschlossen ist.

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

Sie können es wie folgt verwenden:

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
Matjaz Hirsman

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 fühlen Sie sich frei, es zu einer normalen Klasse und einem Service zu machen. Ich bin mir nicht sicher, ob Angle die ganze Zeit über eine Instanz hält (neu bei 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 vermute, es könnte einen klügeren Weg geben, der einige Observable-Tricks verwenden würde, aber das war gut für meine Zwecke.

0
Ondra Žižka

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

Der Vorteil dieser Verwendung

  • Es ruft die rest-API nur einmal auf, speichert die Antwort im Cache und gibt dieselbe für nachfolgende Anforderungen zurück.
  • Kann API nach dem Erstellen/Aktualisieren/Löschen nach Bedarf aufrufen.

So wäre Ihr Serviceklasse so ähnlich -

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 Referenz.

0
Tushar Walzade