在我的Angular 5应用程序中,在应用程序的不同位置需要多次某个数据集(不经常更改)。调用API后,结果将与Observable do
运算符一起存储。这样我就可以在我的服务中实现HTTP请求的缓存。
我正在使用Angular 5.1.3和RxJS 5.5.6。
这是一个好习惯吗? 还有更好的选择吗?
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import 'rxjs/add/operator/do';
@Injectable()
export class FruitService {
fruits: Array<string> = [];
constructor(private http: HttpClient) { }
getFruits() {
if (this.fruits.length === 0) {
return this.http.get<any>('api/getFruits')
.do(data => { this.fruits = data })
} else {
return Observable.of(this.fruits);
}
}
}
答案 0 :(得分:13)
您的解决方案存在的问题是,如果第一个呼叫在第一个呼叫挂起时发出,则会创建一个新的http请求。我将如何做到这一点:
@Injectable()
export class FruitService {
readonly fruits = this.http.get<any>('api/getFruits').shareReplay(1);
constructor(private http: HttpClient) { }
}
更大的问题是当你有params而你想根据params缓存。在这种情况下,您需要某种memoize
函数,例如来自lodash(https://lodash.com/docs/4.17.5#memoize)的函数
您还可以为cache
实现一些内存Observable
运算符,例如:
const cache = {};
function cacheOperator<T>(this: Observable<T>, key: string) {
return new Observable<T>(observer => {
const cached = cache[key];
if (cached) {
cached.subscribe(observer);
} else {
const add = this.multicast(new ReplaySubject(1));
cache[key] = add;
add.connect();
add.catch(err => {
delete cache[key];
throw err;
}).subscribe(observer);
}
});
}
declare module 'rxjs/Observable' {
interface Observable<T> {
cache: typeof cacheOperator;
}
}
Observable.prototype.cache = cacheOperator;
并使用它:
getFruit(id: number) {
return this.http.get<any>(`api/fruit/${id}`).cache(`fruit:${id}`);
}
答案 1 :(得分:2)
实际上,缓存响应并共享单个订阅(不为每个订阅者提出新请求)的最简单方法是使用publishReplay(1)
和refCount()
(我使用pipable运算符)
readonly fruits$ = this.http.get<any>('api/getFruits')
.pipe(
publishReplay(1), // publishReplay(1, _time_)
refCount(),
take(1),
);
然后,当您想要获得缓存/新鲜值时,您只需订阅fresh$
。
fresh$.subscribe(...)
publishReplay
运算符缓存该值,然后refCount
仅维护其父级的一个订阅,并且如果没有订阅者则取消订阅。 take(1)
是在单个值之后正确完成链的必要条件。
最重要的部分是,当您订阅此链时publishReplay
会在订阅时发出其缓冲区,如果它包含缓存值,它会立即传播到完成链的take(1)
所以它根本不会创建this.http.get
的订阅。如果publishReplay
不包含任何内容,则会订阅其来源并发出HTTP请求。
答案 2 :(得分:2)
还有另一种方法与shareReplay和Angular 5、6或7一起创建服务:
import { Observable } from 'rxjs/Observable';
import { shareReplay } from 'rxjs/operators';
const CACHE_SIZE = 1;
private cache$: Observable<Object>;
get api() {
if ( !this.cache$ ) {
this.cache$ = this.requestApi().pipe( shareReplay(CACHE_SIZE) );
}
return this.cache_arbitrage$;
}
private requestApi() {
const API_ENDPOINT = 'yoururl/';
return this.http.get<any>(API_ENDPOINT_ARBITRATION).pipe();
}
public resetCache() {
this.cache$ = null;
}
要直接在html文件中读取数据,请使用以下方法:
<div *ngIf="this.apiService.api | async as api">{{api | json}}</div>
在您的组件中,您可以像这样订阅:
this.apiService.api.pipe().subscribe(res => {/*your code*/})
答案 3 :(得分:1)
对于Angular 6,RxJS 6和简单的缓存过期,请使用以下代码:
interface CacheEntry<T> {
expiry: number;
observable: Observable<T>;
}
const DEFAULT_MAX_AGE = 300000;
const globalCache: { [key: string]: CacheEntry<any>; } = {};
export function cache(key: string, maxAge: number = DEFAULT_MAX_AGE) {
return function cacheOperatorImpl<T>(source: Observable<T>) {
return Observable.create(observer => {
const cached = globalCache[key];
if (cached && cached.expiry >= Date.now()) {
cached.observable.subscribe(observer);
} else {
const add = source.pipe(multicast(new ReplaySubject(1))) as ConnectableObservable<T>;
globalCache[key] = {observable: add, expiry: Date.now() + maxAge};
add.connect();
add.pipe(
catchError(err => {
delete globalCache[key];
return throwError(err);
})
).subscribe(observer);
}
});
};
}
答案 4 :(得分:0)
根据其他一些答案,如果 API 调用具有参数(例如 kind
、color
),这里有一个简单的变体。
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { shareReplay } from 'rxjs/operators';
@Injectable()
export class FruitService {
private readonly cache: Map<string, Observable<string[]>> =
new Map<string, Observable<string[]>>();
constructor(private readonly httpClient: HttpClient) {}
getFruits(kind: string, color: string): Observable<string[]> {
const key = `${kind}${color}`;
if (!this.cache[key]) {
this.cache[key] = this.httpClient
.get<string[]>('api/getFruits', { params: { kind, color} })
.pipe(shareReplay(1));
}
return this.cache[key];
}
}