¿Cuál es la forma correcta de compartir el resultado de una llamada de red Angular Http en RxJs 5?
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.
EDITAR: a partir de 2021, la forma correcta es utilizar el shareReplay
operador 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
.
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();
}
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")
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 shareReplay
variante 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.