example - ¿Cuál es la forma correcta de compartir el resultado de una llamada de red Angular Http en RxJs 5?
rxjs pipe (22)
Lo que queremos hacer es asegurarnos de que esto no provoque múltiples solicitudes de red.
Mi favorito personal es utilizar métodos
async
para llamadas que realizan solicitudes de red.
Los métodos en sí mismos no devuelven un valor, sino que actualizan un
BehaviorSubject
dentro del mismo servicio, al que se suscribirán los componentes.
Ahora, ¿por qué usar un
BehaviorSubject
lugar de un
Observable
?
Porque,
-
Tras la suscripción, BehaviorSubject devuelve el último valor, mientras que un observable regular solo se dispara cuando recibe un
onnext
. -
Si desea recuperar el último valor del BehaviorSubject en un código no observable (sin suscripción), puede usar el método
getValue()
.
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, donde sea necesario, podemos suscribirnos a los
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 para obtener
getCustomers
, los datos se retienen en el
customers$
BehaviorSubject.
Entonces, ¿qué pasa si desea actualizar estos datos?
solo llame a
getCustomers()
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 this respuesta.
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 usando 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 el nuevo
HttpClient
Angular 5, la parte
.map(res => res.json())
en todos los ejemplos ahora es inútil, ya que el resultado JSON ahora se supone por defecto.
¡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
}
}
Here está el enlace para más referencia.
¿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
// ...
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 hace 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);
}
}
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 no dude en 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
Observable
trucos, pero esto estaba bien para mis propósitos.
Esto es exactamente para lo que he creado la biblioteca ngx-rxcache.
Míralo en https://github.com/adriandavidbrand/ngx-rxcache y mira un ejemplo de trabajo en https://stackblitz.com/edit/angular-jxqaiv
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 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
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;
});
}
}
Solo use esta capa de caché, hace todo lo que necesita e incluso administra la caché para solicitudes 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]);
}
}
);
}
});
}
}
ACTUALIZACIÓN: Ben Lesh dice que la próxima versión menor después de 5.2.0, solo 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")
Datos de respuesta HTTP en caché usando 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 la memoria 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 conserva 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.
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 proof :)
Solo hay una
getCustomer().subscribe(customer$)
:
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 son suscriptores (de ReplaySubject).
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();
});
}
}
}
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());
Guarde 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;
}
}
}
Este artículo
https://blog.thoughtram.io/angular/2018/03/05/advanced-caching-with-rxjs.html
es una gran explicación de cómo almacenar en caché con
shareReplay
.
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í:
/**
* 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;
}
Definición del decorador:
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 */
}
Según la sugerencia de @Cristian, esta es una manera 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();
}
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 la plataforma del navegador como en la del servidor .
Digamos que tenemos el siguiente método:
getCustomer() {
return this.http.get(''/someUrl'').map(res => res.json());
}
Puede usar el decorador en
Cached
de
@ngx-cache/core
para almacenar el valor devuelto del método que realiza la llamada HTTP en el
cache storage
en
cache storage
(
el
storage
puede ser configurable, verifique la implementación en
ng-seed/universal
) - directamente en La primera ejecución.
Las próximas veces que se invoca el método (no importa en el
navegador
o
la
plataforma del
servidor
), el valor se recupera del
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 utilizar métodos de almacenamiento en caché (
has
,
get
,
set
) utilizando la
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:
- @ngx-cache/core : utilidad de caché
- @ngx-cache/platform-browser : implementación de la plataforma SPA / Browser
- @ngx-cache/platform-server : implementación de la plataforma del servidor
- @ngx-cache/fs-storage : utilidad de almacenamiento (requerida para la plataforma del servidor)
de acuerdo con este article
Resulta que podemos agregar fácilmente el almacenamiento en caché a lo observable agregando editReplay (1) y refCount.
así que adentro si las declaraciones solo se agregan
.publishReplay(1)
.refCount();
a
.map(...)
rxjs 5.4.0 tiene un nuevo método shareReplay .
- rx-book shareReplay ()
- No hay documentos en reactivex.io/rxjs
El autor dice explícitamente "ideal para manejar cosas como el almacenamiento en caché de resultados AJAX"
rxjs PR # 2443 feat (shareReplay): agrega la variante
publishReplay
de
publishReplay
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.
rxjs versión 5.4.0 (2017-05-09) agrega soporte para shareReplay .
¿Por qué usar shareReplay?
En general, desea utilizar shareReplay cuando tiene efectos secundarios o cálculos de impuestos que no desea que se ejecuten 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
@CacheObservableArgsKey
getMyThing(id: string): Observable<any> {
return this.http.get(''things/''+id);
}
Tenga en cuenta que la asignación en el constructor podría trasladarse al método
getCustomers
pero como los observables devueltos por
HttpClient
son "fríos",
hacer esto en el constructor es aceptable ya que la llamada http solo se realizará con la primera llamada para
subscribe
.
También la suposición aquí es que los datos iniciales devueltos no se vuelven obsoletos durante la vida útil de la instancia de la aplicación.
rxjs 5.3.0
No he sido feliz con
.map(myFunction).publishReplay(1).refCount()
Con múltiples suscriptores,
.map()
ejecuta
myFunction
dos veces en algunos casos (espero que solo se ejecute una vez).
Una solución parece ser
publishReplay(1).refCount().take(1)
Otra cosa que puede hacer es simplemente no usar
refCount()
y hacer que el Observable esté
refCount()
inmediato:
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.