¿Cuál es la forma correcta de compartir el resultado de una llamada de red Angular Http en RxJs 5?


303

Al usar Http, llamamos a un método que realiza una llamada de red y devuelve un http observable:

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

Si tomamos esto observable y le agregamos múltiples suscriptores:

let network$ = getCustomer();

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

Lo que queremos hacer es asegurarnos de que esto no provoque múltiples solicitudes de red.

Esto puede parecer un escenario inusual, pero en realidad es bastante común: por ejemplo, si la persona que llama se suscribe a lo observable para mostrar un mensaje de error y lo pasa a la plantilla utilizando la tubería asíncrona, ya tenemos dos suscriptores.

¿Cuál es la forma correcta de hacerlo en RxJs 5?

A saber, esto parece funcionar bien:

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

Pero, ¿es esta la forma idiomática de hacer esto en RxJs 5, o deberíamos hacer algo más?

Nota: Según Angular 5 nuevo HttpClient, la .map(res => res.json())parte en todos los ejemplos ahora es inútil, ya que el resultado JSON ahora se supone por defecto.


1
> compartir es idéntico a publicar (). refCount (). En realidad no lo es. Vea la siguiente discusión: github.com/ReactiveX/rxjs/issues/1363
Christian

1
pregunta editada, según el problema, parece que los documentos del código deben actualizarse -> github.com/ReactiveX/rxjs/blob/master/src/operator/share.ts
Angular University

Creo que "depende". Pero para las llamadas en las que no puede almacenar en caché los datos localmente b / c, podría no tener sentido debido a los cambios / combinaciones de parámetros .share () parece ser absolutamente lo correcto. Pero si puede almacenar en caché las cosas localmente, algunas de las otras respuestas con respecto a ReplaySubject / BehaviorSubject también son buenas soluciones.
JimB

Creo que no solo necesitamos almacenar en caché los datos, también necesitamos actualizar / modificar los datos almacenados en caché. Es un caso común. Por ejemplo, si deseo agregar un nuevo campo al modelo en caché o actualizar el valor del campo. ¿Quizás crear un DataCacheService singleton con el método CRUD es una mejor manera? Como tienda de Redux . ¿Qué piensas?
slideshowp2

¡Simplemente podría usar ngx-cacheable ! Se adapta mejor a tu escenario. Refiera mi respuesta a continuación
Tushar Walzade el

Respuestas:


230

Caché los datos y si están disponibles en caché, devuélvalos, de lo contrario realice la solicitud 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;
    }
  }
}

Ejemplo de Plunker

Este artículo https://blog.thoughtram.io/angular/2018/03/05/advanced-caching-with-rxjs.html es una gran explicación sobre cómo almacenar en caché shareReplay.


3
do()al contrario de map()no modifica el evento. También podría usarlo map(), pero luego tuvo que asegurarse de que se devuelva el valor correcto al final de la devolución de llamada.
Günter Zöchbauer

3
Si el sitio de llamada que .subscribe()no necesita el valor, puede hacerlo porque podría obtener solo null(dependiendo de lo que this.extractDataregrese), pero en mi humilde opinión, esto no expresa bien la intención del código.
Günter Zöchbauer

2
Cuando this.extraDatatermina de extraData() { if(foo) { doSomething();}}otra manera, se devuelve el resultado de la última expresión, que podría no ser lo que desea.
Günter Zöchbauer

99
@ Günter, gracias por el código, funciona. Sin embargo, estoy tratando de entender por qué estás haciendo un seguimiento de los datos y observables por separado. ¿No lograría efectivamente el mismo efecto almacenando en caché solo Observable <Data> como este? 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 Es una clase a la que se deserializa el JSON recibido, para obtener una verificación de tipo y autocompletado fuertes. No hay necesidad de usarlo, pero es común.
Günter Zöchbauer

44

Según la sugerencia de @Cristian, esta es una forma que funciona bien para los observables HTTP, que solo emiten una vez y luego completan:

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

Hay un par de problemas con el uso de este enfoque: el observable devuelto no se puede cancelar ni volver a intentar. Esto podría no ser un problema para usted, pero nuevamente podría serlo. Si esto es un problema, entonces el shareoperador podría ser una opción razonable (aunque con algunos casos extremos desagradables). Para una discusión profunda sobre las opciones, vea la sección de comentarios en esta publicación de blog: blog.jhades.org/…
Christian

1
Pequeña aclaración ... Aunque estrictamente la fuente observable que se comparte publishLast().refCount()no puede cancelarse, una vez que todas las suscripciones a la observable devuelta por refCounthan sido canceladas, el efecto neto es que la fuente observable será anulada, cancelando si está "en vuelo"
Christian

@ Christian Hola, ¿puedes explicar lo que quieres decir con "no se puede cancelar ni volver a intentar"? Gracias.
indefinido

37

ACTUALIZACIÓN: Ben Lesh dice que la próxima versión menor después de 5.2.0, podrá llamar a shareReplay () para realmente almacenar en caché.

PREVIAMENTE.....

En primer lugar, no use share () o publishingReplay (1) .refCount (), son lo mismo y el problema es que solo se comparte si se realizan conexiones mientras el observable está activo, si se conecta después de que se complete , crea un nuevo observable de nuevo, traducción, no realmente almacenamiento en caché.

Birowski dio la solución correcta arriba, que es usar ReplaySubject. ReplaySubject almacenará en caché los valores que le dé (bufferSize) en nuestro caso 1. No creará un nuevo observable como share () una vez que refCount llegue a cero y realice una nueva conexión, que es el comportamiento correcto para el almacenamiento en caché.

Aquí hay una función reutilizable

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

Aquí se explica cómo 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'));
  }
}

A continuación se muestra una versión más avanzada de la función almacenable en caché. Esta permite tener su propia tabla de búsqueda + la capacidad de proporcionar una tabla de búsqueda personalizada. De esta manera, no tiene que verificar esto._caché como en el ejemplo anterior. Observe también que, en lugar de pasar el observable como primer argumento, pasa una función que devuelve los observables, esto se debe a que Http de Angular se ejecuta de inmediato, por lo que al devolver una función ejecutada de manera diferida, podemos decidir no llamarla si ya está en nuestro caché

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")

¿Hay alguna razón para no usar esta solución como operador RxJs const data$ = this._http.get('url').pipe(cacheable()); /*1st subscribe*/ data$.subscribe(); /*2nd subscribe*/ data$.subscribe();:? Por lo tanto, se comporta más como cualquier otro operador ..
Felix

31

rxjs 5.4.0 tiene un nuevo método shareReplay .

El autor dice explícitamente "ideal para manejar cosas como el almacenamiento en caché de resultados AJAX"

rxjs PR # 2443 feat (shareReplay): agrega una shareReplayvariante depublishReplay

shareReplay devuelve un observable que es la fuente multidifusión sobre un ReplaySubject. Ese tema de reproducción se recicla por error de la fuente, pero no al finalizar la fuente. Esto hace que shareReplay sea ideal para manejar cosas como el almacenamiento en caché de resultados AJAX, ya que es recuperable. Sin embargo, su comportamiento de repetición difiere de compartir en que no repetirá la fuente observable, sino que repetirá los valores de la fuente observable.


¿Está relacionado con esto? Sin embargo, estos documentos son de 2014. github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/…
Aaron Hoffman

44
Intenté agregar .shareReplay (1, 10000) a un observable, pero no noté ningún cambio de caché o comportamiento. ¿Hay un ejemplo de trabajo disponible?
Aydus-Matthew

Mirando el registro de cambios github.com/ReactiveX/rxjs/blob/… Apareció anteriormente, se eliminó en la v5, se agregó de nuevo en la 5.4: el enlace del libro rx se refiere a la v4, pero existe en la actual LTS v5.5.6 y está en v6. Me imagino que el enlace del libro rx no está actualizado.
Jason Awbrey

25

de acuerdo con este artículo

Resulta que podemos agregar fácilmente el almacenamiento en caché a lo observable mediante la publicación postReplay (1) y refCount.

así que adentro si las declaraciones solo se agregan

.publishReplay(1)
.refCount();

a .map(...)


11

rxjs versión 5.4.0 (2017-05-09) agrega soporte para shareReplay .

¿Por qué usar shareReplay?

En general, desea usar shareReplay cuando tiene efectos secundarios o cálculos de impuestos que no desea ejecutar entre varios suscriptores. También puede ser valioso en situaciones en las que sabe que tendrá suscriptores tardíos a una secuencia que necesita acceso a los valores emitidos previamente. Esta capacidad de reproducir valores en la suscripción es lo que diferencia compartir y shareReplay.

Puede modificar fácilmente un servicio angular para usar esto y devolver un observable con un resultado en caché que solo realizará la llamada http una sola vez (suponiendo que la primera llamada haya sido exitosa).

Ejemplo de servicio angular

Aquí hay un servicio al cliente muy simple que utiliza 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 */
}

Tenga en cuenta que la asignación en el constructor podría moverse al método, getCustomerspero como los observables devueltos HttpClientson "fríos", hacer esto en el constructor es aceptable ya que la llamada http solo se realizará con la primera llamada a subscribe.

También se supone aquí que los datos devueltos iniciales no se vuelven obsoletos durante la vida útil de la instancia de la aplicación.


Realmente me gusta este patrón y estoy tratando de implementarlo dentro de una biblioteca compartida de servicios de API que utilizo en varias aplicaciones. Un ejemplo es un UserService, y en todas partes, excepto en un par de lugares, no es necesario invalidar el caché durante la vida útil de la aplicación, pero para esos casos, ¿cómo podría invalidarlo sin causar que las suscripciones anteriores se vuelvan huérfanas?
SirTophamHatt

10

Destaqué la pregunta, pero intentaré intentarlo.

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

Aquí está la prueba :)

Solo hay una comida para llevar: getCustomer().subscribe(customer$)

No nos estamos suscribiendo a la respuesta api de getCustomer(), nos estamos suscribiendo a un ReplaySubject que es observable y que también puede suscribirse a un Observable diferente y (y esto es importante) mantener su último valor emitido y volver a publicarlo en cualquiera de sus (ReplaySubject's ) suscriptores.


1
Me gusta este enfoque, ya que hace un buen uso de rxjs y no es necesario agregar una lógica personalizada, gracias
Thibs

7

Encontré una manera de almacenar el resultado de http en sessionStorage y usarlo para la sesión, para que nunca vuelva a llamar al servidor.

Lo usé para llamar a github API para evitar el límite de uso.

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

Para su información, el límite de almacenamiento de sesión es de 5M (o 4.75M). Por lo tanto, no debe usarse así para un gran conjunto de datos.

------ editar -------------
Si desea tener datos actualizados con F5, que utiliza datos de memoria en lugar 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();
        });
    }
  }
}

Si guardará en el almacenamiento de la sesión, ¿cómo se asegurará de que el almacenamiento de la sesión se destruya cuando salga de la aplicación?
Gags

pero esto introduce un comportamiento inesperado para el usuario. Cuando el usuario presiona F5 o el botón de actualización del navegador, espera datos nuevos del servidor. Pero en realidad está obteniendo datos obsoletos de localStorage. Informes de errores, tickets de soporte, etc. entrantes ... Como su nombre lo sessionStorageindica, lo usaría solo para datos que se espera sean consistentes durante toda la sesión.
Martin Schneider

@ MA-Maddin como dije "Lo usé para evitar el límite de uso". Si desea que los datos se actualicen con F5, debe usar memoria en lugar de sessionStorage. La respuesta ha sido editada con este enfoque.
allenhwkim

Sí, eso podría ser un caso de uso. Acabo de activarme ya que todos están hablando de Cache y OP lo ha hecho getCustomeren su ejemplo. ;) Así que solo quería advertir a algunas personas que podrían no ver los riesgos :)
Martin Schneider

5

La implementación que elija dependerá de si desea cancelar la suscripción () para cancelar su solicitud HTTP o no.

En cualquier caso, los decoradores TypeScript son una buena manera de estandarizar el comportamiento. Este es el que escribí:

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

Definición del decorador:

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

Hola @Arlo: el ejemplo anterior no se compila. Property 'connect' does not exist on type '{}'.de la linea returnValue.connect();. ¿Puedes elaborar?
Pezuña

4

Datos de respuesta HTTP en caché utilizando Rxjs Observer / Observable + Caching + Subscription

Ver código abajo

* descargo de responsabilidad: soy nuevo en rxjs, así que tenga en cuenta que podría estar haciendo un mal uso del enfoque observable / observador. Mi solución es puramente un conglomerado de otras soluciones que encontré, y es la consecuencia de no haber encontrado una solución simple y bien documentada. Por lo tanto, estoy proporcionando mi solución de código completa (como me hubiera gustado encontrar) con la esperanza de que ayude a otros.

* nota, este enfoque se basa libremente en GoogleFirebaseObservables. Desafortunadamente, me falta la experiencia / tiempo adecuado para replicar lo que hicieron bajo el capó. Pero la siguiente es una forma simplista de proporcionar acceso asíncrono a algunos datos aptos para caché.

Situación : el componente 'lista de productos' tiene la tarea de mostrar una lista de productos. El sitio es una aplicación web de una sola página con algunos botones de menú que 'filtrarán' los productos que se muestran en la página.

Solución : El componente "se suscribe" a un método de servicio. El método de servicio devuelve una matriz de objetos de producto, a los que accede el componente a través de la devolución de llamada de suscripción. El método de servicio envuelve su actividad en un observador recién creado y devuelve el observador. Dentro de este observador, busca datos almacenados en caché y los devuelve al suscriptor (el componente) y los devuelve. De lo contrario, emite una llamada http para recuperar los datos, se suscribe a la respuesta, donde puede procesar esos datos (por ejemplo, asignar los datos a su propio modelo) y luego pasar los datos al suscriptor.

El código

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 (el modelo)

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

Aquí hay una muestra de la salida que veo cuando cargo la página en Chrome. Tenga en cuenta que en la carga inicial, los productos se obtienen de http (llamar al servicio de reposo de mi nodo, que se ejecuta localmente en el puerto 3000). Cuando hago clic para navegar a una vista 'filtrada' de los productos, los productos se encuentran en caché.

Mi registro de Chrome (consola):

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

... [hizo clic en un botón de menú para filtrar los productos] ...

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

Conclusión: esta es la forma más simple que he encontrado (hasta ahora) para implementar datos de respuesta http almacenables en caché. En mi aplicación angular, cada vez que navego a una vista diferente de los productos, el componente de la lista de productos se recarga. ProductService parece ser una instancia compartida, por lo que la memoria caché local de 'productos: Producto []' en ProductService se retiene durante la navegación, y las llamadas posteriores a "GetProducts ()" devuelve el valor almacenado en caché. Una nota final, he leído comentarios sobre cómo deben cerrarse los observables / suscripciones cuando haya terminado para evitar 'pérdidas de memoria'. No he incluido esto aquí, pero es algo a tener en cuenta.


1
Nota: desde entonces he encontrado una solución más poderosa, que involucra RxJS BehaviorSubjects, que simplifica el código y reduce drásticamente la 'sobrecarga'. En products.service.ts, 1. importe {BehaviorSubject} desde 'rxjs'; 2. cambie 'productos: Producto []' a 'producto $: BehaviorSubject <Producto []> = new BehaviorSubject <Producto []> ([]);' 3. Ahora puede simplemente llamar al http sin devolver nada. http_getProducts () {this.http.get (...). map (res => res.json ()). subscribe (productos => this.product $ .next (productos))};
ObjectiveTC

1
La variable local 'product $' es un conductSubject, que EMITARÁ y ALMACENARÁ los últimos productos (de la llamada al producto $ .next (..) en la parte 3). Ahora en sus componentes, inyecte el servicio normalmente. Obtiene el valor asignado más recientemente del producto $ usando productService.product $ .value. O suscríbase al producto $ si desea realizar una acción siempre que el producto $ reciba un nuevo valor (es decir, la función del producto $ .next (...) se llama en la parte 3).
ObjectiveTC

1
Por ejemplo, en products.component.ts ... this.productService.product $ .takeUntil (this.ngUnsubscribe) .subscribe ((products) => {this.category); let filterProducts = this.productService.getProductsByCategory (this.category); this.products = filterProducts; });
ObjectiveTC

1
Una nota importante sobre la cancelación de la suscripción de observables: ".takeUntil (this.ngUnsubscribe)". Vea esta pregunta / respuesta de desbordamiento de pila, que parece mostrar la forma recomendada 'de facto' para darse de baja de los eventos: stackoverflow.com/questions/38008334/…
ObjectiveTC

1
La alternativa es a .first () o .take (1) si el observable solo está destinado a recibir datos una vez. Todas las demás 'secuencias infinitas' de observables deben darse de baja en 'ngOnDestroy ()', y si no lo hace, puede terminar con devoluciones de llamada duplicadas 'observables'. stackoverflow.com/questions/28007777/…
ObjectiveTC

3

Supongo que @ ngx-cache / core podría ser útil para mantener las características de almacenamiento en caché para las llamadas http, especialmente si la llamada HTTP se realiza tanto en las plataformas del navegador como en las del servidor .

Digamos que tenemos el siguiente método:

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

Se puede utilizar el Cacheddecorador de @ NGX-cache / núcleo para almacenar el valor devuelto por el método de hacer la llamada HTTP en el cache storage( la storagepuede ser configurable, por favor verifica la aplicación a ng-semilla / universal ) - a la derecha en la primera ejecución. Las próximas veces que se invoca el método (no importa en el navegador o en la plataforma del servidor ), el valor se recupera de 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());
}

También existe la posibilidad de usar métodos de almacenamiento en caché ( has, get, set) utilizando el API de almacenamiento en caché .

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

Aquí está la lista de paquetes, tanto para el almacenamiento en caché del lado del cliente como del lado del servidor:


1

rxjs 5.3.0

No he sido feliz con .map(myFunction).publishReplay(1).refCount()

Con varios suscriptores, se .map()ejecuta myFunctiondos veces en algunos casos (espero que solo se ejecute una vez). Una solución parece serpublishReplay(1).refCount().take(1)

Otra cosa que puede hacer es simplemente no usar refCount()y hacer que el Observable se caliente inmediatamente:

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

Esto iniciará la solicitud HTTP independientemente de los suscriptores. No estoy seguro de si cancelar la suscripción antes de que finalice HTTP GET lo cancele o no.


1

Lo que queremos hacer es asegurarnos de que esto no provoque múltiples solicitudes de red.

Mi favorito personal es utilizar asyncmétodos para llamadas que hacen solicitudes de red. Los métodos en sí mismos no devuelven un valor, sino que actualizan uno BehaviorSubjectdentro del mismo servicio, al que se suscribirán los componentes.

Ahora, ¿por qué usar un en BehaviorSubjectlugar de un Observable? Porque,

  • Tras la suscripción, BehaviorSubject devuelve el último valor, mientras que un observable regular solo se activa cuando recibe un onnext.
  • Si desea recuperar el último valor del BehaviorSubject en un código no observable (sin una suscripción), puede usar el getValue()método.

Ejemplo:

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

Luego, cuando sea necesario, podemos suscribirnos customers$.

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

O tal vez quieras usarlo directamente en una plantilla

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

Así que ahora, hasta que realice otra llamada getCustomers, los datos se retienen en customers$BehaviorSubject.

Entonces, ¿qué pasa si desea actualizar estos datos? solo llama agetCustomers()

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

Con este método, no tenemos que retener explícitamente los datos entre las llamadas de red posteriores, ya que es manejado por BehaviorSubject.

PD: Por lo general, cuando un componente se destruye, es una buena práctica deshacerse de las suscripciones, para eso puede usar el método sugerido en esta respuesta.


1

Grandes respuestas

O podrías hacer esto:

Esto es de la última versión de rxjs. Estoy usando la versión 5.5.7 de RxJS

import {share} from "rxjs/operators";

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

0

Simplemente llame a share () después del mapa y antes de suscribirse .

En mi caso, tengo un servicio genérico (RestClientService.ts) que realiza la llamada de descanso, extrae datos, verifica si hay errores y regresa observable a un servicio de implementación concreto (p. Ej .: ContractClientService.ts), finalmente esta implementación concreta devuelve observable a de ContractComponent.ts, y este se suscribe para actualizar 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

Escribí una clase de caché

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

}

Todo es estático debido a cómo lo usamos, pero siéntase libre de convertirlo en una clase normal y un servicio. Sin embargo, no estoy seguro de si angular mantiene una sola instancia durante todo el tiempo (nuevo en Angular2).

Y así es como 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;
            }

Supongo que podría haber una forma más inteligente, que usaría algunos Observabletrucos, pero esto estaba bien para mis propósitos.


0

Simplemente use esta capa de caché, hace todo lo que necesita e incluso administra la caché para las solicitudes de ajax.

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

Es muy fácil de usar.

@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 capa (como un servicio angular inyectable) es

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

Es .publishReplay(1).refCount();o .publishLast().refCount();desde que los observables de Angular Http se completan después de la solicitud.

Esta clase simple almacena en caché el resultado para que pueda suscribirse a .value muchas veces y solo realiza 1 solicitud. También puede usar .reload () para hacer una nueva solicitud y publicar datos.

Puedes usarlo como:

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

y la fuente:

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

Puede crear una clase simple Cacheable <> que ayuda a administrar los datos recuperados del servidor http con múltiples suscriptores:

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

Declarar el objeto Cacheable <> (presumiblemente como parte del servicio):

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

y manejador:

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

Llamar desde un componente:

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

Puede tener varios componentes suscritos a él.

Más detalles y ejemplos de código están aquí: http://devinstance.net/articles/20171021/rxjs-cacheable


0

¡Simplemente podría usar ngx-cacheable ! Se adapta mejor a tu escenario.

El beneficio de usar esto

  • Llama a rest API solo una vez, almacena en caché la respuesta y devuelve lo mismo para las siguientes solicitudes.
  • Puede llamar a la API según sea necesario después de la operación de creación / actualización / eliminación.

Entonces, su clase de servicio sería algo como esto:

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

Aquí está el enlace para más referencia.


-4

¿Has intentado ejecutar el código que ya tienes?

Debido a que está construyendo el Observable a partir de la promesa resultante getJSON(), la solicitud de red se realiza antes de que nadie se suscriba. Y la promesa resultante es compartida por todos los suscriptores.

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

he editado la pregunta para que sea Angular 2 específica
Angular University
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.