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 genere 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 usando la tubería 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 otra cosa en su lugar?
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 asume de forma predeterminada.
Almacene los datos en caché y, si está disponible, devuélvalo; 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.ilsttram.io/angular/2018/03/05/advanced-caching-with-rxjs.html es una gran explicación de 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 la próxima versión menor después de la 5.2.0, podrá simplemente llamar a shareReplay () para 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 completa , crea un nuevo observable de nuevo, la traducción, no el 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í como 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 marcar this._cache como en el ejemplo anterior. También tenga en cuenta que en lugar de pasar el observable como primer argumento, pasa una función que devuelve los observables, esto se debe a que el 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 .
El autor dice explícitamente "ideal para manejar cosas como almacenar en caché los resultados de AJAX"
rxjs PR # 2443 feat (shareReplay): agrega una shareReplay
variante depublishReplay
shareReplay devuelve un observable que es la fuente multidifundida sobre un ReplaySubject. Ese sujeto de repetición se recicla por error de la fuente, pero no cuando se completa la fuente. Esto hace que shareReplay sea ideal para manejar cosas como almacenar en caché los resultados de AJAX, ya que es recuperable. 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.
rxjs versión 5.4.0 (2017-05-09) agrega soporte para shareReplay .
¿Por qué utilizar shareReplay?
Por lo general, desea utilizar shareReplay cuando tiene efectos secundarios o cálculos impositivos 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 transmisión que necesitan acceso a valores emitidos anteriormente. Esta capacidad de reproducir valores en la suscripción es lo que diferencia a share y shareReplay.
Puede modificar fácilmente un servicio angular para usar esto y devolver un observable con un resultado en caché que solo hará la llamada http una sola vez (asumiendo que la primera llamada fue exitosa).
Aquí hay un servicio de atención 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 trasladarse al método, getCustomers
pero como los observables devueltos desde HttpClient
son "fríos", hacer esto en el constructor es aceptable ya que la llamada http solo se realizará con la primera llamada a subscribe
.
Además, la suposición aquí es que los datos devueltos iniciales no quedan obsoletos durante la vida útil de la instancia de la aplicación.
Destaqué la pregunta, pero intentaré hacer esto.
//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 conclusión: getCustomer().subscribe(customer$)
No nos suscribimos a la respuesta de la API de getCustomer()
, nos suscribimos a un ReplaySubject que es observable 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.
Encontré una manera de almacenar http get result en sessionStorage y usarlo para la sesión, de modo que nunca vuelva a llamar al servidor.
Lo usé para llamar a la API de github 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 5 millones (o 4,75 millones). Por lo tanto, no debe usarse así para un gran conjunto de datos.
------ editar -------------
Si desea tener datos actualizados con F5, que usa 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();
});
}
}
}
Datos de respuesta HTTP almacenables en caché utilizando Rxjs Observer / Observable + Caching + Suscripción
Ver código a continuación
* descargo de responsabilidad: soy nuevo en rxjs, así que tenga en cuenta que puedo 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 podido encontrar una solución simple y bien documentada. Por lo tanto, estoy proporcionando mi solución de código completa (como me hubiera gustado haber encontrado) con la esperanza de que ayude a otros.
* nota, este enfoque se basa libremente en GoogleFirebaseObservables. Desafortunadamente, no tengo la experiencia / el tiempo adecuados para replicar lo que hicieron bajo el capó. Pero la siguiente es una forma simplista de proporcionar acceso asincrónico a algunos datos que se pueden almacenar en caché.
Situación : un componente de '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 el componente accede a través de la devolución de llamada de suscripción. El método de servicio envuelve su actividad en un Observer recién creado y devuelve el observador. Dentro de este observador, busca datos almacenados en caché y los pasa de vuelta al suscriptor (el componente) y regresa. 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 del resultado que veo cuando cargo la página en Chrome. Tenga en cuenta que en la carga inicial, los productos se obtienen de http (llame al servicio de descanso 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 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 sencilla que he encontrado (hasta ahora) de 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 caché local de 'productos: Producto []' en ProductService se retiene durante la navegación y las llamadas posteriores a "GetProducts ()" devuelven el valor en caché. Una nota final, he leído comentarios sobre cómo los observables / suscripciones deben cerrarse cuando haya terminado para evitar 'pérdidas de memoria'. No he incluido esto aquí, pero es algo a tener en cuenta.
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 de TypeScript son una buena forma de estandarizar el comportamiento. Este es el que escribí:
@CacheObservableArgsKey
getMyThing(id: string): Observable<any> {
return this.http.get('things/'+id);
}
Definición de 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;
}
Supongo que @ ngx-cache / core podría ser útil para mantener las funciones de almacenamiento en caché para las llamadas http, especialmente si la llamada HTTP se realiza tanto en plataformas de navegador como de servidor .
Digamos que tenemos el siguiente método:
getCustomer() {
return this.http.get('/someUrl').map(res => res.json());
}
Se puede utilizar el Cached
decorador de @ NGX-cache / núcleo para almacenar el valor devuelto por el método de hacer la llamada HTTP en el cache storage
( la storage
puede ser configurable, por favor verifica la aplicación a ng-semilla / universal ) - a la derecha en la primera ejecución. La próxima vez que se invoca el método (sin importar 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 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:
Lo que queremos hacer es asegurarnos de que esto no genere múltiples solicitudes de red.
Mi favorito personal es utilizar async
métodos 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, a qué componentes se suscribirán.
Ahora, ¿por qué usar un en BehaviorSubject
lugar de un Observable
? Porque,
onnext
.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, donde 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 a getCustomers
, los datos se conservan en customers$
BehaviorSubject.
Entonces, ¿qué pasa si desea actualizar estos datos? solo haz una llamada 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 llamadas de red posteriores, ya que los maneja 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.
Excelentes 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());
rxjs 5.3.0
No he sido feliz con .map(myFunction).publishReplay(1).refCount()
Con varios suscriptores, se .map()
ejecuta myFunction
dos 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 calentar el Observable de 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 la cancelará o no.
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 realice solo una solicitud. También puede usar .reload () para realizar 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);
}
}
Puede crear una clase Cacheable <> simple que ayude 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 <> en caché (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.
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 está haciendo la llamada de descanso, extrayendo datos, verificando errores y devolviendo observables a un servicio de implementación concreto (por ejemplo: 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;
});
}
}
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 y un servicio normales. 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.
Simplemente 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 tan 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]);
}
}
);
}
});
}
}
¡Simplemente podría usar ngx-cacheable ! Se adapta mejor a tu escenario.
El beneficio de usar esto
- Llama a la API rest 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 referencias.
La estrella de HGTV, Christina Hall, revela que le diagnosticaron envenenamiento por mercurio y plomo, probablemente debido a su trabajo como manipuladora de casas.
Recientemente salió a la luz un informe policial que acusa a la estrella de 'Love Is Blind', Brennon, de violencia doméstica. Ahora, Brennon ha respondido a los reclamos.
Conozca cómo Wynonna Judd se dio cuenta de que ahora es la matriarca de la familia mientras organizaba la primera celebración de Acción de Gracias desde que murió su madre, Naomi Judd.
Descubra por qué un destacado experto en lenguaje corporal cree que es fácil trazar "tales paralelismos" entre la princesa Kate Middleton y la princesa Diana.
Los inodoros arrojan columnas de aerosol invisibles con cada descarga. ¿Como sabemos? La prueba fue capturada por láseres de alta potencia.
Air travel is far more than getting from point A to point B safely. How much do you know about the million little details that go into flying on airplanes?
The world is a huge place, yet some GeoGuessr players know locations in mere seconds. Are you one of GeoGuessr's gifted elite? Take our quiz to find out!
¿Sigue siendo efectivo ese lote de repelente de insectos que te quedó del verano pasado? Si es así, ¿por cuánto tiempo?
¿Quiere probar un cepillo de dientes Sonicare sin gastar mucho dinero en uno de sus modelos favoritos de gama alta? Puede comprar un kit de la Serie 2 o Serie 3 por tan solo $ 30 hoy en Amazon. Haga clic aquí para ver la lista completa de modelos elegibles y tenga en cuenta que se descontarán $ 10 adicionales en su carrito.
Tapas elásticas de silicona de Tomorrow's Kitchen, paquete de 12 | $14 | Amazonas | Código promocional 20OFFKINJALids son básicamente los calcetines de la cocina; siempre perdiéndose, dejando contenedores huérfanos que nunca podrán volver a cerrarse. Pero, ¿y si sus tapas pudieran estirarse y adaptarse a todos los recipientes, ollas, sartenes e incluso frutas en rodajas grandes que sobran? Nunca más tendrás que preocuparte por perder esa tapa tan específica.
Hemos pirateado algunas ciudades industriales en esta columna, como Los Ángeles y Las Vegas. Ahora es el momento de una ciudad militar-industrial-compleja.
Un minorista está enlatando su sección de tallas grandes. Pero no están tomando la categoría solo en línea o descontinuándola por completo.
El equipo está a la espera de las medallas que ganó en los Juegos Olímpicos de Invierno de 2022 en Beijing, ya que se está resolviendo un caso de dopaje que involucra a la patinadora artística rusa Kamila Valieva.
Miles de compradores de Amazon recomiendan la funda de almohada de seda Mulberry, y está a la venta en este momento. La funda de almohada de seda viene en varios colores y ayuda a mantener el cabello suave y la piel clara. Compre las fundas de almohada de seda mientras tienen hasta un 46 por ciento de descuento en Amazon
El jueves se presentó una denuncia de delito menor amenazante agravado contra Joe Mixon.
El Departamento de Policía de Lafayette comenzó a investigar a un profesor de la Universidad de Purdue en diciembre después de recibir varias denuncias de un "hombre sospechoso que se acercaba a una mujer".
Al igual que el mundo que nos rodea, el lenguaje siempre está cambiando. Mientras que en eras anteriores los cambios en el idioma ocurrían durante años o incluso décadas, ahora pueden ocurrir en cuestión de días o incluso horas.
Estoy de vuelta por primera vez en seis años. No puedo decirte cuánto tiempo he estado esperando esto.
“And a river went out of Eden to water the garden, and from thence it was parted and became into four heads” Genesis 2:10. ? The heart is located in the middle of the thoracic cavity, pointing eastward.
Creo, un poco tarde en la vida, en dar oportunidades a la gente. Generosamente.