Quelle est la bonne façon de partager le résultat d'un appel réseau Angular Http dans RxJs 5?

303

En utilisant Http, nous appelons une méthode qui effectue un appel réseau et renvoie un http observable:

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

Si nous prenons cela observable et y ajoutons plusieurs abonnés:

let network$ = getCustomer();

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

Ce que nous voulons faire, c'est nous assurer que cela ne provoque pas de multiples requêtes réseau.

Cela peut sembler inhabituel, mais c'est en fait assez courant: par exemple, si l'appelant s'abonne à l'observable pour afficher un message d'erreur et le transmet au modèle à l'aide du canal asynchrone, nous avons déjà deux abonnés.

Quelle est la bonne façon de procéder dans RxJs 5?

À savoir, cela semble bien fonctionner:

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

Mais est-ce la façon idiomatique de le faire dans RxJs 5, ou devrions-nous faire autre chose à la place?

Remarque: Conformément à Angular 5 new HttpClient, la .map(res => res.json())pièce dans tous les exemples est désormais inutile, car le résultat JSON est désormais supposé par défaut.

Université angulaire
la source
1
> share est identique à publish (). refCount (). En fait, ce n'est pas le cas. Voir la discussion suivante: github.com/ReactiveX/rxjs/issues/1363
Christian
1
question modifiée, selon le problème, les documents sur le code doivent être mis à jour -> github.com/ReactiveX/rxjs/blob/master/src/operator/share.ts
Angular University
Je pense que ça dépend. Mais pour les appels où vous ne pouvez pas mettre en cache les données localement b / c, cela pourrait ne pas avoir de sens en raison des changements de paramètres / combinaisons .share () semble être absolument la bonne chose. Mais si vous pouvez mettre les choses en cache localement, certaines des autres réponses concernant ReplaySubject / BehaviorSubject sont également de bonnes solutions.
JimB
Je pense que non seulement nous avons besoin de mettre en cache les données, nous avons également besoin de mettre à jour / modifier les données mises en cache. C'est un cas courant. Par exemple, si je veux ajouter un nouveau champ au modèle mis en cache ou mettre à jour la valeur du champ. Peut-être que créer un DataCacheService singleton avec la méthode CRUD est une meilleure façon? Comme magasin de Redux . Qu'est-ce que tu penses?
slideshowp2
Vous pouvez simplement utiliser ngx-cacheable ! Cela convient mieux à votre scénario. Reportez ma réponse ci
Tushar Walzade

Réponses:

230

Mettez les données en cache et, si elles sont disponibles en cache, renvoyez-les sinon faites la requête 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;
    }
  }
}

Exemple Plunker

Cet article https://blog.oughttram.io/angular/2018/03/05/advanced-caching-with-rxjs.html est une excellente explication sur la façon de mettre en cache avec shareReplay.

Günter Zöchbauer
la source
3
do()contrairement à map()ne modifie pas l'événement. Vous pouvez également l'utiliser map(), mais vous devez ensuite vous assurer que la valeur correcte est renvoyée à la fin du rappel.
Günter Zöchbauer du
3
Si le site d'appel qui le fait .subscribe()n'a pas besoin de la valeur, vous pouvez le faire car cela pourrait être juste null(en fonction de ce qui this.extractDataretourne), mais à mon humble avis, cela n'exprime pas bien l'intention du code.
Günter Zöchbauer
2
Lorsque this.extraDatase termine comme extraData() { if(foo) { doSomething();}}autrement, le résultat de la dernière expression est renvoyé, ce qui pourrait ne pas être ce que vous voulez.
Günter Zöchbauer
9
@ Günter, merci pour le code, ça marche. Cependant, j'essaie de comprendre pourquoi vous gardez une trace des données et observables séparément. Souhaitez-vous pas effectivement obtenir le même effet en mettant en cache juste <Data> observable comme ça? if (this.observable) { return this.observable; } else { this.observable = this.http.get(url) .map(res => res.json().data); return this.observable; }
July.Tech
3
@HarleenKaur C'est une classe vers laquelle le JSON reçu est désérialisé, pour obtenir une vérification de type et une autocomplétion solides. Il n'est pas nécessaire de l'utiliser, mais c'est courant.
Günter Zöchbauer
44

Selon la suggestion de @Cristian, c'est une façon qui fonctionne bien pour les observables HTTP, qui n'émettent qu'une seule fois puis se terminent:

getCustomer() {
    return this.http.get('/someUrl')
        .map(res => res.json()).publishLast().refCount();
}
Université angulaire
la source
L'utilisation de cette approche pose quelques problèmes: l'observable renvoyé ne peut pas être annulé ou réessayé. Ce n'est peut-être pas un problème pour vous, mais là encore, cela pourrait l'être. Si c'est un problème, l' shareopérateur peut être un choix raisonnable (bien qu'avec certains cas de bord méchants). Pour une discussion approfondie sur les options, voir la section des commentaires dans cet article de blog: blog.jhades.org/…
Christian
1
Petite précision ... Bien que strictement la source observable partagée par publishLast().refCount()ne puisse pas être annulée, une fois que tous les abonnements à l'observable retourné par refCountont été annulés, l'effet net est que la source observable sera désinscrite, l'annulant si elle était "en vol"
Christian
@Christian Hé, pouvez-vous expliquer ce que vous voulez dire en disant "ne peut pas être annulé ou réessayé"? Merci.
indéfini
37

MISE À JOUR: Ben Lesh dit que la prochaine version mineure après 5.2.0, vous pourrez simplement appeler shareReplay () pour vraiment mettre en cache.

PRÉCÉDEMMENT.....

Tout d'abord, n'utilisez pas share () ou publishReplay (1) .refCount (), ils sont les mêmes et le problème avec cela, c'est qu'il ne partage que si les connexions sont établies alors que l'observable est actif, si vous vous connectez après qu'il se soit terminé , il crée à nouveau un nouvel observable, la traduction, pas vraiment la mise en cache.

Birowski a donné la bonne solution ci-dessus, qui consiste à utiliser ReplaySubject. ReplaySubject mettra en cache les valeurs que vous lui donnez (bufferSize) dans notre cas 1. Il ne créera pas un nouveau observable comme share () une fois que refCount atteint zéro et que vous établissez une nouvelle connexion, ce qui est le bon comportement pour la mise en cache.

Voici une fonction réutilisable

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

Voici comment l'utiliser

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

Vous trouverez ci-dessous une version plus avancée de la fonction pouvant être mise en cache. Celle-ci permet d'avoir sa propre table de recherche + la possibilité de fournir une table de recherche personnalisée. De cette façon, vous n'avez pas à vérifier this._cache comme dans l'exemple ci-dessus. Notez également qu'au lieu de passer l'observable comme premier argument, vous passez une fonction qui renvoie les observables, c'est parce que le Http d'Angular s'exécute immédiatement, donc en retournant une fonction exécutée paresseuse, nous pouvons décider de ne pas l'appeler si elle est déjà dans notre 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;
}

Usage:

getData() => cacheable(this._http.get("YOUR URL"), "this is key for my cache")
Guojian Miguel Wu
la source
Y at - il raison de ne pas utiliser cette solution comme un opérateur RxJs: const data$ = this._http.get('url').pipe(cacheable()); /*1st subscribe*/ data$.subscribe(); /*2nd subscribe*/ data$.subscribe();? Il se comporte donc plus comme n'importe quel autre opérateur ..
Felix
31

rxjs 5.4.0 a une nouvelle méthode shareReplay .

L'auteur dit explicitement "idéal pour gérer des choses comme la mise en cache des résultats AJAX"

rxjs PR # 2443 feat (shareReplay): ajoute une shareReplayvariante depublishReplay

shareReplay renvoie un observable qui est la source multidiffusée sur un ReplaySubject. Ce sujet de relecture est recyclé en cas d'erreur de la source, mais pas à la fin de la source. Cela rend shareReplay idéal pour gérer des choses comme la mise en cache des résultats AJAX, car il est possible de réessayer. Son comportement de répétition, cependant, diffère du partage en ce qu'il ne répétera pas la source observable, il répétera plutôt les valeurs de la source observable.

Arlo
la source
Est-ce lié à cela? Cependant, ces documents datent de 2014. github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/…
Aaron Hoffman
4
J'ai essayé d'ajouter .shareReplay (1, 10000) à un observable mais je n'ai remarqué aucune mise en cache ou changement de comportement. Existe-t-il un exemple de travail disponible?
Aydus-Matthew
En regardant le journal des modifications github.com/ReactiveX/rxjs/blob/… Il est apparu plus tôt, a été supprimé dans la v5, ajouté en 5.4 - ce lien rx-book fait référence à la v4, mais il existe dans la LTS actuelle v5.5.6 et c'est en v6. J'imagine que le lien rx-book est obsolète.
Jason Awbrey
25

selon cet article

Il s'avère que nous pouvons facilement ajouter la mise en cache à l'observable en ajoutant publishReplay (1) et refCount.

donc à l' intérieur si les déclarations ne font qu'ajouter

.publishReplay(1)
.refCount();

à .map(...)

Ivan
la source
11

rxjs version 5.4.0 (2017-05-09) ajoute la prise en charge de shareReplay .

Pourquoi utiliser shareReplay?

Vous souhaitez généralement utiliser shareReplay lorsque vous avez des effets secondaires ou des calculs fiscaux que vous ne souhaitez pas exécuter entre plusieurs abonnés. Il peut également être utile dans les situations où vous savez que vous aurez des abonnés tardifs à un flux qui ont besoin d'accéder aux valeurs précédemment émises. Cette capacité à rejouer les valeurs lors de l'abonnement est ce qui différencie share et shareReplay.

Vous pouvez facilement modifier un service angulaire pour l'utiliser et renvoyer un observable avec un résultat mis en cache qui ne fera que l'appel http une seule fois (en supposant que le premier appel a réussi).

Exemple de service angulaire

Voici un service client très simple qui utilise 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 */
}

Notez que l'affectation dans le constructeur peut être déplacée vers la méthode, getCustomersmais comme les observables retournés HttpClientsont "à froid", le faire dans le constructeur est acceptable car l'appel http ne sera effectué que lors du premier appel à subscribe.

L'hypothèse ici est également que les données retournées initiales ne sont pas périmées pendant la durée de vie de l'instance d'application.

Igor
la source
J'aime vraiment ce modèle et cherche à l'implémenter dans une bibliothèque partagée de services api que j'utilise dans un certain nombre d'applications. Un exemple est un UserService, et partout sauf quelques endroits n'ont pas besoin d'invalider le cache pendant la durée de vie de l'application, mais dans ces cas, comment pourrais-je l'invalider sans que les abonnements précédents ne deviennent orphelins?
SirTophamHatt
10

J'ai mis la question en vedette, mais je vais essayer de l'essayer.

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

Voici la preuve :)

Il n'y a qu'un seul plat à emporter: getCustomer().subscribe(customer$)

Nous ne souscrivons pas à la réponse api de getCustomer(), nous souscrivons à un ReplaySubject qui est observable qui est également capable de s'abonner à un autre Observable et (et c'est important) de conserver sa dernière valeur émise et de la republier à l'un de ses (ReplaySubject's ) les abonnés.

Daniel Birowsky Popeski
la source
1
J'aime cette approche car elle fait bon usage de rxjs et pas besoin d'ajouter une logique personnalisée, merci
Thibs
7

J'ai trouvé un moyen de stocker le résultat http get dans sessionStorage et de l'utiliser pour la session, afin qu'il n'appelle plus jamais le serveur.

Je l'ai utilisé pour appeler l'API github pour éviter la limite d'utilisation.

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

Pour info, la limite de sessionStorage est de 5M (ou 4.75M). Il ne doit donc pas être utilisé comme ceci pour un grand ensemble de données.

------ edit -------------
Si vous voulez avoir des données actualisées avec F5, qui utilise des données de mémoire au lieu de 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();
        });
    }
  }
}
allenhwkim
la source
Si vous stockez dans le stockage de session, alors comment vous assurer que le stockage de session est détruit lorsque vous quittez l'application?
Gags
mais cela introduit un comportement inattendu pour l'utilisateur. Lorsque l'utilisateur frappe F5 ou le bouton d'actualisation du navigateur, il attend de nouvelles données du serveur. Mais en réalité, il obtient des données obsolètes de localStorage. Rapports de bogues, tickets de support, etc. entrants ... Comme son nom l' sessionStorageindique, je ne l'utiliserais que pour des données qui devraient être cohérentes pour toute la session.
Martin Schneider
@ MA-Maddin comme je l'ai dit "je l'ai utilisé pour éviter la limite d'utilisation". Si vous souhaitez que les données soient actualisées avec F5, vous devez utiliser de la mémoire au lieu de sessionStorage. La réponse a été modifiée avec cette approche.
allenhwkim
oui, cela pourrait être un cas d'utilisation. Je viens d'être déclenché car tout le monde parle de Cache et OP en a getCustomerdans son exemple. ;) Alors je voulais juste avertir certains ppl qui pourraient ne pas voir les risques :)
Martin Schneider
5

L'implémentation que vous choisirez dépendra si vous souhaitez que unsubscribe () annule ou non votre requête HTTP.

Dans tous les cas, les décorateurs TypeScript sont un bon moyen de normaliser le comportement. Voici celui que j'ai écrit:

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

Définition du décorateur:

/**
 * 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;
}
Arlo
la source
Salut @Arlo - l'exemple ci-dessus ne compile pas. Property 'connect' does not exist on type '{}'.de la ligne returnValue.connect();. Peux-tu élaborer?
Hoof
4

Données de réponse HTTP pouvant être mises en cache à l'aide de Rxjs Observer / Observable + Caching + Subscription

Voir le code ci-dessous

* Avertissement: Je suis nouveau sur rxjs, alors gardez à l'esprit que je peux mal utiliser l'approche observable / observateur. Ma solution est purement un conglomérat d'autres solutions que j'ai trouvées, et est la conséquence d'avoir échoué à trouver une solution simple et bien documentée. Ainsi, je fournis ma solution de code complète (comme j'aurais aimé la trouver) dans l'espoir qu'elle aide les autres.

* notez que cette approche est vaguement basée sur GoogleFirebaseObservables. Malheureusement, je manque d'expérience / de temps pour reproduire ce qu'ils ont fait sous le capot. Mais ce qui suit est un moyen simpliste de fournir un accès asynchrone à certaines données pouvant être mises en cache.

Situation : un composant «liste de produits» est chargé d'afficher une liste de produits. Le site est une application Web d'une seule page avec quelques boutons de menu qui «filtreront» les produits affichés sur la page.

Solution : le composant "s'abonne" à une méthode de service. La méthode de service renvoie un tableau d'objets de produit, auquel le composant accède via le rappel d'abonnement. La méthode de service encapsule son activité dans un Observateur nouvellement créé et renvoie l'observateur. À l'intérieur de cet observateur, il recherche les données mises en cache et les retransmet à l'abonné (le composant) et retourne. Sinon, il émet un appel http pour récupérer les données, s'abonne à la réponse, où vous pouvez traiter ces données (par exemple, mapper les données à votre propre modèle), puis transmettre les données à l'abonné.

Le 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 (le modèle)

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

Voici un exemple de la sortie que je vois lorsque je charge la page dans Chrome. Notez qu'à la charge initiale, les produits sont récupérés depuis http (appel à mon service de repos de noeud, qui s'exécute localement sur le port 3000). Lorsque je clique ensuite pour accéder à une vue «filtrée» des produits, les produits sont trouvés dans le cache.

Mon journal Chrome (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

... [cliqué sur un bouton de menu pour filtrer les produits] ...

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

Conclusion: C'est la manière la plus simple que j'ai trouvée (jusqu'à présent) pour implémenter des données de réponse http pouvant être mises en cache. Dans mon application angulaire, chaque fois que je navigue vers une vue différente des produits, le composant de la liste de produits se recharge. ProductService semble être une instance partagée, donc le cache local de «products: Product []» dans ProductService est conservé pendant la navigation, et les appels ultérieurs à «GetProducts ()» renvoient la valeur mise en cache. Une dernière note, j'ai lu des commentaires sur la façon dont les observables / abonnements doivent être fermés lorsque vous avez terminé pour éviter les «fuites de mémoire». Je ne l'ai pas inclus ici, mais c'est quelque chose à garder à l'esprit.

ObjectiveTC
la source
1
Remarque - J'ai depuis trouvé une solution plus puissante, impliquant RxJS BehaviorSubjects, qui simplifie le code et réduit considérablement les «frais généraux». Dans products.service.ts, 1. importez {BehaviorSubject} depuis 'rxjs'; 2. remplacez «products: Product []» par «product $: BehaviorSubject <Product []> = new BehaviorSubject <Product []> ([]);» 3. Vous pouvez maintenant simplement appeler le http sans rien retourner. http_getProducts () {this.http.get (...). map (res => res.json ()). subscribe (products => this.product $ .next (products))};
ObjectiveTC
1
La variable locale «product $» est un subjectSubject qui émettra et stockera les derniers produits (à partir de l'appel du produit $ .next (..) dans la partie 3). Maintenant, dans vos composants, injectez le service comme d'habitude. Vous obtenez la dernière valeur attribuée au produit $ en utilisant productService.product $ .value. Ou abonnez-vous au produit $ si vous souhaitez effectuer une action chaque fois que le produit $ reçoit une nouvelle valeur (c'est-à-dire que la fonction produit $ .next (...) est appelée dans la partie 3).
ObjectiveTC
1
Par exemple, dans products.component.ts ... this.productService.product $ .takeUntil (this.ngUnsubscribe) .subscribe ((products) => {this.category); laissez filterProducts = this.productService.getProductsByCategory (this.category); this.products = filtersProducts; });
ObjectiveTC
1
Une note importante sur la désinscription des observables: ".takeUntil (this.ngUnsubscribe)". Voir cette question / réponse sur le débordement de pile, qui semble montrer la manière recommandée de facto de se désabonner des événements: stackoverflow.com/questions/38008334/…
ObjectiveTC
1
L'alternative est au .first () ou .take (1) si l'observable n'est destiné à recevoir des données qu'une seule fois. Tous les autres 'flux infinis' d'observables doivent être désabonnés dans 'ngOnDestroy ()', et si vous ne le faites pas, vous pouvez vous retrouver avec des rappels 'observables' en double. stackoverflow.com/questions/28007777/…
ObjectiveTC
3

Je suppose que @ ngx-cache / core pourrait être utile pour maintenir les fonctionnalités de mise en cache pour les appels http, en particulier si l'appel HTTP est effectué à la fois sur les plates-formes de navigateur et de serveur .

Disons que nous avons la méthode suivante:

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

Vous pouvez utiliser le Cacheddécorateur de @ ngx-cache / core pour stocker la valeur renvoyée par la méthode faisant l'appel HTTP sur le cache storage( le storagepeut être configurable, veuillez vérifier l'implémentation sur ng-seed / universal ) - dès la première exécution. La prochaine fois que la méthode est invoquée (quel que soit le navigateur ou la plate-forme de serveur ), la valeur est récupérée à partir du 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());
}

Il y a aussi la possibilité de méthodes de mise en cache d'utilisation ( has, get, set) en utilisant l' API de mise en 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';
  }
}

Voici la liste des packages, pour la mise en cache côté client et côté serveur:

Burak Tasci
la source
1

rxjs 5.3.0

Je ne suis pas content .map(myFunction).publishReplay(1).refCount()

Avec plusieurs abonnés, .map()exécutemyFunction deux fois dans certains cas (je m'attends à ce qu'il ne s'exécute qu'une seule fois). Une solution semble êtrepublishReplay(1).refCount().take(1)

Une autre chose que vous pouvez faire est simplement de ne pas utiliser refCount()et de rendre l'Observable chaud tout de suite:

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

Cela lancera la requête HTTP indépendamment des abonnés. Je ne sais pas si la désinscription avant la fin de HTTP GET l'annulera ou non.

Arlo
la source
1

Ce que nous voulons faire, c'est nous assurer que cela ne provoque pas de multiples requêtes réseau.

Mon préféré est d'utiliser des asyncméthodes pour les appels qui font des requêtes réseau. Les méthodes elles-mêmes ne renvoient pas de valeur, mais elles mettent à jour un BehaviorSubjectdans le même service, auquel les composants s'abonneront.

Maintenant, pourquoi utiliser un BehaviorSubjectau lieu d'un Observable? Car,

  • Lors de l'abonnement, BehaviorSubject renvoie la dernière valeur, tandis qu'un observable normal ne se déclenche que lorsqu'il reçoit un onnext .
  • Si vous souhaitez récupérer la dernière valeur du BehaviorSubject dans un code non observable (sans abonnement), vous pouvez utiliser la getValue()méthode.

Exemple:

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

Ensuite, lorsque cela est nécessaire, nous pouvons simplement nous abonner customers$.

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

Ou peut-être souhaitez-vous l'utiliser directement dans un modèle

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

Alors maintenant, jusqu'à ce que vous fassiez un autre appel à getCustomers, les données sont conservées dans le customers$BehaviorSubject.

Et si vous souhaitez actualiser ces données? il suffit d'appelergetCustomers()

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

En utilisant cette méthode, nous n'avons pas à conserver explicitement les données entre les appels réseau suivants car elles sont gérées par le BehaviorSubject.

PS: Habituellement, lorsqu'un composant est détruit, il est recommandé de se débarrasser des abonnements, pour cela, vous pouvez utiliser la méthode suggérée dans cette réponse.

cyberpirate92
la source
1

Excellentes réponses.

Ou vous pouvez le faire:

Il s'agit de la dernière version de rxjs. J'utilise la version 5.5.7 de RxJS

import {share} from "rxjs/operators";

this.http.get('/someUrl').pipe(share());
Jay Modi
la source
0

Appelez simplement share () après la carte et avant de vous abonner .

Dans mon cas, j'ai un service générique (RestClientService.ts) qui fait le reste, extrait les données, vérifie les erreurs et renvoie observable à un service d'implémentation concret (ex: ContractClientService.ts), enfin cette implémentation concrète renvoie observable à de ContractComponent.ts, et celui-ci s'abonne pour mettre à jour la vue.

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

}
surfealokesea
la source
0

J'ai écrit une classe de 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>();
    }

}

Tout est statique en raison de la façon dont nous l'utilisons, mais n'hésitez pas à en faire une classe et un service normaux. Je ne sais pas si angular conserve une seule instance pendant tout le temps (nouveau pour Angular2).

Et voici comment je l'utilise:

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

Je suppose qu'il pourrait y avoir un moyen plus intelligent, qui utiliserait quelques Observableastuces, mais c'était très bien pour mes besoins.

Ondra Žižka
la source
0

Utilisez simplement cette couche de cache, elle fait tout ce dont vous avez besoin et gère même le cache pour les requêtes ajax.

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

C'est tellement simple à utiliser

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

La couche (en tant que service angulaire injectable) est

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]);
                     }
                     }
                 );
             }
            });
        }
}
Ravinder Payal
la source
0

C'est .publishReplay(1).refCount();ou .publishLast().refCount();puisque les observables Angular Http se terminent après la demande.

Cette classe simple met en cache le résultat afin que vous puissiez vous abonner à .value plusieurs fois et ne faire qu'une seule demande. Vous pouvez également utiliser .reload () pour effectuer une nouvelle demande et publier des données.

Vous pouvez l'utiliser comme:

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

et la source:

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

}
Matjaz Hirsman
la source
0

Vous pouvez créer une classe simple Cacheable <> qui aide à gérer les données récupérées du serveur http avec plusieurs abonnés:

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

}

Usage

Déclarez l'objet Cacheable <> (probablement dans le cadre du service):

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

et gestionnaire:

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

Appel depuis un composant:

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

Vous pouvez y souscrire plusieurs composants.

Plus de détails et un exemple de code sont ici: http://devinstance.net/articles/20171021/rxjs-cacheable

yfranz
la source
0

Vous pouvez simplement utiliser ngx-cacheable ! Cela convient mieux à votre scénario.

L'avantage d'utiliser ce

  • Il n'appelle l'API de repos qu'une seule fois, met en cache la réponse et renvoie la même chose pour les demandes suivantes.
  • Peut appeler l'API selon les besoins après l'opération de création / mise à jour / suppression.

Donc, votre classe de service serait quelque chose comme ça -

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

Voici le lien pour plus de référence.

Tushar Walzade
la source
-4

Avez-vous essayé d'exécuter le code que vous avez déjà?

Parce que vous construisez l'Observable à partir de la promesse qui en résulte getJSON(), la demande réseau est effectuée avant que quiconque ne s'abonne. Et la promesse qui en résulte est partagée par tous les abonnés.

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
// ...
Brandon
la source
j'ai édité la question pour la rendre spécifique à Angular 2
Angular University