Qual è il modo corretto di condividere il risultato di una chiamata di rete Http angolare in RxJs 5?


303

Utilizzando Http, chiamiamo un metodo che esegue una chiamata di rete e restituisce un http osservabile:

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

Se prendiamo questo osservabile e aggiungiamo più abbonati ad esso:

let network$ = getCustomer();

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

Quello che vogliamo fare è garantire che ciò non causi più richieste di rete.

Questo potrebbe sembrare uno scenario insolito, ma in realtà è abbastanza comune: ad esempio se il chiamante si abbona all'osservabile per visualizzare un messaggio di errore e lo passa al modello utilizzando la pipe asincrona, abbiamo già due abbonati.

Qual è il modo corretto di farlo in RxJs 5?

Vale a dire, questo sembra funzionare bene:

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

Ma è questo il modo idiomatico di farlo in RxJs 5, o dovremmo invece fare qualcos'altro?

Nota: come da Angular 5 nuovo HttpClient, la .map(res => res.json())parte in tutti gli esempi è ora inutile, poiché il risultato JSON è ora assunto per impostazione predefinita.


1
> share è identico a publishing (). refCount (). In realtà non lo è. Vedi la seguente discussione: github.com/ReactiveX/rxjs/issues/1363
Christian

1
domanda modificata, in base al problema sembra che i documenti sul codice debbano essere aggiornati -> github.com/ReactiveX/rxjs/blob/master/src/operator/share.ts
Angular University

Penso che "dipende". Ma per le chiamate in cui non è possibile memorizzare nella cache i dati localmente in b / c, potrebbe non avere senso a causa della modifica / combinazione dei parametri .share () sembra assolutamente la cosa giusta. Ma se riesci a memorizzare nella cache le cose localmente, alcune delle altre risposte riguardanti ReplaySubject / BehaviorSubject sono anche buone soluzioni.
JimB,

Penso che non solo abbiamo bisogno di memorizzare nella cache i dati, ma abbiamo anche bisogno di aggiornare / modificare i dati memorizzati nella cache. È un caso comune. Ad esempio, se voglio aggiungere un nuovo campo al modello memorizzato nella cache o aggiornare il valore del campo. Forse creare un DataCacheService singleton con il metodo CRUD è un modo migliore? Come il negozio di Redux . Cosa ne pensi?
slideshowp2

Potresti semplicemente usare ngx-cacheable ! Si adatta meglio al tuo scenario. Di seguito la mia risposta
Tushar Walzade del

Risposte:


230

Memorizza nella cache i dati e, se disponibili, memorizza nella cache, restituiscilo altrimenti effettua la richiesta HTTP.

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

Esempio di Plunker

Questo articolo https://blog.thoughtram.io/angular/2018/03/05/advanced-caching-with-rxjs.html è un'ottima spiegazione su come memorizzare nella cache shareReplay.


3
do()al contrario map(), non modifica l'evento. È possibile utilizzare map()anche ma è necessario assicurarsi che venga restituito il valore corretto alla fine della richiamata.
Günter Zöchbauer,

3
Se il sito di chiamata che fa il .subscribe()non ha bisogno del valore, puoi farlo perché potrebbe ottenere solo null(a seconda di cosa this.extractDataritorna), ma IMHO questo non esprime bene l'intento del codice.
Günter Zöchbauer,

2
Quando this.extraDatafinisce come extraData() { if(foo) { doSomething();}}altrimenti viene restituito il risultato dell'ultima espressione che potrebbe non essere quello desiderato.
Günter Zöchbauer,

9
@ Günter, grazie per il codice, funziona. Tuttavia, sto cercando di capire perché stai tenendo traccia di Dati e Osservabili separatamente. Non otterresti effettivamente lo stesso effetto memorizzando nella cache solo <Data> osservabile come questo? if (this.observable) { return this.observable; } else { this.observable = this.http.get(url) .map(res => res.json().data); return this.observable; }
Luglio.

3
@HarleenKaur È una classe in cui viene deserializzato il JSON ricevuto, per ottenere un forte controllo del tipo e il completamento automatico. Non è necessario usarlo, ma è comune.
Günter Zöchbauer,

44

Secondo il suggerimento di @Cristian, questo è un modo che funziona bene con gli osservabili HTTP, che emettono solo una volta e poi completano:

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

Esistono un paio di problemi con l'utilizzo di questo approccio: l'osservabile restituito non può essere annullato o riprovato. Questo potrebbe non essere un problema per te, ma potrebbe esserlo ancora. Se questo è un problema, l' shareoperatore potrebbe essere una scelta ragionevole (anche se con alcuni casi limite). Per una discussione approfondita sulle opzioni, consultare la sezione commenti in questo post del blog: blog.jhades.org/…
Christian

1
Piccolo chiarimento ... Anche se publishLast().refCount()non è possibile annullare rigorosamente la fonte osservabile condivisa da , una volta annullati tutti gli abbonamenti all'osservabile restituiti refCount, l'effetto netto è che la fonte osservabile sarà annullata, annullandola se "in volo"
Christian

@Christian Ehi, puoi spiegare cosa intendi dicendo "impossibile annullare o riprovare"? Grazie.
indefinito il

37

AGGIORNAMENTO: Ben Lesh dice che la prossima versione minore dopo la 5.2.0, sarai in grado di chiamare shareReplay () per veramente cache.

IN PRECEDENZA.....

In primo luogo, non utilizzare share () o publishingReplay (1) .refCount (), sono gli stessi e il problema è che condivide solo se vengono effettuate connessioni mentre l'osservabile è attivo, se ci si connette dopo il completamento , crea di nuovo un nuovo osservabile, traduzione, non proprio cache.

Birowski ha dato la soluzione giusta sopra, che è usare ReplaySubject. ReplaySubject memorizzerà nella cache i valori forniti (bufferSize) nel nostro caso 1. Non creerà una nuova osservabile come share () una volta che refCount raggiunge lo zero e si crea una nuova connessione, che è il comportamento corretto per la memorizzazione nella cache.

Ecco una funzione riutilizzabile

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

Ecco come usarlo

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

Di seguito è riportata una versione più avanzata della funzione memorizzabile nella cache Questa consente una propria tabella di ricerca + la possibilità di fornire una tabella di ricerca personalizzata. In questo modo, non è necessario controllare this._cache come nell'esempio sopra. Si noti inoltre che invece di passare l'osservabile come primo argomento, si passa una funzione che restituisce gli osservabili, questo perché l'Http di Angular viene eseguito immediatamente, quindi restituendo una funzione lazy eseguita, possiamo decidere di non chiamarlo se è già in la nostra 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;
}

Uso:

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

C'è qualche ragione per non utilizzare questa soluzione come operatore RxJs: const data$ = this._http.get('url').pipe(cacheable()); /*1st subscribe*/ data$.subscribe(); /*2nd subscribe*/ data$.subscribe();? Quindi si comporta più come qualsiasi altro operatore ..
Felix,

31

rxjs 5.4.0 ha un nuovo metodo shareReplay .

L'autore dice esplicitamente "ideale per gestire cose come la memorizzazione nella cache dei risultati AJAX"

rxjs PR # 2443 feat (shareReplay): aggiunge la shareReplayvariante dipublishReplay

shareReplay restituisce un osservabile che è la sorgente multicast su un oggetto Replay. Quell'oggetto di replay viene riciclato per errore dalla fonte, ma non al completamento della fonte. Questo rende shareReplay ideale per gestire cose come la memorizzazione nella cache dei risultati AJAX, poiché è riprovabile. Il suo comportamento di ripetizione, tuttavia, differisce dalla condivisione in quanto non ripeterà la fonte osservabile, piuttosto ripeterà i valori della fonte osservabile.


È legato a questo? Questi documenti sono del 2014 però. github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/…
Aaron Hoffman,

4
Ho provato ad aggiungere .shareReplay (1, 10000) a un osservabile ma non ho notato alcun cambiamento nella cache o nel comportamento. È disponibile un esempio funzionante?
Aydus-Matthew,

Guardando il log delle modifiche github.com/ReactiveX/rxjs/blob/… È apparso prima, è stato rimosso in v5, aggiunto di nuovo in 5.4 - quel collegamento rx-book fa riferimento a v4, ma esiste nell'attuale LTS v5.5.6 e è in v6. Immagino che il link al libro rx non sia aggiornato.
Jason Awbrey,

25

secondo questo articolo

Si scopre che possiamo facilmente aggiungere la cache all'osservabile aggiungendo publishingReplay (1) e refCount.

così dentro se solo le istruzioni aggiungono

.publishReplay(1)
.refCount();

per .map(...)


11

rxjs versione 5.4.0 (2017-05-09) aggiunge il supporto per shareReplay .

Perché usare shareReplay?

In genere si desidera utilizzare shareReplay quando si hanno effetti collaterali o calcoli fiscali che non si desidera eseguire tra più abbonati. Può anche essere utile in situazioni in cui sai che avrai abbonati in ritardo a uno stream che necessita dell'accesso a valori precedentemente emessi. Questa capacità di riprodurre valori su abbonamento è ciò che differenzia share e shareReplay.

È possibile modificare facilmente un servizio angolare per utilizzarlo e restituire un osservabile con un risultato memorizzato nella cache che renderà la chiamata http solo una volta sola (supponendo che la prima chiamata abbia avuto esito positivo).

Esempio di servizio angolare

Ecco un servizio clienti molto semplice che utilizza 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 */
}

Si noti che l'assegnazione nel costruttore potrebbe essere spostata nel metodo getCustomersma poiché gli osservabili restituiti HttpClientsono "freddi", farlo nel costruttore è accettabile in quanto la chiamata http verrà effettuata solo con la prima chiamata a subscribe.

Anche in questo caso si presuppone che i dati restituiti iniziali non diventino obsoleti nella durata dell'istanza dell'applicazione.


Mi piace molto questo modello e sto cercando di implementarlo all'interno di una libreria condivisa di servizi API che utilizzo in diverse applicazioni. Un esempio è un UserService e ovunque, tranne un paio di posizioni, non è necessario invalidare la cache durante la vita dell'app, ma in quei casi, come potrei fare per invalidarla senza far diventare orfani gli abbonamenti precedenti?
SirTophamHatt,

10

Ho recitato la domanda, ma cercherò di provarlo.

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

Ecco la prova :)

C'è solo uno da asporto: getCustomer().subscribe(customer$)

Non stiamo sottoscrivendo la risposta API di getCustomer(), stiamo sottoscrivendo un ReplaySubject che è osservabile che è anche in grado di abbonarsi a un Osservabile diverso e (e questo è importante) detenere il suo ultimo valore emesso e ripubblicarlo in uno qualsiasi dei suoi (ReplaySubject's ) iscritti.


1
Mi piace questo approccio in quanto fa buon uso di rxjs e non è necessario aggiungere una logica personalizzata, grazie
Thibs

7

Ho trovato un modo per archiviare il risultato http get in sessionStorage e utilizzarlo per la sessione, in modo che non chiamerà mai più il server.

L'ho usato per chiamare l'API github per evitare limiti di utilizzo.

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

Cordiali saluti, il limite di sessionStorage è 5M (o 4.75M). Quindi, non dovrebbe essere usato in questo modo per un ampio set di dati.

------ edit -------------
Se vuoi avere dati aggiornati con F5, che utilizza i dati di memoria invece di sessionStorage;

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

Se effettuerai l'archiviazione nella sessione Archiviazione, come ti assicurerai che l'archiviazione della sessione venga distrutta quando lasci l'app?
Bavaglio

ma ciò introduce comportamenti imprevisti per l'utente. Quando l'utente preme F5 o il pulsante di aggiornamento del browser, si aspetta nuovi dati dal server. Ma in realtà sta ottenendo dati obsoleti da localStorage. Segnalazioni di bug, ticket di supporto, ecc. In arrivo ... Come sessionStoragedice il nome , lo userei solo per i dati che dovrebbero essere coerenti per l'intera sessione.
Martin Schneider,

@ MA-Maddin come ho detto "L'ho usato per evitare il limite di utilizzo". Se si desidera aggiornare i dati con F5, è necessario utilizzare la memoria anziché sessionStorage. La risposta è stata modificata con questo approccio.
Allenhwkim,

sì, potrebbe essere un caso d'uso. Mi sono appena attivato dal momento che tutti parlano di Cache e OP getCustomernel suo esempio. ;) Quindi volevo solo avvertire alcuni ppl che potrebbero non vedere i rischi :)
Martin Schneider,

5

L'implementazione scelta dipenderà se desideri annullare l'iscrizione o meno alla tua richiesta HTTP.

In ogni caso, i decoratori TypeScript sono un bel modo di standardizzare il comportamento. Questo è quello che ho scritto:

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

Definizione del decoratore:

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

Ciao @Arlo - l'esempio sopra non viene compilato. Property 'connect' does not exist on type '{}'.dalla linea returnValue.connect();. Puoi elaborare?
Zoccolo

4

Dati di risposta HTTP memorizzabili nella cache utilizzando Rxjs Observer / Observable + Caching + Abbonamento

Vedi il codice sotto

* disclaimer: sono un nuovo utente di rxjs, quindi tieni presente che potrei usare impropriamente l'approccio osservabile / osservatore. La mia soluzione è puramente una conglomerazione di altre soluzioni che ho trovato ed è la conseguenza di non aver trovato una soluzione semplice e ben documentata. Quindi sto fornendo la mia soluzione di codice completa (come mi sarebbe piaciuto aver trovato) nella speranza che aiuti gli altri.

* nota, questo approccio è vagamente basato su GoogleFirebaseObservables. Purtroppo mi manca la giusta esperienza / tempo per replicare ciò che hanno fatto sotto il cofano. Ma il seguente è un modo semplicistico di fornire accesso asincrono ad alcuni dati in grado di memorizzare nella cache.

Situazione : un componente "elenco prodotti" ha il compito di visualizzare un elenco di prodotti. Il sito è un'app Web a pagina singola con alcuni pulsanti di menu che "filtrano" i prodotti visualizzati nella pagina.

Soluzione : il componente "sottoscrive" a un metodo di servizio. Il metodo di servizio restituisce una matrice di oggetti prodotto a cui il componente accede tramite il callback dell'abbonamento. Il metodo di servizio avvolge la sua attività in un osservatore appena creato e restituisce l'osservatore. All'interno di questo osservatore, cerca i dati memorizzati nella cache e li restituisce al sottoscrittore (il componente) e ritorna. Altrimenti emette una chiamata http per recuperare i dati, si abbona alla risposta, dove è possibile elaborare tali dati (ad es. Mappare i dati sul proprio modello) e quindi restituire i dati al sottoscrittore.

Il codice

prodotto-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 (il modello)

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

Ecco un esempio dell'output che vedo quando carico la pagina in Chrome. Si noti che al caricamento iniziale, i prodotti vengono recuperati da http (chiamare il mio servizio di resto del nodo, che è in esecuzione localmente sulla porta 3000). Quando faccio clic su per passare a una vista "filtrata" dei prodotti, i prodotti vengono trovati nella cache.

Il mio Chrome Log (console):

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

... [fatto clic su un pulsante del menu per filtrare i prodotti] ...

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

Conclusione: questo è il modo più semplice che ho trovato (finora) per implementare i dati di risposta http memorizzabili nella cache. Nella mia app angolare, ogni volta che passo a una diversa visualizzazione dei prodotti, il componente dell'elenco prodotti viene ricaricato. ProductService sembra essere un'istanza condivisa, quindi la cache locale di 'prodotti: Product []' in ProductService viene mantenuta durante la navigazione e le successive chiamate a "GetProducts ()" restituiscono il valore memorizzato nella cache. Un'ultima nota, ho letto i commenti su come osservabili / abbonamenti devono essere chiusi quando hai finito per prevenire "perdite di memoria". Non l'ho incluso qui, ma è qualcosa da tenere a mente.


1
Nota: da allora ho trovato una soluzione più potente, che coinvolge RxJS BehaviorSubjects, che semplifica il codice e riduce drasticamente il "sovraccarico". In products.service.ts, 1. importare {BehaviorSubject} da 'rxjs'; 2. cambia 'prodotti: Prodotto []' in 'prodotto $: BehaviorSubject <Prodotto []> = new BehaviorSubject <Prodotto []> ([]);' 3. Ora puoi semplicemente chiamare http senza restituire nulla. http_getProducts () {this.http.get (...). map (res => res.json ()). iscriviti (products => this.product $ .next (products))};
Obiettivo

1
La variabile locale 'product $' è un oggetto del comportamento, che EMIT e STORE saranno entrambi gli ultimi prodotti (dal prodotto $ .next (..) chiameranno nella parte 3). Ora nei componenti, iniettare il servizio normalmente. Ottieni il valore assegnato più recentemente del prodotto $ utilizzando productService.product $ .value. Oppure iscriviti al prodotto $ se desideri eseguire un'azione ogni volta che il prodotto $ riceve un nuovo valore (ovvero, la funzione $ .next (...) del prodotto viene chiamata nella parte 3).
Obiettivo

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

1
Una nota importante sull'annullamento dell'iscrizione agli osservabili: ".takeUntil (this.ngUnsubscribe)". Vedi questa domanda / risposta di overflow dello stack, che sembra mostrare il modo raccomandato di fatto per annullare l'iscrizione agli eventi: stackoverflow.com/questions/38008334/…
ObjectiveTC

1
L'alternativa è .first () o .take (1) se l'osservabile è destinato a ricevere dati una sola volta. Tutti gli altri "flussi infiniti" di osservabili dovrebbero essere annullati in "ngOnDestroy ()" e, in caso contrario, si potrebbe finire con callback duplicati "osservabili". stackoverflow.com/questions/28007777/…
ObjectiveTC

3

Suppongo che @ NGX-cache / core potrebbe essere utile per mantenere la funzionalità di caching per le chiamate HTTP, soprattutto se la chiamata HTTP viene effettuata sia sul navigatore e server di piattaforme.

Diciamo che abbiamo il seguente metodo:

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

È possibile utilizzare il Cacheddecoratore di @ NGX-cache / core per memorizzare il valore restituito dal metodo che effettua la chiamata HTTP al cache storage( la storagepuò essere configurabile, si prega di controllare l'attuazione a ng-seme / Universal ) - proprio sulla prima esecuzione. Al successivo richiamo del metodo (indipendentemente dal browser o dalla piattaforma del server ), il valore viene recuperato da 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());
}

C'è anche la possibilità di metodi uso caching ( has, get, set) utilizzando l'API cache .

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

Ecco l'elenco dei pacchetti, sia per la cache lato client che lato server:


1

rxjs 5.3.0

Non sono stato contento .map(myFunction).publishReplay(1).refCount()

Con più abbonati, .map()viene eseguito myFunctiondue volte in alcuni casi (mi aspetto che venga eseguito solo una volta). Una soluzione sembra esserepublishReplay(1).refCount().take(1)

Un'altra cosa che puoi fare è semplicemente non usare refCount()e rendere subito l'Osservabile caldo:

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

Ciò avvierà la richiesta HTTP indipendentemente dagli abbonati. Non sono sicuro che l'annullamento dell'iscrizione prima che HTTP GET finisca lo annullerà o meno.


1

Quello che vogliamo fare è garantire che ciò non causi più richieste di rete.

Il mio preferito è utilizzare asyncmetodi per le chiamate che effettuano richieste di rete. I metodi stessi non restituiscono un valore, ma aggiornano uno BehaviorSubjectall'interno dello stesso servizio, a cui i componenti si abboneranno.

Ora Perché usare un BehaviorSubjectinvece di un Observable? Perché,

  • Al momento della sottoscrizione, BehaviorSubject restituisce l'ultimo valore mentre Un osservabile regolare si attiva solo quando riceve un onnext.
  • Se si desidera recuperare l'ultimo valore di BehaviorSubject in un codice non osservabile (senza abbonamento), è possibile utilizzare il getValue()metodo

Esempio:

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

Quindi, ovunque richiesto, possiamo semplicemente iscriverci customers$.

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

O forse vuoi usarlo direttamente in un modello

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

Quindi, fino a quando non si effettua un'altra chiamata getCustomers, i dati vengono conservati in customers$BehaviorSubject.

Quindi cosa succede se si desidera aggiornare questi dati? basta fare una chiamata agetCustomers()

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

Utilizzando questo metodo, non è necessario conservare esplicitamente i dati tra le chiamate di rete successive poiché vengono gestiti da BehaviorSubject.

PS: di solito quando un componente viene distrutto è una buona pratica sbarazzarsi degli abbonamenti, per questo puoi usare il metodo suggerito in questa risposta.


1

Grandi risposte.

Oppure potresti farlo:

Questo è dall'ultima versione di rxjs. Sto usando la versione 5.5.7 di RxJS

import {share} from "rxjs/operators";

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

0

Chiama share () dopo la mappa e prima di iscriverti .

Nel mio caso, ho un servizio generico (RestClientService.ts) che sta effettuando la chiamata di riposo, estraendo i dati, verificando la presenza di errori e ritornando osservabile a un servizio di implementazione concreto (ad esempio: ContractClientService.ts), infine questa implementazione concreta ritorna osservabile a de ContractComponent.ts e questo si abbona per aggiornare la vista.

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

Ho scritto una classe di cache,

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

}

È tutto statico a causa di come lo usiamo, ma sentiti libero di renderlo una classe normale e un servizio. Non sono sicuro se angolare mantenga una singola istanza per tutto il tempo (nuovo in Angular2).

Ed è così che lo uso:

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

Presumo che ci potrebbe essere un modo più intelligente, che avrebbe usato alcuni Observabletrucchi, ma questo andava bene per i miei scopi.


0

Basta usare questo livello di cache, fa tutto ciò che serve e persino gestire la cache per le richieste Ajax.

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

È molto facile da usare

@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"}]};
}

Lo strato (come servizio angolare iniettabile) è

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

È .publishReplay(1).refCount();o .publishLast().refCount();poiché gli osservabili di Angular Http sono completi dopo la richiesta.

Questa semplice classe memorizza nella cache il risultato in modo che tu possa iscriverti a .value più volte ed effettuare solo 1 richiesta. Puoi anche usare .reload () per effettuare nuove richieste e pubblicare dati.

Puoi usarlo come:

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

e la fonte:

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

Puoi creare una semplice cache <> di classe che aiuta a gestire i dati recuperati dal server http con più abbonati:

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

}

uso

Dichiara oggetto <> memorizzabile nella cache (presumibilmente come parte del servizio):

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

e gestore:

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

Chiama da un componente:

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

È possibile abbonarsi diversi componenti.

Maggiori dettagli ed esempio di codice sono qui: http://devinstance.net/articles/20171021/rxjs-cacheable


0

Potresti semplicemente usare ngx-cacheable ! Si adatta meglio al tuo scenario.

Il vantaggio di usare questo

  • Chiama l'API di riposo una sola volta, memorizza nella cache la risposta e restituisce la stessa per le seguenti richieste.
  • Può chiamare l'API come richiesto dopo l'operazione di creazione / aggiornamento / eliminazione.

Quindi, la tua classe di servizio sarebbe qualcosa del genere -

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
    }
}

Ecco il link per ulteriori riferimenti.


-4

Hai provato a eseguire il codice che hai già?

Poiché stai costruendo l'Osservabile dalla promessa risultante getJSON(), la richiesta di rete viene effettuata prima che qualcuno si abboni. E la promessa risultante è condivisa da tutti gli abbonati.

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

ho modificato la domanda per renderla specifica Angular 2
Angular University
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.