Angular 5 次缓存 http 服务 api 次调用
Angular 5 caching http service api calls
在我的 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);
}
}
}
您的解决方案的问题是,如果在第一个调用未决时出现第二个调用,它会创建一个新的 http 请求。以下是我的做法:
@Injectable()
export class FruitService {
readonly fruits = this.http.get<any>('api/getFruits').shareReplay(1);
constructor(private http: HttpClient) { }
}
更大的问题是当你有参数并且你想根据参数进行缓存时。在那种情况下,您将需要某种 memoize
函数,例如来自 lodash (https://lodash.com/docs/4.17.5#memoize)
的函数
您还可以为 Observable
实现一些内存中的 cache
运算符,例如:
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}`);
}
实际上,缓存响应和共享单个订阅(不为每个订阅者发出新请求)的最简单方法是使用 publishReplay(1)
和 refCount()
(我正在使用 pipable 运算符) .
readonly fruits$ = this.http.get<any>('api/getFruits')
.pipe(
publishReplay(1), // publishReplay(1, _time_)
refCount(),
take(1),
);
然后当您想要获得 cached/fresh 值时,您只需订阅 fresh$
。
fresh$.subscribe(...)
publishReplay
运算符缓存该值,然后 refCount
只维护一个对其父级的订阅,如果没有订阅者则取消订阅。 take(1)
是正确完成单个值后的链所必需的。
最重要的部分是,当您订阅此链时,publishReplay
会在订阅时发出其缓冲区,如果它包含缓存值,它将立即传播到完成链的 take(1)
所以它根本不会创建对 this.http.get
的订阅。如果 publishReplay
不包含任何内容,它将订阅其来源并发出 HTTP 请求。
对于Angular6,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);
}
});
};
}
还有另一种方法,使用 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$;
}
private requestApi() {
const API_ENDPOINT = 'yoururl/';
return this.http.get<any>(API_ENDPOINT);
}
public resetCache() {
this.cache$ = null;
}
要直接读取 html 文件中的数据,请使用:
<div *ngIf="this.apiService.api | async as api">{{api | json}}</div>
在您的组件中,您可以这样订阅:
this.apiService.api.subscribe(res => {/*your code*/})
根据其他一些答案,如果 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];
}
}
在我的 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);
}
}
}
您的解决方案的问题是,如果在第一个调用未决时出现第二个调用,它会创建一个新的 http 请求。以下是我的做法:
@Injectable()
export class FruitService {
readonly fruits = this.http.get<any>('api/getFruits').shareReplay(1);
constructor(private http: HttpClient) { }
}
更大的问题是当你有参数并且你想根据参数进行缓存时。在那种情况下,您将需要某种 memoize
函数,例如来自 lodash (https://lodash.com/docs/4.17.5#memoize)
您还可以为 Observable
实现一些内存中的 cache
运算符,例如:
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}`);
}
实际上,缓存响应和共享单个订阅(不为每个订阅者发出新请求)的最简单方法是使用 publishReplay(1)
和 refCount()
(我正在使用 pipable 运算符) .
readonly fruits$ = this.http.get<any>('api/getFruits')
.pipe(
publishReplay(1), // publishReplay(1, _time_)
refCount(),
take(1),
);
然后当您想要获得 cached/fresh 值时,您只需订阅 fresh$
。
fresh$.subscribe(...)
publishReplay
运算符缓存该值,然后 refCount
只维护一个对其父级的订阅,如果没有订阅者则取消订阅。 take(1)
是正确完成单个值后的链所必需的。
最重要的部分是,当您订阅此链时,publishReplay
会在订阅时发出其缓冲区,如果它包含缓存值,它将立即传播到完成链的 take(1)
所以它根本不会创建对 this.http.get
的订阅。如果 publishReplay
不包含任何内容,它将订阅其来源并发出 HTTP 请求。
对于Angular6,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);
}
});
};
}
还有另一种方法,使用 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$;
}
private requestApi() {
const API_ENDPOINT = 'yoururl/';
return this.http.get<any>(API_ENDPOINT);
}
public resetCache() {
this.cache$ = null;
}
要直接读取 html 文件中的数据,请使用:
<div *ngIf="this.apiService.api | async as api">{{api | json}}</div>
在您的组件中,您可以这样订阅:
this.apiService.api.subscribe(res => {/*your code*/})
根据其他一些答案,如果 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];
}
}