web-dev-qa-db-fra.com

Angular 5 mise en cache des appels api du service http

Dans mon application Angular 5, un certain ensemble de données (qui ne change pas très souvent) est nécessaire plusieurs fois à différents endroits de l'application. Après l'appel de l'API, le résultat est stocké avec l'Observable do operator. De cette façon, j'ai implémenté la mise en cache des requêtes HTTP dans mon service.

J'utilise Angular 5.1.3 et RxJS 5.5.6.

Est-ce une bonne pratique? Existe-t-il de meilleures alternatives?

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import 'rxjs/add/operator/do';

@Injectable()
export class FruitService {

  fruits: Array<string> = [];

  constructor(private http: HttpClient) { }

  getFruits() {
    if (this.fruits.length === 0) {
      return this.http.get<any>('api/getFruits')
        .do(data => { this.fruits = data })
    } else {
      return Observable.of(this.fruits);
    }
  }
}
10
Herman Fransen

Le problème avec votre solution est que si un 2e appel arrive alors qu'un 1er est en attente, il crée une nouvelle requête http. Voici comment je le ferais:

@Injectable()
export class FruitService {

  readonly fruits = this.http.get<any>('api/getFruits').shareReplay(1);

  constructor(private http: HttpClient) { }
}

le plus gros problème est lorsque vous avez des paramètres et que vous souhaitez mettre en cache en fonction des paramètres. Dans ce cas, vous auriez besoin d'une sorte de fonction memoize comme celle de lodash ( https://lodash.com/docs/4.17.5#memoize )

Vous pouvez également implémenter un opérateur cache en mémoire pour Observable, comme:

const cache = {};

function cacheOperator<T>(this: Observable<T>, key: string) {
    return new Observable<T>(observer => {
        const cached = cache[key];
        if (cached) {
            cached.subscribe(observer);
        } else {
            const add = this.multicast(new ReplaySubject(1));
            cache[key] = add;
            add.connect();
            add.catch(err => {
                delete cache[key];
                throw err;
            }).subscribe(observer);
        }
    });
}

declare module 'rxjs/Observable' {
    interface Observable<T> {
        cache: typeof cacheOperator;
    }
}

Observable.prototype.cache = cacheOperator;

et l'utiliser comme:

getFruit(id: number) {
  return this.http.get<any>(`api/fruit/${id}`).cache(`fruit:${id}`);
}
14
Andrei Tătar

Il existe une autre façon de le faire avec shareReplay et Angular 5, 6 ou 7: créez un service:

import { Observable } from 'rxjs/Observable';
import { shareReplay } from 'rxjs/operators';
const CACHE_SIZE = 1;

private cache$: Observable<Object>;

get api() {
  if ( !this.cache$ ) {
    this.cache$ = this.requestApi().pipe( shareReplay(CACHE_SIZE) );
  }
  return this.cache_arbitrage$;
}

private requestApi() {
  const API_ENDPOINT = 'yoururl/';
  return this.http.get<any>(API_ENDPOINT_ARBITRATION);
}

public resetCache() {
  this.cache$ = null;
}

Pour lire les données directement dans votre fichier html, utilisez ceci:

<div *ngIf="this.apiService.api | async as api">{{api | json}}</div>

Dans votre composant, vous pouvez vous abonner comme ceci:

this.apiService.api.subscribe(res => {/*your code*/})
3
D3F

En fait, le moyen le plus simple de mettre en cache les réponses et de partager également un seul abonnement (sans faire de nouvelle demande pour chaque abonné) est d'utiliser publishReplay(1) et refCount() (j'utilise des opérateurs pipables).

readonly fruits$ = this.http.get<any>('api/getFruits')
  .pipe(
    publishReplay(1), // publishReplay(1, _time_)
    refCount(),
    take(1),
  );

Ensuite, lorsque vous souhaitez obtenir la valeur mise en cache/fraîche, vous vous abonnerez simplement à fresh$.

fresh$.subscribe(...)

L'opérateur publishReplay met en cache la valeur, puis refCount conserve un seul abonnement à son parent et se désabonne s'il n'y a pas d'abonnés. La take(1) est nécessaire pour terminer correctement la chaîne après une seule valeur.

La partie la plus importante est que lorsque vous vous abonnez à cette chaîne publishReplay émet son tampon lors de l'abonnement et si elle contient une valeur mise en cache, elle sera immédiatement propagée à take(1) qui complète la chaîne afin qu'elle ne crée aucun abonnement à this.http.get. Si publishReplay ne contient rien, il s'abonnera à sa source et fera la requête HTTP.

2
martin

Pour Angular 6, RxJS 6 et expiration de cache simple, utilisez le code suivant:

interface CacheEntry<T> {
  expiry: number;
  observable: Observable<T>;
}

const DEFAULT_MAX_AGE = 300000;
const globalCache: { [key: string]: CacheEntry<any>; } = {};

export function cache(key: string, maxAge: number = DEFAULT_MAX_AGE) {
  return function cacheOperatorImpl<T>(source: Observable<T>) {
    return Observable.create(observer => {
      const cached = globalCache[key];
      if (cached && cached.expiry >= Date.now()) {
        cached.observable.subscribe(observer);
      } else {
        const add = source.pipe(multicast(new ReplaySubject(1))) as ConnectableObservable<T>;
        globalCache[key] = {observable: add, expiry: Date.now() + maxAge};
        add.connect();
        add.pipe(
          catchError(err => {
            delete globalCache[key];
            return throwError(err);
          })
        ).subscribe(observer);
      }
    });
  };
}
1
mohlendo