web-dev-qa-db-fra.com

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

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

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

Si nous prenons cet 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 demandes multiples du réseau.

Cela peut sembler un scénario inhabituel, mais en fait très 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 faire cela 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 faire cela dans RxJs 5, ou devrions-nous faire autre chose? 

Remarque: comme indiqué dans Angular 5 new HttpClient, la partie .map(res => res.json()) de tous les exemples est désormais inutile, car le résultat JSON est désormais pris en charge par défaut.

246
Angular University

Mettez les données en cache et, si elles sont disponibles en cache, renvoyez-les sinon, faites la demande 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.iblytram.io/angular/2018/03/05/advanced-caching-with-rxjs.html est une excellente explication sur la manière de mettre en cache avec shareReplay.

202
Günter Zöchbauer

Selon la suggestion de @Cristian, c’est un moyen qui fonctionne bien pour les observables HTTP, qui n’émettent qu’une seule fois et qui sont ensuite complètes:

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

MISE À JOUR: Ben Lesh dit que la prochaine version mineure après la version 5.2.0 vous permettra d'appeler simplement shareReplay () pour réellement mettre en cache.

PRÉCÉDEMMENT.....

Premièrement, n'utilisez ni share () ni publishReplay (1) .refCount (), ils sont identiques et le problème, c'est qu'il ne partage que si les connexions sont établies tant que l'observable est actif, si vous vous connectez après l'avoir terminée. , il crée à nouveau une nouvelle 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 de nouvelle observable comme share () une fois que refCount atteindra zéro et vous établirez une nouvelle connexion, ce qui est le comportement approprié 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 consultation + la possibilité de fournir une table de consultation 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 en tant que premier argument, vous transmettez une fonction qui renvoie les observables, car le Http d'Angular s'exécute immédiatement. Par conséquent, en renvoyant une fonction exécutée paresseuse, nous pouvons décider de ne pas l'appeler si elle est déjà entrée. 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")
30
Guojian Miguel Wu

rxjs 5.4.0 a une nouvelle méthode shareReplay.

L'auteur dit explicitement "idéal pour gérer des tâches telles que la mise en cache AJAX results"

rxjs PR # 2443 feat (shareReplay): ajoute la variante shareReplay de la variable publishReplay

shareReplay renvoie un observable correspondant à la source multicast. un ReplaySubject. Ce sujet de relecture est recyclé en cas d'erreur de la part du source, mais pas à la fin de la source. Cela rend shareReplay idéal pour gérer des tâches telles que la mise en cache AJAX, car il s'agit de Retryable. Son comportement de répétition, cependant, diffère de share en cela il ne répètera pas la source observable, mais plutôt le les valeurs de la source observable.

22
Arlo

selon ceci article

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

alors dedans si les déclarations ajoutent simplement

.publishReplay(1)
.refCount();

à .map(...)

22
Ivanesses

J'ai joué la question, mais je vais essayer de tenter ma chance.

//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'une seule livraison: getCustomer().subscribe(customer$)

Nous ne nous abonnons pas à la réponse API de getCustomer(), nous abonnons à un ReplaySubject qui est observable et qui est également capable de s'abonner à un autre observable et (et cela est important) de conserver sa dernière valeur émise et de le republier vers n'importe lequel ReplaySubject's).

7
Birowsky

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

Dans tous les cas, les décorateurs TypeScript sont un moyen agréable de normaliser le comportement. C'est 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;
}
4
Arlo

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 afin d'é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 votre information, sessionStorage limite est 5M (ou 4.75M). Ainsi, il ne devrait pas être utilisé comme ceci pour un grand ensemble de données.

------ modifier -------------
Si vous voulez avoir actualisé les données avec F5, utilisez des données en 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();
        });
    }
  }
}
4
allenhwkim

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

Voir le code ci-dessous

* disclaimer: je suis nouveau sur rxjs, alors gardez bien à l'esprit que je risque d'abuser de l'approche observable/observateur. Ma solution est purement une agglomération d'autres solutions que j'ai trouvées et est la conséquence de l'échec à trouver une solution simple et bien documentée. Ainsi, je fournis ma solution de code complète (comme je l’aurais aimé), dans l’espoir d’aider les autres.

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

Situation : Un composant 'product-list' est chargé d'afficher une liste de produits. Le site est une application Web d'une seule page avec des boutons de menu permettant de "filtrer" les produits affichés sur la page.

Solution: le composant "s'abonne" à une méthode de service. La méthode service renvoie un tableau d'objets du 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 en cache, les renvoie à l'abonné (le composant) et les retourne. Sinon, il émet un appel http pour extraire les données, s'abonne à la réponse, où vous pouvez traiter ces données (par exemple, mapper les données sur votre propre modèle), puis les renvoyer à 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 que lors du chargement initial, les produits sont extraits de http (appel du service de repos de mon noeud, qui s'exécute localement sur le port 3000). Lorsque je clique ensuite pour accéder à une vue "filtrée" des produits, ceux-ci se trouvent 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

... [cliquez 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 le moyen le plus simple que j’ai trouvé (jusqu’à présent) d’implémenter les données de réponse http pouvant être mises en cache. Dans mon application angulaire, chaque fois que je navigue dans une vue différente des produits, le composant de la liste de produits est rechargé. ProductService semble être une instance partagée. Le cache local de 'products: Product []' dans ProductService est conservé pendant la navigation. Les appels suivants à "GetProducts ()" renvoient la valeur mise en cache. Une note finale, 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 n'ai pas inclus cela ici, mais c'est quelque chose à garder à l'esprit.

3
ObjectiveTC

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

Disons que nous avons la méthode suivante:

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

Vous pouvez utiliser le décorateur Cached de @ ngx-cache/core pour stocker la valeur renvoyée par la méthode effectuant l'appel HTTP à cache storage (la storage peut être configurée, veuillez vérifier l'implémentation à ng). -seed/universal) - à droite lors de la première exécution. Lors du prochain appel de la méthode (quelle que soit la plate-forme browser ou server), la valeur est extraite 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 est également possible d'utiliser des méthodes de mise en cache (has, get, set) à l'aide de 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, à la fois pour la mise en cache côté client et côté serveur:

2
Burak Tasci

Grandes réponses.

Ou vous pouvez faire ceci:

Ceci est de la dernière version de rxjs. J'utilise 5.5.7 version de RxJS

import {share} from "rxjs/operators";

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

rxjs 5.3.0

Je n'ai pas été satisfait de .map(myFunction).publishReplay(1).refCount()

Avec plusieurs abonnés, .map() exécute myFunction deux fois dans certains cas (je suppose qu'il ne s'exécutera qu'une fois). Un correctif semble être publishReplay(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 le désabonnement avant la fin de HTTP GET l'annulera ou non.

1
Arlo

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

Mon préféré consiste à utiliser les méthodes async pour les appels faisant des demandes réseau. Les méthodes elles-mêmes ne renvoient pas de valeur, mais mettent à jour une BehaviorSubject au sein du même service, à laquelle les composants s'abonnent.

Maintenant, pourquoi utiliser une BehaviorSubject au lieu d'une Observable? Parce que, 

  • Lors de la souscription, BehaviorSubject renvoie la dernière valeur, alors qu’une observable régulière ne se déclenche que si elle reçoit une onnext.
  • Si vous souhaitez récupérer la dernière valeur de BehaviorSubject dans un code non observable (sans abonnement), vous pouvez utiliser la méthode getValue().

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, chaque fois que nécessaire, nous pouvons simplement nous abonner à customers$.

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

Ou peut-être que vous voulez 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.

Alors que faire si vous voulez actualiser ces données? Il suffit d'appeler getCustomers()

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

En utilisant cette méthode, il n’est pas nécessaire de conserver explicitement les données entre les appels réseau ultérieurs, car elles sont gérées par la variable BehaviorSubject.

PS: Généralement, lorsqu'un composant est détruit, il est recommandé de supprimer les abonnements. Pour cela, vous pouvez utiliser la méthode suggérée dans this answer.

1
cyberpirate92

Utilisez simplement cette couche de cache, elle fait tout ce dont vous avez besoin et gérez même le cache des requêtes ajax.

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

C'est tellement facile à 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]);
                     }
                     }
                 );
             }
            });
        }
}
0
Ravinder Payal

rxjs version 5.4.0 (2017-05-09) ajoute le support pour shareReplay .

Pourquoi utiliser shareReplay?

Vous voulez généralement utiliser shareReplay lorsque vous avez des effets secondaires ou des calculs pénibles que vous ne souhaitez pas exécuter entre plusieurs abonnés. Cela peut également être utile dans les cas où vous savez que vous aurez des abonnés tardifs à un flux ayant besoin d'accéder aux valeurs précédemment émises. Cette capacité à rejouer les valeurs lors de la souscription 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 abouti).

Exemple de service angulaire

Voici un service client très simple utilisant 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 pourrait être déplacée vers la méthode getCustomers mais que les observables renvoyées par HttpClient sont "froides" cela est acceptable dans le constructeur, car l'appel http ne sera effectué qu'avec le appel à subscribe.

On suppose également ici que les données renvoyées initialement ne deviennent pas périmées pendant la durée de vie de l'instance d'application.

0
Igor

Vous pouvez créer une classe simple, Cacheable <>, qui facilite la gestion des données extraites d'un 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();
    }

}

Utilisation

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 avoir plusieurs composants souscrits.

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

0
yfranz

Il suffit d'appeler share () après map et avant tout subscribe .

Dans mon cas, j'ai un service générique (RestClientService.ts) qui passe l'appel, extrait les données, vérifie les erreurs et renvoie l'observable à un service d'implémentation concrète (par exemple, ContractClientService.ts), enfin cette implémentation concrète. renvoie observable à 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;
   });
  }

}
0
surfealokesea

C'est exactement ce que j'ai créé pour la bibliothèque ngx-rxcache.

Jetez un coup d'oeil sur https://github.com/adriandavidbrand/ngx-rxcache et voyez un exemple de travail sur https://stackblitz.com/edit/angular-jxqaiv

0
Adrian Brand

C'est .publishReplay(1).refCount(); ou .publishLast().refCount(); puisque les observables HTTP angulaires sont terminés après la requête.

Cette classe simple met en cache le résultat afin que vous puissiez vous abonner à .value plusieurs fois et ne fait qu'une demande. Vous pouvez également utiliser .reload () pour faire 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);
  }

}
0
Matjaz Hirsman

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 à cause de notre utilisation, mais n'hésitez pas à en faire une classe et un service normaux. Je ne sais pas si angular garde une seule instance pour la totalité du temps (nouveauté 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 des astuces Observable, mais cela me convenait parfaitement.

0
Ondra Žižka

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

L'avantage d'utiliser cette

  • Il appelle l'API rest 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.

Ainsi, votre classe de service ressemblerait à ceci:

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.

0
Tushar Walzade