Httpを使用して、ネットワーク呼び出しを行い、httpオブザーバブルを返すメソッドを呼び出します。
getCustomer() {
return this.http.get('/someUrl').map(res => res.json());
}
この観測量を取り、それに複数の加入者を追加すると、
let network$ = getCustomer();
let subscriber1 = network$.subscribe(...);
let subscriber2 = network$.subscribe(...);
私たちがやりたいことは、これが複数のネットワーク要求を引き起こさないようにすることです。
これは珍しいシナリオのように思えるかもしれませんが、実際には非常に一般的です。たとえば、呼び出し側がエラーメッセージを表示するためにオブザーバブルをサブスクライブし、非同期パイプを使用してテンプレートに渡す場合、すでに2人のサブスクライバがいます。
RxJs 5でそれを行うための正しい方法は何ですか?
つまり、これでうまくいくようです。
getCustomer() {
return this.http.get('/someUrl').map(res => res.json()).share();
}
しかし、これはRxJs 5でこれを行う慣用的な方法なのでしょうか。
注:Angular 5 new HttpClient
に従って、JSONの結果はデフォルトで想定されるようになったため、すべての例の.map(res => res.json())
の部分は使用できなくなりました。
データをキャッシュし、キャッシュされていればそれを返し、そうでなければ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;
}
}
}
この記事 https://blog.thoughtram.io/angular/2018/03/05/advanced-caching-with-rxjs.html _はshareReplay
でキャッシュする方法についての素晴らしい説明です。
@Cristianの提案によると、これはHTTPオブザーバブルに適した1つの方法です。
getCustomer() {
return this.http.get('/someUrl')
.map(res => res.json()).publishLast().refCount();
}
アップデート:Ben Leshが5.2.0以降の次のマイナーリリースで、本当にcacheReplay()を呼び出せるようになるだろうと言っている。
前.....
まず、share()やpublishReplay(1).refCount()を使用しないでください。これらは同じで、問題は、観察可能オブジェクトがアクティブなときに接続が行われた場合にのみ共有されることです。つまり、それは実際にはキャッシュされているのではなく、再び新しい観察可能な翻訳を作成します。
Birowskiは上記の正しい解決法を与えました、それはReplaySubjectを使うことです。私たちの場合1では、ReplaySubjectはあなたがそれに与えた値(bufferSize)をキャッシュします。refCountがゼロに達し、新しい接続をすると、それはshare()のような新しい観測可能オブジェクトを作成しません。
これが再利用可能な機能です。
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();
}
使い方はこちら
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'));
}
}
以下はキャッシュ可能な関数のより高度なバージョンです。これは独自のルックアップテーブル+カスタムルックアップテーブルを提供する機能を持ちます。こうすれば、上記の例のようにthis._cacheをチェックする必要はありません。また、最初の引数としてオブザーバブルを渡す代わりに、オブザーバブルを返す関数を渡します。これは、AngularのHttpがすぐに実行されるためです。遅延実行関数を返すことで、既に実行中の場合は呼び出さないことができます。私たちのキャッシュ.
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;
}
使用法:
getData() => cacheable(this._http.get("YOUR URL"), "this is key for my cache")
rxjs 5.4.0 に新しい shareReplay メソッドが追加されました。
作者は明示的に"キャッシングAJAXのようなものを扱うのに理想的です"と言います
rxjs PR#2443 feat(shareReplay):shareReplay
のpublishReplay
バリアントを追加
shareReplayは、ReplaySubject上でマルチキャストされたソースであるオブザーバブルを返します。その再生サブジェクトはソースからのエラー時にリサイクルされますが、ソースの完成時にはリサイクルされません。これにより、shareReplayはAJAX結果のキャッシュなどの処理に理想的になります。それは繰り返しの振る舞いですが、それは観察可能なソースを繰り返すのではなく、観察可能なソースの値を繰り返すという点でshareとは異なります。
これによると 記事
PublishReplay(1)とrefCountを追加することで、オブザーバブルにキャッシュを簡単に追加できることがわかりました。
so if文の内側 を追加するだけ
.publishReplay(1)
.refCount();
.map(...)
へ
私は質問に主演したが、私はこれを試してみるつもりだ。
//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);
});
}
これが プルーフ :)です
たった1つのテイクアウトがあります:getCustomer().subscribe(customer$)
私たちはgetCustomer()
のapiレスポンスを購読しているのではなく、観察可能なReplaySubjectを購読しています。これは異なるObservableを購読することもできます(そしてこれは重要です)。 ReplaySubjectの)購読者。
選択した実装は、unsubscribe()でHTTP要求をキャンセルするかどうかによって異なります。
いずれにせよ、 TypeScriptデコレータ は動作を標準化するための良い方法です。これは私が書いたものです:
@CacheObservableArgsKey
getMyThing(id: string): Observable<any> {
return this.http.get('things/'+id);
}
デコレータの定義:
/**
* 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;
}
Http getの結果をsessionStorageに格納し、それをセッションに使用する方法を見つけました。これにより、サーバーが再度呼び出されることはありません。
使用制限を回避するためにgithub APIを呼び出すために使用しました。
@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();
});
}
}
}
参考までに、sessionStorageの上限は5M(または4.75M)です。そのため、大量のデータに対してこのように使用しないでください。
------編集-------------
F5でデータを更新したい場合は、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();
});
}
}
}
Rxjs Observer/Observable + Caching + Subscriptionを使用したキャッシュ可能なHTTPレスポンスデータ
以下のコードを参照
*免責事項:私はrxjsに慣れていないので、観察可能/オブザーバのアプローチを誤用しているかもしれないことに注意してください。私の解決策は純粋に私が見つけた他の解決策の集まりであり、そして簡単でよく文書化された解決策を見つけられなかった結果である。それで、私はそれが他の人たちを助けることを願って私が(私が見つけたことが好きだったように)私の完全なコードソリューションを提供しています。
*注意してください、このアプローチはおおまかにGoogleFirebaseObservablesに基づいています。残念ながら、私は彼らがフードの下でしたことを再現するための適切な経験/時間を欠いています。しかし、以下はキャッシュ可能なデータへの非同期アクセスを提供するための単純な方法です。
シチュエーション : 'product-list'コンポーネントは製品のリストを表示することを任されています。このサイトは、ページに表示される商品を「フィルタリング」するいくつかのメニューボタンを備えた単一ページのWebアプリです。
解決策 :コンポーネントはサービスメソッドに「加入」しています。サービスメソッドは製品オブジェクトの配列を返し、コンポーネントはそれにサブスクリプションコールバックを通してアクセスします。サービスメソッドは、そのアクティビティを新しく作成されたObserverにラップし、そのobserverを返します。このオブザーバの内部では、キャッシュされたデータを検索し、それをサブスクライバ(コンポーネント)に渡して戻ります。そうでなければ、それはデータを検索するためにhttp呼び出しを発行し、あなたがそのデータを処理し(例えばあなた自身のモデルにデータをマッピングする)そしてそれからデータを購読者に戻すことができる応答に購読する。
コード
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(モデル)
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...
);
}
}
これは、Chromeでページを読み込んだときに表示される出力例です。初期ロード時に、製品はhttp(ポート3000でローカルに実行されている私のnode restサービスへの呼び出し)から取り出されることに注意してください。それから私がクリックして商品の「フィルターされた」ビューにナビゲートすると、商品はキャッシュに入っています。
私のChromeログ(コンソール):
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
... [商品を絞り込むためにメニューボタンをクリックしました] ...
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
結論:これは、キャッシュ可能なhttp応答データを実装するための(これまでの)最も簡単な方法です。私のAngularアプリでは、商品の別のビューに移動するたびに商品リストコンポーネントがリロードされます。 ProductServiceは共有インスタンスのように見えるため、ProductService内の 'products:Product []'のローカルキャッシュはナビゲーション中も保持され、それ以降の "GetProducts()"の呼び出しではキャッシュされた値が返されます。最後に、「メモリリーク」を防ぐために、オブザーバブル/サブスクリプションを閉じるときのクローズ方法についてのコメントを読みました。ここには記載していませんが、覚えておくべきことです。
HTTP呼び出しが browser と server platformの両方で行われている場合は特に、 @ ngx-cache/core はhttp呼び出しのキャッシュ機能を維持するのに役立ちます。 。
次のような方法があるとしましょう。
getCustomer() {
return this.http.get('/someUrl').map(res => res.json());
}
@ ngx-cache/core のCached
デコレータを使用して、HTTP呼び出しを行うメソッドからの戻り値をcache storage
(storage
は設定可能にすることができます。 ngで実装を確認してください。) -seed/universal) - 最初の実行直後。次にメソッドが呼び出されると( browser または server platformに関係なく)、値は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());
}
キャッシングAPI を使ってキャッシングメソッド(has
、get
、set
)を使うことも可能です。
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';
}
}
これは、クライアント側とサーバー側の両方のキャッシュ用のパッケージのリストです。
素晴らしい答えです。
それとも、これを行うことができます:
これはrxjsの最新バージョンからのものです。 5.5.7 を使用しています RxJS
import {share} from "rxjs/operators";
this.http.get('/someUrl').pipe(share());
rxjsバージョン5.4.0(2017-05-09) は、 shareReplay のサポートを追加しました。
なぜshareReplayを使うのですか?
一般的に、複数の加入者の間で実行したくない副作用や課税の計算がある場合は、shareReplayを使用します。また、以前に発行された値にアクセスする必要があるストリームを最近購読しているユーザーがいることがわかっている場合にも役立ちます。サブスクリプションで値を再生するこの機能は、shareとshareReplayを区別するものです。
これを使用するように角度付きサービスを簡単に変更し、http呼び出しを1回しか実行しないキャッシュされた結果を持つオブザーバブルを返すことができます(1回目の呼び出しが成功したと仮定)。
これは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 */
}
コンストラクタ内の代入はメソッドgetCustomers
に移動することができますが、 HttpClient
から返されるオブザーバブルは "コールド"です これはコンストラクタ内で行うことは受け入れられます。 subscribe
を呼び出します。
ここでの前提は、最初に返されたデータがアプリケーションインスタンスの有効期間内に古くなることはないということです。
rxjs 5.3.0
私は.map(myFunction).publishReplay(1).refCount()
に満足していません
複数のサブスクライバの場合、.map()
はmyFunction
を2回実行することがあります(私は1回しか実行しないと思います)。 1つの修正はpublishReplay(1).refCount().take(1)
のようです
もう1つできることは、refCount()
を使用せずにObservableをすぐにホットにすることです。
let obs = this.http.get('my/data.json').publishReplay(1);
obs.connect();
return obs;
これは加入者に関係なくHTTPリクエストを開始します。 HTTP GETが完了する前に購読を解除してもキャンセルされるかどうかわかりません。
私たちがやりたいことは、これが複数のネットワーク要求を引き起こさないようにすることです。
私の個人的なお気に入りは、ネットワーク要求を行う呼び出しにasync
メソッドを利用することです。メソッド自体は値を返さず、代わりにコンポーネントがサブスクライブする同じサービス内でBehaviorSubject
を更新します。
では、なぜBehaviorSubject
の代わりにObservable
を使うのですか?だから、
onnext
を受け取ったときにのみトリガされます。getValue()
メソッドを使用できます。例:
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);
}
その後、必要に応じてcustomers$
を購読するだけです。
public ngOnInit(): void {
this.customerService.customers$
.subscribe((customers: Customer[]) => this.customerList = customers);
}
または、テンプレートで直接使用したい場合もあります。
<li *ngFor="let customer of customerService.customers$ | async"> ... </li>
そのため、getCustomers
をもう一度呼び出すまで、データはcustomers$
BehaviorSubjectに保持されます。
それでは、このデータを更新したい場合はどうなりますか? getCustomers()
を呼び出すだけです。
public async refresh(): Promise<void> {
try {
await this.customerService.getCustomers();
}
catch (e) {
// request failed, handle exception
console.error(e);
}
}
このメソッドを使用すると、BehaviorSubject
によって処理されるため、後続のネットワーク呼び出し間でデータを明示的に保持する必要はありません。
PS: 通常、コンポーネントが破壊されたときは、 this answerで提案されている方法を使うことができるので、購読を取り除くのは良い習慣です。
このキャッシュレイヤを使用するだけで、必要なことはすべて実行できます。さらに、Ajaxリクエスト用のキャッシュを管理することもできます。
http://www.ravinderpayal.com/blogs/12Jan2017-Ajax-Cache-Mangement-Angular2-Service.html
これはとても使いやすいです
@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"}]};
}
レイヤ(注入可能な角度サービスとして)は
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]);
}
}
);
}
});
}
}
単純なクラスCacheable <>を構築して、httpサーバーから取得したデータを複数の加入者と一緒に管理することができます。
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();
}
}
使用方法
(おそらくサービスの一部として)Cacheable <>オブジェクトを宣言します。
list: Cacheable<string> = new Cacheable<string>();
そしてハンドラ:
this.list.getHandler = () => {
// get data from server
return this.http.get(url)
.map((r: Response) => r.json() as string[]);
}
コンポーネントから呼び出す:
//gets data from server
List.getData().subscribe(…)
いくつかのコンポーネントを購読することができます。
詳細とコード例はこちら: http://devinstance.net/articles/20171021/rxjs-cacheable
share() を map の後、そして subscribe の前に呼び出すだけです。
私の場合は、残りの呼び出しを行い、データを抽出し、エラーをチェックし、観察可能なものを具象実装サービス(f.ex。:ContractClientService.ts)に返す汎用サービス(RestClientService.ts)があります。 de ContractComponent.tsにobservableを返し、これはビューを更新するために購読します。
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;
});
}
}
これがまさに私がライブラリngx-rxcacheを作成したものです。
https://github.com/adriandavidbrand/ngx-rxcache でそれを見てください、そして https://stackblitz.com/edit/angular-jxqaiv で実用的な例を見てください
Angular Httpオブザーバブルはリクエスト後に完了するので、.publishReplay(1).refCount();
または.publishLast().refCount();
です。
この単純なクラスは結果をキャッシュするので、あなたは.valueを何度も購読することができて、1つの要求だけをします。また、.reload()を使用して、新しい要求を出してデータを公開することもできます。
あなたはそれを使うことができます:
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);
});
そして源:
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);
}
}
私はキャッシュクラスを書きました、
/**
* 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>();
}
}
使い方は静的ですが、気軽に普通のクラスやサービスにしてください。ただし、Angularが常に単一のインスタンスを保持するかどうかはわかりません(Angular 2の新機能)。
そしてこれが私が使う方法です:
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;
}
私はもっと賢い方法があると思います、それはいくつかのObservable
のトリックを使うでしょうが、これは私の目的のためにちょうど良いものでした。
ngx-cacheableを使用することもできます。それはあなたのシナリオにより適しています。
これを使用する利点
- それは一度だけrest APIを呼び出し、レスポンスをキャッシュして&後続のリクエストに対して同じを返します。
- 作成/更新/削除操作後に必要に応じてAPIを呼び出すことができます。
だから、あなたのサービスクラスはこんな感じになるでしょう -
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
}
}
ここでは参考のためのリンクです。