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

Resuelto Angular University asked hace 8 años • 0 respuestas

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

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

Si tomamos este observable y le agregamos varios 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 al observable para mostrar un mensaje de error y lo pasa a la plantilla mediante la canalización asíncrona, ya tenemos dos suscriptores.

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

Es decir, 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 new HttpClient, la .map(res => res.json())parte en todos los ejemplos ahora es inútil, ya que el resultado JSON ahora se asume de forma predeterminada.

Angular University avatar Mar 29 '16 04:03 Angular University
Aceptado

EDITAR: a partir de 2021, la forma correcta es utilizar el shareReplayoperador propuesto de forma nativa por RxJs. Vea más detalles en las respuestas a continuación.


Almacene en caché los datos y, si están disponibles, 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 plomero

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

Günter Zöchbauer avatar Mar 29 '2016 17:03 Günter Zöchbauer

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

getCustomer() {
    return this.http.get('/someUrl')
        .map(res => res.json()).publishLast().refCount();
}
Angular University avatar Mar 29 '2016 22:03 Angular University

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

PREVIAMENTE.....

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

Birowski dio la solución correcta arriba, que es usar ReplaySubject. ReplaySubject almacenará en caché los valores que le proporcione (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();
}

He aquí 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 es necesario marcar this._cache como en el ejemplo anterior. También observe 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 forma 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")
Guojian Miguel Wu avatar Mar 23 '2017 01:03 Guojian Miguel Wu

rxjs 5.4.0 tiene un nuevo método shareReplay .

  • rx-libro compartirReplay()
  • No hay documentos en reactivex.io/rxjs

El autor dice explícitamente "ideal para manejar cosas como almacenar en caché los resultados de AJAX".

rxjs PR #2443 hazaña (shareReplay): agrega shareReplayvariante depublishReplay

shareReplay devuelve un observable que es la fuente multidifundida a través de un ReplaySubject. Ese tema de repetición se recicla en caso de error de la fuente, pero no al completar la fuente. Esto hace que shareReplay sea ideal para manejar cosas como almacenar en caché los resultados de AJAX, ya que se puede volver a intentar. Sin embargo, su comportamiento de repetición difiere del de compartir en que no repetirá la fuente observable, sino que repetirá los valores de la fuente observable.

Arlo avatar May 12 '2017 17:05 Arlo