My 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);
}
}
}
ソリューションの問題は、1番目の呼び出しが保留中に2番目の呼び出しが来た場合、新しいhttp要求を作成することです。ここに私がそれをする方法があります:
@Injectable()
export class FruitService {
readonly fruits = this.http.get<any>('api/getFruits').shareReplay(1);
constructor(private http: HttpClient) { }
}
大きな問題は、パラメーターがあり、そのパラメーターに基づいてキャッシュする場合です。その場合、lodashのようなmemoize
関数が必要になります( 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}`);
}
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);
}
public resetCache() {
this.cache$ = null;
}
Htmlファイルのデータを直接読み取るには、次を使用します。
<div *ngIf="this.apiService.api | async as api">{{api | json}}</div>
コンポーネントでは、次のようにサブスクライブできます。
this.apiService.api.subscribe(res => {/*your code*/})
実際、応答をキャッシュし、単一のサブスクリプションを共有する(すべてのサブスクライバーに新しいリクエストを作成しない)最も簡単な方法は、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
はその親に対するサブスクリプションを1つだけ維持し、サブスクライバーがいない場合はサブスクリプションを解除します。 take(1)
は、単一の値の後にチェーンを適切に完了するために必要です。
最も重要な部分は、このチェーンにサブスクライブすると、publishReplay
がサブスクリプションでバッファを発行し、キャッシュされた値が含まれている場合、チェーンを完了するtake(1)
にすぐに伝播されるためです。 _this.http.get
_へのサブスクリプションをまったく作成しません。 publishReplay
に何も含まれていない場合、そのソースにサブスクライブし、HTTPリクエストを作成します。
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);
}
});
};
}