RxJ 5でAngular Httpネットワークコールの結果を共有する正しい方法は何ですか?


303

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();
}

しかし、これはRxJ 5でこれを行う慣用的な方法ですか、それとも代わりに何か他のことをする必要がありますか?

注:Angular 5 newに従って、JSON結果がデフォルトで想定されるようになったため、すべての例HttpClient.map(res => res.json())部分は役に立たなくなりました。


1
>共有はpublish()。refCount()と同じです。実際にはそうではありません。次のディスカッションを参照してください:github.com/ReactiveX/rxjs/issues/1363
Christian

1
問題によると、編集された質問は、コードのドキュメントを更新する必要があるようです-> github.com/ReactiveX/rxjs/blob/master/src/operator/share.ts
Angular University

「依存する」と思います。ただし、ローカルでデータをキャッシュできない呼び出しの場合、パラメーターの変更/組み合わせにより、意味がなくなる場合があります。.share()は完全に正しいようです。しかし、ローカルにキャッシュできる場合は、ReplaySubject / BehaviorSubjectに関する他の回答のいくつかも良い解決策です。
JimB

データをキャッシュするだけでなく、キャッシュされたデータを更新/変更する必要があると思います。それは一般的なケースです。たとえば、キャッシュされたモデルに新しいフィールドを追加する場合や、フィールドの値を更新する場合などです。たぶん 、CRUDメソッドでシングルトンDataCacheService作成する方が良い方法でしょうか?Reduxののように。どう思いますか?
slideshowp2 2017

単にngx-cacheableを使用できます!シナリオに適しています。以下の私の回答を参照してください
Tushar Walzade

回答:


230

データをキャッシュし、使用可能な場合はこれを返します。それ以外の場合は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です。


3
do()逆にmap()イベントを変更しません。使用することもできますmap()が、コールバックの最後に正しい値が返されることを確認する必要がありました。
ギュンターZöchbauer

3
を行う呼び出しサイトが.subscribe()値を必要としない場合、それはnull(何this.extractDataが返されるかによって)取得される可能性があるため、それを行うことができますが、IMHOこれはコードの意図を適切に表現しません。
ギュンターZöchbauer

2
それ以外の場合にthis.extraData終了するextraData() { if(foo) { doSomething();}}と、最後の式の結果が返されますが、これは希望とは異なる場合があります。
ギュンターZöchbauer

9
@ギュンター、コードをありがとう、それは動作します。ただし、なぜデータと監視可能データを別々に追跡しているのかを理解しようとしています。このようにObservable <Data>だけをキャッシュすることで、同じ効果を効果的に達成しませんか?if (this.observable) { return this.observable; } else { this.observable = this.http.get(url) .map(res => res.json().data); return this.observable; }
July.Tech

3
@HarleenKaur強力な型チェックとオートコンプリートを取得するために、受信したJSONがデシリアライズされるクラスです。使用する必要はありませんが、一般的です。
ギュンターZöchbauer

44

@Cristianの提案によると、これはHTTPオブザーバブルでうまく機能する1つの方法であり、1回だけ発行してから完了するものです。

getCustomer() {
    return this.http.get('/someUrl')
        .map(res => res.json()).publishLast().refCount();
}

このアプローチの使用にはいくつかの問題があります-返されたオブザーバブルはキャンセルまたは再試行できません。これはあなたにとって問題ではないかもしれませんが、その後再び問題になるかもしれません。これが問題である場合、share演算子は妥当な選択である可能性があります(ただし、厄介なエッジケースがあります)。オプションの深いダイビングの議論については、このブログの記事のコメントセクションを参照してください。blog.jhades.org/...
クリスチャン

1
小さな説明...によって共有されているソースオブザーバブルを厳密publishLast().refCount()にキャンセルすることはできませんが、によって返されたオブザーバブルへのすべてのサブスクリプションがキャンセルされると、ソースオブザーバブルのサブスクリプションrefCountが取り消され、 "インフライト"の場合はキャンセルされます
クリスチャン

@クリスチャンねえ、「キャンセルもリトライもできない」ってどういう意味?ありがとう。
未定義

37

更新:Ben Leshは、5.2.0以降の次のマイナーリリースでは、shareReplay()を呼び出すだけで本当にキャッシュできるようになると述べています。

以前に.....

まず、share()またはpublishReplay(1).refCount()を使用しないでください。これらは同じであり、問​​題があります。オブザーバブルがアクティブなときに接続が確立された場合にのみ共有されることです。 、それは実際にキャッシングではなく、新しいオブザーバブル、つまり翻訳を作成します。

Birowskiは、ReplaySubjectを使用するという正しい解決策を上に示しました。この例では、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演算子として使用しない理由はありますconst data$ = this._http.get('url').pipe(cacheable()); /*1st subscribe*/ data$.subscribe(); /*2nd subscribe*/ data$.subscribe();か?したがって、他の演算子と同じように動作します
フェリックス

31

rxjs 5.4.0には新しいshareReplayメソッドがあります。

著者は「AJAX結果のキャッシュなどの処理に理想」と明確に述べています

rxjs PR#2443 feat(shareReplay):のshareReplayバリアントを追加publishReplay

shareReplayは、ReplaySubjectを介してマルチキャストされるソースであるオブザーバブルを返します。そのリプレイサブジェクトは、ソースからのエラー時にリサイクルされますが、ソースの完了時にはリサイクルされません。これにより、shareReplayは再試行可能であるため、AJAX結果のキャッシュなどの処理に最適です。ただし、繰り返しの動作は共有とは異なり、ソースオブザーバブルを繰り返しません。ソースオブザーバブルの値を繰り返します。


これに関連していますか?これらのドキュメントは2014年のものです。github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/…–
アーロンホフマン

4
オブザーバブルに.shareReplay(1、10000)を追加してみましたが、キャッシュや動作の変更に気付きませんでした。実用的な例はありますか?
Aydus-Matthew 2017

変更ログgithub.com/ReactiveX/rxjs/blob/を見てください。以前に表示され、v5で削除され、5.4で追加されました。rx -bookリンクはv4を参照しますが、現在のLTS v5.5.6およびそれはv6です。そこにあるrx-bookリンクが古くなっていると思います。
Jason Awbrey 2018

25

この記事によると

publishReplay(1)とrefCountを追加することで、オブザーバブルにキャッシュを簡単に追加できることがわかりました。

だから ifステートメントの中に追加するだけ

.publishReplay(1)
.refCount();

.map(...)


11

rxjsバージョン5.4.0(2017-05-09)は、shareReplayのサポートを追加します

shareReplayを使用する理由

複数のサブスクライバー間で実行したくない副作用や課税計算がある場合は、通常、shareReplayを使用します。また、以前に発行された値にアクセスする必要があるストリームへのサブスクライバーが遅れることがわかっている場合にも役立ちます。サブスクリプションで値を再生するこの機能は、shareとshareReplayを区別するものです。

これを使用するように角度サービスを簡単に変更し、キャッシュされた結果でオブザーバブルを返すことができます。

Angularサービスの例

を使用する非常にシンプルなカスタマーサービスを次に示します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が、http呼び出しはへの最初の呼び出しでのみ行われるため、コンストラクターでこれを行うことHttpClientは「コールド」なので、コンストラクターでこれを行うことは許容されsubscribeます。

また、ここでの前提は、最初に返されたデータがアプリケーションインスタンスの存続期間中に古くならないことです。


私はこのパターンが本当に好きで、多くのアプリケーションで使用するAPIサービスの共有ライブラリ内に実装したいと考えています。1つの例はUserServiceであり、2か所を除くすべての場所でアプリの有効期間中にキャッシュを無効にする必要はありませんが、そのような場合、以前のサブスクリプションを孤立させることなく、キャッシュを無効にする方法を教えてください。
SirTophamHatt

10

質問にスターを付けましたが、これを試してみます。

//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);
  });
}

ここに証明があります :)

唯一のポイントがあります: getCustomer().subscribe(customer$)

私たちはのAPI応答をサブスクライブしていません。getCustomer()別のObservableをサブスクライブすることもでき、(これは重要です)、最後に発行された値を保持して、そのいずれかに再発行することができる、ReplaySubjectをサブスクライブしています(ReplaySubject's )購読者。


1
rxjsをうまく利用し、カスタムロジックを追加する必要がないので、このアプローチが好きです。ありがとう
Thibs

7

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();
        });
    }
  }
}

セッションストレージに保存する場合、アプリを終了するときにセッションストレージが破棄されることをどのように確認しますか?
ギャグ

しかし、これはユーザーに予期しない動作をもたらします。ユーザーがブラウザのF5キーまたは更新ボタンを押すと、サーバーからの新しいデータを期待します。しかし実際には、彼はlocalStorageから古いデータを取得しています。バグレポート、サポートチケットなどの着信...名前が示すsessionStorageように、セッション全体で一貫性が期待されるデータにのみ使用します。
Martin Schneider

「使用制限を回避するために使用しました」と述べた@ MA-Maddin。F5でデータを更新する場合は、sessionStorageの代わりにメモリを使用する必要があります。回答はこのアプローチで編集されています。
allenhwkim 2018年

うん、それはユースケースかもしれません。誰もがキャッシュについて話していて、OPがgetCustomer彼の例で持っているので、私はちょうどトリガーされました。;)だから、リスクが見えないかもしれないpplに警告したかっただけです:)
Martin Schneider

5

選択する実装は、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;
}

こんにちは@Arlo-上記の例はコンパイルされません。Property 'connect' does not exist on type '{}'.ラインからreturnValue.connect();。詳しく説明できますか?

4

Rxjs Observer / Observable + Caching + Subscriptionを使用したキャッシュ可能なHTTP応答データ

以下のコードを参照してください

*免責事項:私はrxjsを初めて使用するので、観察可能/観察者アプローチを誤用している可能性があることに注意してください。私の解決策は純粋に私が見つけた他の解決策の集まりであり、単純な十分に文書化された解決策を見つけることができなかった結果です。したがって、私は他の人を助けることを期待して、(私が見つけたかった)私の完全なコードソリューションを提供しています。

*注、このアプローチは大まかにGoogleFirebaseObservablesに基づいています。残念ながら、私は彼らが内部でやったことを再現するための適切な経験/時間を欠いています。ただし、以下は、キャッシュ可能なデータへの非同期アクセスを提供する単純な方法です。

状況:「製品​​リスト」コンポーネントには、製品のリストを表示するタスクがあります。このサイトは、ページに表示されている商品を「フィルター」するいくつかのメニューボタンを持つ単一ページのWebアプリです。

解決策:コンポーネントはサービスメソッドに「サブスクライブ」します。serviceメソッドは、コンポーネントがサブスクリプションコールバックを通じてアクセスする製品オブジェクトの配列を返します。サービスメソッドは、新しく作成されたオブザーバーでアクティビティをラップし、オブザーバーを返します。このオブザーバー内で、キャッシュされたデータを検索し、それをサブスクライバー(コンポーネント)に返し、戻ります。それ以外の場合は、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でローカルに実行されているノードレストサービスを呼び出します)。次にクリックして製品の「フィルターされた」ビューに移動すると、製品がキャッシュに見つかります。

私の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応答データを実装する最も簡単な方法です。私の角度付きアプリでは、製品の別のビューに移動するたびに、製品リストコンポーネントが再ロードされます。ProductServiceは共有インスタンスのようです。そのため、ProductService内の「products:Product []」のローカルキャッシュはナビゲーション中に保持され、その後の「GetProducts()」の呼び出しはキャッシュされた値を返します。最後に、「メモリリーク」を防ぐために終了時にオブザーバブル/サブスクリプションを閉じる方法についてのコメントを読みました。ここには含まれていませんが、覚えておく必要があります。


1
注-その後、RxJS BehaviorSubjectsを含むより強力なソリューションを見つけました。これにより、コードが簡略化され、「オーバーヘッド」が大幅に削減されます。products.service.tsで、1。 'rxjs'から{BehaviorSubject}をインポートします。2.「products:Product []」を「product $:BehaviorSubject <Product []> = new BehaviorSubject <Product []>([]);」に変更します 3.これで、何も返さずに単にhttpを呼び出すことができます。http_getProducts(){this.http.get(...)。map(res => res.json())。subscribe(products => this.product $ .next(products))};
ObjectiveTC

1
ローカル変数 'product $'はbehaviorSubjectであり、(パート3のproduct $ .next(..)呼び出しから)最新の製品をEMITおよびSTOREします。これでコンポーネントに、通常どおりサービスを挿入します。productService.product $ .valueを使用して、product $の最後に割り当てられた値を取得します。または、product $が新しい値を受け取るたびにアクションを実行する場合は、product $にサブスクライブします(つまり、product $ .next(...)関数がパート3で呼び出されます)。
ObjectiveTC

1
例:products.component.ts ... this.productService.product $ .takeUntil(this.ngUnsubscribe).subscribe((products)=> {this.category); FilteredProducts = this.productService.getProductsByCategory(this.category);を許可します。this.products = FilteredProducts; });
ObjectiveTC

1
オブザーバブルの登録解除に関する重要な注意事項:「.takeUntil(this.ngUnsubscribe)」。このスタックオーバーフローの質問/回答をご覧ください。イベントの登録を解除するための「事実上の」推奨方法を示しているようです。stackoverflow.com
ObjectiveTC

1
オブザーバブルがデータを1回だけ受信することを意図している場合は、.first()または.take(1)の代わりになります。オブザーバブルのその他すべての「無限ストリーム」は「ngOnDestroy()」でサブスクライブ解除する必要があります。サブスクライブしないと、「オブザーバブル」コールバックが重複する可能性があります。 stackoverflow.com/questions/28007777/...
ObjectiveTC

3

私がいることを前提とし@ NGX-キャッシュ/コアは、 HTTP呼び出しが両方で行われている場合は特に、HTTP呼び出しのためのキャッシング機能を維持するために有用である可能性ブラウザーサーバーのプラットフォーム。

次のメソッドがあるとします。

getCustomer() {
  return this.http.get('/someUrl').map(res => res.json());
}

@ ngx-cache / coreCachedデコレーターを使用して、HTTP呼び出しを行うメソッドからの戻り値を(最初の実行時にng-seed / universalで実装を確認してください)で格納できますこれは構成可能です)。次にメソッドが呼び出されたときに(ブラウザまたはサーバープラットフォームに関係なく)、値はから取得されます。cache storagestoragecache storage

import { Cached } from '@ngx-cache/core';

...

@Cached('get-customer') // the cache key/identifier
getCustomer() {
  return this.http.get('/someUrl').map(res => res.json());
}

方法(キャッシュを使用する可能性もありますhasgetset使用した)キャッシュAPIが

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';
  }
}

クライアント側とサーバー側の両方のキャッシュ用のパッケージのリストを次に示します。


1

rxjs 5.3.0

私は満足していません .map(myFunction).publishReplay(1).refCount()

複数のサブスクライバーで、場合によって.map()myFunction2回実行されます(1回だけ実行されると期待しています)。1つの修正があるようですpublishReplay(1).refCount().take(1)

あなたができる別のことは、単に使用refCount()せず、Observableをすぐにホットにすることです:

let obs = this.http.get('my/data.json').publishReplay(1);
obs.connect();
return obs;

これにより、サブスクライバーに関係なくHTTP要求が開始されます。HTTP GETが完了する前に登録を解除すると、キャンセルされるかどうかはわかりません。


1

私たちがしたいことは、これが複数のネットワーク要求を引き起こさないことを確認することです。

私の個人的なお気に入りは、asyncネットワーク要求を行う呼び出しにメソッドを使用することです。メソッド自体は値を返さずBehaviorSubject、コンポーネントがサブスクライブする同じサービス内のを更新します。

なぜのBehaviorSubject代わりにを使用するのObservableですか?なぜなら、

  • サブスクリプション時にBehaviorSubjectは最後の値を返しますが、通常のオブザーバブルはを受信したときにのみトリガーされますonnext
  • (サブスクリプションなしの)監視不能コードでBehaviorSubjectの最後の値を取得する場合は、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:通常、コンポーネントが破棄された場合、サブスクリプションを削除することをお勧めします。そのため、この回答で提案されている方法を使用できます。


1

素晴らしい答え。

またはこれを行うことができます:

これは、rxjsの最新バージョンからのものです。5.5.7バージョンのRxJSを使用しています

import {share} from "rxjs/operators";

this.http.get('/someUrl').pipe(share());

0

マップの後でサブスクライブする前に、share()を呼び出すだけです。

私の場合、残りの呼び出しを行い、データを抽出し、エラーをチェックして、オブザーバブルを具体的な実装サービス(f.ex .: ContractClientService.ts)に返し、最後にこの具体的な実装を行う汎用サービス(RestClientService.ts)があります。監視可能をde ContractComponent.tsに返し、これはビューを更新するためにサブスクライブします。

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;
   });
  }

}

0

私はキャッシュクラスを書きました、

/**
 * 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がずっと1つのインスタンスを保持するかどうかはわかりません(Angular2の新機能)。

そして、これは私がそれを使う方法です:

            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トリックを使用するより賢い方法があると思いますが、これは私の目的には問題ありませんでした。


0

このキャッシュレイヤーを使用するだけで、必要なすべてのことが実行され、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]);
                     }
                     }
                 );
             }
            });
        }
}

0

それはだ.publishReplay(1).refCount();か、.publishLast().refCount();角度のHttp観測要求の後に完成以来。

この単純なクラスは結果をキャッシュするため、.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);
  }

}

0

複数のサブスクライバーを持つhttpサーバーから取得したデータの管理に役立つ単純なクラスCacheable <>を作成できます。

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


0

単に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
    }
}

詳細はこちらのリンクをご覧ください。


-4

すでに持っているコードを実行してみましたか?

の結果のpromiseからObservableを構築しているためgetJSON()、誰もがサブスクライブする前にネットワーク要求が行われます。そして、結果の約束はすべての加入者によって共有されます。

var promise = jQuery.getJSON(requestUrl); // network call is executed now
var o = Rx.Observable.fromPromise(promise); // just wraps it in an observable
o.subscribe(...); // does not trigger network call
o.subscribe(...); // does not trigger network call
// ...

質問を編集してAngular 2固有にしました
Angular University
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.