RxJs 5'te Açısal Http ağ çağrısının sonucunu paylaşmanın doğru yolu nedir?


303

Http kullanarak, bir ağ çağrısı yapan ve bir http gözlemlenebilir döndüren bir yöntem çağırırız:

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

Bu gözlemlenebilir olanı alıp birden fazla abone eklersek:

let network$ = getCustomer();

let subscriber1 = network$.subscribe(...);
let subscriber2 = network$.subscribe(...);

Yapmak istediğimiz, bunun birden fazla ağ isteğine neden olmamasını sağlamaktır.

Bu alışılmadık bir senaryo gibi görünebilir, ancak aslında oldukça yaygındır: örneğin, arayan bir hata mesajı görüntülemek için gözlemlenebilir kişiye abone olursa ve zaman uyumsuz boruyu kullanarak şablona iletirse, zaten iki abonemiz var.

Bunu RxJs 5'te yapmanın doğru yolu nedir?

Yani, bu iyi çalışıyor gibi görünüyor:

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

Peki bu RxJs 5'te bunu yapmanın deyimsel yolu mu yoksa onun yerine başka bir şey yapmalı mıyız?

Not: Yeni açısal 5'e göre , JSON sonucu varsayılan olarak varsayıldığı için tüm örneklerde yer HttpClientalan .map(res => res.json())kısım artık işe yaramaz.


1
> paylaşım yayınla () aynıdır refCount (). Aslında öyle değil. Aşağıdaki tartışmaya bakın: github.com/ReactiveX/rxjs/issues/1363
Christian

1
düzenlenmiş soru, konuya göre kod üzerindeki belgelerin güncellenmesi gerekiyor gibi görünüyor -> github.com/ReactiveX/rxjs/blob/master/src/operator/share.ts
Angular University

Bence 'duruma bağlı'. Ancak, verileri yerel olarak önbelleğe alamadığınız çağrılar için parametrelerin değiştirilmesi / kombinasyonlar nedeniyle .share () kesinlikle doğru gibi görünüyor. Ancak, şeyleri yerel olarak önbelleğe alabiliyorsanız, ReplaySubject / BehaviorSubject ile ilgili diğer cevaplardan bazıları da iyi çözümlerdir.
JimB

Sadece verileri önbelleğe almamızın yanı sıra önbelleğe alınan verileri de güncellememiz / değiştirmeniz gerekiyor. Bu yaygın bir durum. Örneğin, önbelleğe alınan modele yeni bir alan eklemek veya alanın değerini güncellemek istersem. Belki CRUD yöntemi ile tek bir DataCacheService oluşturmak daha iyi bir yoludur? Gibi mağazanın içinde Redux . Ne düşünüyorsun?
Slideshowp2

Sadece ngx-cacheable kullanabilirsiniz ! Senaryonuza daha iyi uyuyor. Aşağıdaki
yanıtıma

Yanıtlar:


230

Verileri önbelleğe alın ve varsa önbelleğe alındı, aksi takdirde HTTP isteğini yapın.

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

Plunker örneği

Bu makale https://blog.thoughtram.io/angular/2018/03/05/advanced-caching-with-rxjs.html önbellekleme ile nasıl harika bir açıklamadır shareReplay.


3
do()aksine map()etkinliği değiştirmez. Siz de kullanabilirsiniz map(), ancak sonra geri aramanın sonunda doğru değerin döndürüldüğünden emin olmalısınız.
Günter Zöchbauer

3
Yapmayan çağrı sitesi .subscribe()değere ihtiyaç duymuyorsa, bunu alabilirsiniz null( çünkü ne this.extractDatadöndürdüğüne bağlı olarak ), ancak IMHO bu kodun amacını iyi ifade etmez.
Günter Zöchbauer

2
Ne zaman this.extraDatabiter mi extraData() { if(foo) { doSomething();}}istediğini olmayabilir döndürülen son ifadenin aksi sonucunu.
Günter Zöchbauer

9
@ Günter, kod için teşekkürler, işe yarıyor. Ancak, Veri ve Gözlemlenebilir neden ayrı ayrı izlediğinizi anlamaya çalışıyorum. Sadece Gözlemlenebilir <Veri> 'yi bu şekilde önbelleğe alarak etkili bir şekilde aynı etkiyi elde edemez misiniz? if (this.observable) { return this.observable; } else { this.observable = this.http.get(url) .map(res => res.json().data); return this.observable; }
Temmuz.Tek

3
@HarleenKaur Güçlü tip kontrolü ve otomatik tamamlama elde etmek için alınan JSON'un serileştirilmediği bir sınıftır. Kullanmaya gerek yoktur, ancak yaygındır.
Günter Zöchbauer

44

@Cristian önerisine göre, bu yalnızca bir kez yayan ve sonra tamamlanan HTTP gözlemlenebilirleri için iyi çalışan bir yoldur:

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

Bu yaklaşımı kullanmayla ilgili birkaç sorun var - iade edilen gözlemlenebilir iptal edilemez veya yeniden denenemez. Bu sizin için bir sorun olmayabilir, ama sonra tekrar olabilir. Bu bir sorunsa, shareoperatör makul bir seçim olabilir (bazı kötü durumlarda da olsa). Seçenekler hakkında derin bir tartışma için bu blog gönderisindeki yorumlar bölümüne bakın: blog.jhades.org/…
Christian

1
Küçük açıklama ... kesinlikle paylaştığı kaynak gözlemlenebilir varlık rağmen publishLast().refCount()iptal edilemez, tarafından döndürülen gözlemlenebilir tüm abonelikler kez refCountiptal edilmiş, net etkisi de iptal kaynak gözlemlenebilir abonelikten olacaktır eğer o burada "uçak içi"
Christian

@Christian Hey, ne demek istediğini "iptal edilemez veya yeniden denenemez" diyerek açıklayabilir misin? Teşekkürler.
tanımlanmamış

37

GÜNCELLEME: Ben Lesh, 5.2.0'dan sonraki bir sonraki küçük sürümde, gerçekten önbelleğe almak için shareReplay () öğesini çağırabileceğinizi söylüyor.

ÖNCEDEN.....

İlk olarak, share () veya publishReplay (1) .refCount () kullanmayın, bunlar aynıdır ve bununla ilgili sorun, yalnızca gözlemlenebilir etkinken bağlantılar yapıldığında, tamamlandıktan sonra bağlanırsanız paylaşmasıdır. , yeni bir gözlemlenebilir oluşturur, çeviri, gerçekten önbelleğe almaz.

Birowski, ReplaySubject'i kullanmak için yukarıda doğru çözümü verdi. ReplaySubject, bizim durumumuzda 1 (bufferSize) verdiğiniz değerleri önbelleğe alır. RefCount sıfıra ulaştığında ve önbellekleme için doğru davranış olan yeni bir bağlantı oluşturduğunuzda, share () gibi yeni bir gözlemlenebilir oluşturmaz.

İşte yeniden kullanılabilir bir fonksiyon

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

İşte nasıl kullanacağınız

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

Aşağıda önbelleğe alınabilir işlevinin daha gelişmiş bir sürümü bulunmaktadır Bu, kendi arama tablosuna + özel arama tablosu sağlama yeteneğine sahiptir. Bu şekilde, yukarıdaki örnekteki gibi this._cache öğesini kontrol etmeniz gerekmez. Ayrıca, gözlemlenebilir olanı ilk argüman olarak iletmek yerine, gözlemlenebilirleri döndüren bir işlevi ilettiğinize dikkat edin, bunun nedeni Angular'ın Http'sinin hemen yürütmesidir, bu nedenle tembel bir yürütme işlevi döndürerek, zaten varsa önbellek.

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

Kullanımı:

getData() => cacheable(this._http.get("YOUR URL"), "this is key for my cache")

Bu çözümü bir RxJ operatörü olarak kullanmamak için herhangi bir neden var mı const data$ = this._http.get('url').pipe(cacheable()); /*1st subscribe*/ data$.subscribe(); /*2nd subscribe*/ data$.subscribe();? Yani diğer operatörler gibi davranıyor ..
Felix

31

rxjs 5.4.0'ın yeni bir shareReplay yöntemi var.

Yazar açıkça "AJAX sonuçlarını önbelleğe almak gibi şeyleri işlemek için ideal" diyor

rxjs PR # 2443 feat (shareReplay): shareReplaydeğişkenini eklerpublishReplay

shareReplay bir ReplaySubject üzerinde çok noktaya yayınlanan kaynak olan gözlemlenebilir bir değer döndürür. Yeniden oynatma konusu, kaynaktan gelen hata nedeniyle geri dönüştürülür, ancak kaynağın tamamlanmasından sonra geri dönüştürülmez. Bu, shareReplay'ı, yeniden denenebilir olduğu için AJAX sonuçlarını önbelleğe almak gibi şeyleri işlemek için ideal hale getirir. Ancak tekrarlama davranışı, gözlemlenebilir kaynağı tekrar etmeyeceği, ancak gözlemlenebilir kaynak değerlerini tekrarlayacağı için paylaşımdan farklıdır.


Bununla ilgili mi? Bu dokümanlar 2014 yılından. github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/…
Aaron Hoffman

4
Gözlenebilir bir .shareReplay (1, 10000) eklemeyi denedim ama herhangi bir önbellekleme veya davranış değişikliği fark etmedim. Çalışan bir örnek var mı?
Aydus-Matthew

Değişmek baktığımızda github.com/ReactiveX/rxjs/blob/... Daha evvel çıktı 5.4'te geri eklenir, v5 içinde çıkarıldı - yani rx-kitap bağlantı v4 bakın, ancak mevcut LTS v5.5.6 mevcut olan ve v6'da. Rx-kitap bağlantısının güncel olmadığını hayal ediyorum.
Jason Awbrey

25

bu makaleye göre

PublishReplay (1) ve refCount ekleyerek gözlemlenebilirlere kolayca önbellek ekleyebileceğimiz ortaya çıkıyor.

böylece iç tablolar halinde sadece ekleme

.publishReplay(1)
.refCount();

için .map(...)


11

rxjs sürüm 5.4.0 (2017-05-09) shareReplay için destek ekler .

Neden shareReplay kullanıyorsunuz?

Birden fazla abone arasında yürütülmesini istemediğiniz yan etkilere veya vergi hesaplamalara sahip olduğunuzda genellikle shareReplay'i kullanmak istersiniz. Daha önce yayılan değerlere erişmesi gereken bir akışa geç aboneleriniz olacağını bildiğiniz durumlarda da değerli olabilir. Abonelikteki değerleri tekrar oynatma özelliği, share ve shareReplay'i farklı kılan şeydir.

Bunu kullanmak için açısal bir hizmeti kolayca değiştirebilir ve yalnızca http çağrısını tek bir kez yapacak olan (1. çağrının başarılı olduğunu varsayarak) önbelleğe alınmış bir sonuçla gözlemlenebilir bir geri dönüş yapabilirsiniz.

Örnek Açısal Hizmet

İşte kullanan çok basit bir müşteri hizmeti 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 */
}

Yapıcıdaki atamanın yönteme taşınabileceğini, getCustomersancak geri döndürülebilir gözlemlenebilir HttpClient"soğuk" olduğundan, yapıcıda bunu yapmak kabul edilebilir, çünkü http çağrısı yalnızca ilk çağrı ile yapılacaktır subscribe.

Ayrıca buradaki varsayım, ilk döndürülen verilerin uygulama örneğinin ömrü boyunca bayatlamamasıdır.


Bu kalıbı gerçekten seviyorum ve bunu bir dizi uygulamada kullandığım api hizmetlerinin paylaşılan bir kütüphanesinde uygulamak istiyorum. Bir örnek bir UserService'dir ve birkaç yer dışındaki her yerde uygulamanın ömrü boyunca önbelleği geçersiz kılmaya gerek yoktur, ancak bu durumlarda, önceki aboneliklerin yetim kalmasına neden olmadan onu geçersiz kılmaya nasıl devam edebilirim?
SirTophamHatt

10

Soruyu işaretledim, ama buna bir bakmaya çalışacağım.

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

İşte kanıt :

Sadece bir paket servisi var: getCustomer().subscribe(customer$)

API yanıtına getCustomer()abone olmuyoruz, gözlemlenebilir olan ve farklı bir Gözlemlenebilir'e de abone olabilen ve (ve bu önemli) son yayılan değerini tutun ve herhangi birisine yeniden yayınlayan bir ReplaySubject'e abone oluyoruz (ReplaySubject's ) aboneler.


1
Rxjs'i iyi kullandığı ve özel mantık eklemeye gerek olmadığı için bu yaklaşımı beğendim, teşekkür ederim
Thibs

7

Http get sessionStorage almak sonucu depolamak için bir yol buldum ve oturumu için kullanmak, böylece bir daha asla sunucu çağıracak.

Kullanım sınırını önlemek için github API'yi çağırmak için kullandım.

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

FYI, sessionStorage limiti 5M (veya 4.75M). Bu nedenle, büyük veri kümesi için bu şekilde kullanılmamalıdır.

------ edit -------------
FS ile verileri yenilemek istiyorsanız, sessionStorage yerine hafıza verilerini kullanır;

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

Depolama alanında oturum açacaksanız, uygulamadan çıktığınızda Oturum depolamasının imha edildiğinden nasıl emin olacaksınız?
Gags

ancak bu kullanıcı için beklenmedik davranışlar getirir. Kullanıcı tarayıcının F5 veya yenile düğmesine bastığında, sunucudan yeni veriler bekler. Ama aslında localStorage'dan eski veriler alıyor. Hata raporları, destek biletleri vs. gelen ... Adından da anlaşılacağı sessionStoragegibi, bunu yalnızca tüm oturum için tutarlı olması beklenen veriler için kullanırım .
Martin Schneider

@ MA-Maddin "Kullanım sınırından kaçınmak için kullandım" dediğim gibi. Verilerin F5 ile yenilenmesini istiyorsanız sessionStorage yerine bellek kullanmanız gerekir. Cevap bu yaklaşımla düzenlendi.
allenhwkim

evet, bu bir kullanım durumu olabilir. Herkes Cache ve OP'nin getCustomerörneğinde olduğundan bahsettiğim için tetiklendim . ;) Bu yüzden sadece riskleri
Martin Schneider

5

Seçtiğiniz uygulama, abonelikten çıkma () öğesinin HTTP isteğinizi iptal edip etmeyeceğinize bağlı olacaktır.

Her durumda, TypeScript dekoratörleri davranışı standartlaştırmanın güzel bir yoludur. Ben yazdım:

  @CacheObservableArgsKey
  getMyThing(id: string): Observable<any> {
    return this.http.get('things/'+id);
  }

Dekoratör tanımı:

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

Merhaba @Arlo - yukarıdaki örnek derlenmemiştir. Property 'connect' does not exist on type '{}'.çizgiden returnValue.connect();. Detaylandırabilir misin?
Toynak

4

Rxjs Gözlemci / Gözlemlenebilir + Önbellekleme + Abonelik Kullanılarak Önbelleğe Alınabilir HTTP Yanıt Verileri

Bkz. Aşağıdaki Kod

* Feragatname: Ben rxjs için yeniyim, bu yüzden gözlenebilir / gözlemci yaklaşımını kötüye kullandığımı unutmayın. Çözümüm tamamen bulduğum diğer çözümlerin bir araya gelmesi ve iyi belgelenmiş basit bir çözüm bulamamanın sonucudur. Böylece başkalarına yardım umuduyla (bulmayı isterdim gibi) tam kod çözümümü sağlıyorum.

* unutmayın, bu yaklaşım gevşek bir şekilde GoogleFirebaseObservables tabanlıdır. Ne yazık ki uygun deneyim / zaman başlık altında ne yaptığını çoğaltmak için eksikliği. Ancak aşağıdakiler, bazı önbelleklenebilir verilere eşzamansız erişim sağlamanın basit bir yoludur.

Durum : Bir 'ürün listesi' bileşeni, bir ürün listesi görüntülemekle görevlendirilir. Site, sayfada görüntülenen ürünleri 'filtreleyecek' bazı menü düğmelerine sahip tek sayfalık bir web uygulamasıdır.

Çözüm : Bileşen bir hizmet yöntemine "abone olur". Hizmet yöntemi, bileşenin abonelik geri araması yoluyla eriştiği bir dizi ürün nesnesi döndürür. Hizmet yöntemi, etkinliğini yeni oluşturulan bir Gözlemciye sarar ve gözlemciyi döndürür. Bu gözlemcinin içinde, önbelleğe alınmış verileri arar ve aboneye (bileşene) geri aktarır ve geri döner. Aksi takdirde, verileri almak için bir http çağrısı yapar, yanıta abone olur, burada verileri işleyebilir (örneğin verileri kendi modelinize eşleyebilirsiniz) ve ardından verileri aboneye geri aktarabilirsiniz.

Kod

Ürün-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 (model)

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

Sayfayı Chrome'a ​​yüklediğimde gördüğüm çıktıdan bir örnek. İlk yükte, ürünlerin http'den getirildiğini unutmayın (3000 numaralı bağlantı noktasında yerel olarak çalışan düğüm dinlenme hizmetime çağrı yapın). Daha sonra ürünlerin 'filtrelenmiş' görünümüne gitmek için tıkladığımda, ürünler önbellekte bulunur.

Chrome Günlüğüm (konsol):

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

... [ürünleri filtrelemek için bir menü düğmesini tıklattı] ...

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

Sonuç: Bu, şu ana kadar önbelleğe alınabilen http yanıt verilerini uygulamak için bulduğum en basit yöntem. Açısal uygulamamda, ürünlerin farklı bir görünümüne her gittiğimde, ürün listesi bileşeni yeniden yüklenir. ProductService paylaşılan bir örnek gibi görünüyor, bu nedenle ProductService içindeki 'products: Product []' yerel önbelleği gezinme sırasında korunur ve sonraki "GetProducts ()" çağrıları önbelleğe alınan değeri döndürür. Son bir not, 'bellek sızıntılarını' önlemek için tamamlandığında gözlemlenebilirlerin / aboneliklerin nasıl kapatılması gerektiği hakkında yorumları okudum. Bunu buraya dahil etmedim, ama akılda tutulması gereken bir şey.


1
Not - O zamandan beri kodu basitleştiren ve 'yükü' önemli ölçüde azaltan RxJS BehaviorSubjects içeren daha güçlü bir çözüm buldum. Products.service.ts dosyasında, 1. 'rxjs' öğesinden {BehaviorSubject} dosyasını içe aktarın; 2. 'products: Product []' değerini 'product $: BehaviorSubject <Product []> = new BehaviorSubject <Product []> ([]);' olarak değiştirin. 3. Artık hiçbir şey döndürmeden http'yi arayabilirsiniz. http_getProducts () {this.http.get (...). harita (res => res.json ()). abone ol (products => this.product $ .next (ürünler))};
ObjectiveTC

1
'Değişken $' yerel değişkeni, en son ürünleri hem EMIT hem de STORE (3. bölümde $ .next (..) ürününden çağrılacak) davranışı olan bir behaviorSubject öğesidir. Şimdi bileşenlerinize servisi normal şekilde enjekte edin. ProductService.product $ .value değerini kullanarak $ ürününün en son atanan değerini alırsınız. Veya ürün $ her yeni değer aldığında bir eylem gerçekleştirmek istiyorsanız $ ürününe abone olun (örn., Ürün. .Next (...) işlevi bölüm 3'te çağrılır).
ObjectiveTC

1
Örneğin, products.component.ts ... this.productService.product $ .takeUntil (this.ngUnsubscribe) .subscribe ((products) => {this.category); let filterProducts = this.productService.getProductsByCategory (this.category); this.products = filtrelenmişÜrünler; });
ObjectiveTC

1
Gözlenebilirlerden abonelikten çıkma hakkında önemli bir not: ".takeUntil (this.ngUnsubscribe)". Etkinliklerden çıkmak için 'fiili' önerilen yolu gösteren bu yığın taşması sorusuna / cevabına bakın: stackoverflow.com/questions/38008334/…
ObjectiveTC

1
Eğer gözlemlenebilir veri yalnızca bir kez alınırsa alternatif .first () veya .take (1) 'dir. Gözlenebilirlerin diğer tüm 'sonsuz akışları' 'ngOnDestroy ()' öğesinde abonelikten çıkarılmalıdır ve eğer bunu yapmazsanız, yinelenen 'gözlemlenebilir' geri çağrılarla karşılaşabilirsiniz. stackoverflow.com/questions/28007777/…
ObjectiveTC

3

Ben varsayalım @ NGX-cache / çekirdek HTTP çağrısı üzerine her iki yapıldığında, özellikle http aramalar için önbelleğe özelliklerini korumak için yararlı olabilir tarayıcı ve sunucu platformları.

Diyelim ki aşağıdaki yöntemimiz var:

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

Sen kullanabilirsiniz Cachedait dekoratör @ NGX-cache / çekirdek de HTTP çağrısı yapma yönteminden döndürülen değeri saklamak için cache storage( yapılandırılabilir olabilir, en uygulamasını kontrol edin ng-tohum / evrensel ilk yürütme hakkı -). Yöntem bir sonraki çağrıldığında ( tarayıcı veya sunucu platformunda olursa olsun ), değer .storagecache storage

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

...

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

Kullanım önbelleğe alma yöntemlerine (imkanı da var has, get, setkullanarak) önbelleğe alma 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';
  }
}

Hem istemci tarafı hem de sunucu tarafı önbellekleme için paketlerin listesi:


1

rxjs 5.3.0

Memnun kalmadım .map(myFunction).publishReplay(1).refCount()

Birden fazla abone ile, bazı durumlarda iki kez .map()yürütür myFunction(Ben sadece bir kez yürütmek bekliyoruz). Bir düzeltme,publishReplay(1).refCount().take(1)

Yapabileceğiniz başka bir şey, sadece refCount()Gözlemlenebilir'i kullanmamak ve sıcak yapmak değil:

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

Bu, abonelere bakılmaksızın HTTP isteğini başlatır. HTTP GET bitmeden aboneliğinizi iptal edip etmeyeceğinizden emin değilim.


1

Yapmak istediğimiz, bunun birden fazla ağ isteğine neden olmamasını sağlamaktır.

Kişisel favorim, asyncağ istekleri yapan aramalar için yöntemlerden faydalanmaktır. Yöntemlerin kendileri bir değer döndürmez, bunun yerine BehaviorSubjectbileşenlerin abone olacağı aynı hizmet içinde bir güncelleştirme yaparlar .

Şimdi neden bir BehaviorSubjectyerine Observable? Çünkü,

  • Abonelik üzerine BehaviorSubject son değeri döndürürken, düzenli bir gözlemlenebilir yalnızca bir onnext.
  • BehaviorSubject öğesinin son değerini gözlemlenemeyen bir kodda (abonelik olmadan) almak istiyorsanız, getValue()yöntemi kullanabilirsiniz .

Misal:

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

Daha sonra, gerektiğinde, sadece abone olabiliriz customers$.

public ngOnInit(): void {
    this.customerService.customers$
    .subscribe((customers: Customer[]) => this.customerList = customers);
}

Veya doğrudan bir şablonda kullanmak isteyebilirsiniz

<li *ngFor="let customer of customerService.customers$ | async"> ... </li>

Şimdi, başka bir çağrı yapana kadar getCustomers, veriler customers$BehaviorSubject içinde tutulur .

Peki ya bu verileri yenilemek isterseniz? sadece telefon etgetCustomers()

public async refresh(): Promise<void> {
    try {
      await this.customerService.getCustomers();
    } 
    catch (e) {
      // request failed, handle exception
      console.error(e);
    }
}

Bu yöntemi kullanarak, sonraki ağ çağrıları arasındaki verileri, tarafından işlendiği gibi açıkça tutmamız gerekmez BehaviorSubject.

Not: Genellikle bir bileşen yok edildiğinde aboneliklerden kurtulmak iyi bir uygulamadır, çünkü bu cevapta önerilen yöntemi kullanabilirsiniz .


1

Harika cevaplar.

Veya bunu yapabilirsiniz:

Bu rxjs en son sürümünden. Ben kullanıyorum 5.5.7 sürümünü RxJS

import {share} from "rxjs/operators";

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

0

Haritadan sonra ve abone olmadan önce share () işlevini çağırmanız yeterlidir .

Benim durumumda, geri kalan çağrıyı yapan, verileri ayıklayan, hataları kontrol eden ve somut bir uygulama hizmetine (örn. ContractClientService.ts) gözlemlenebilir geri dönen genel bir hizmetim var (RestClientService.ts) de ContractComponent.ts gözlemlenebilir döndürür ve bu görünüm güncelleştirmek için abone olun.

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

Bir önbellek sınıfı yazdım,

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

}

Bunu nasıl kullandığımız nedeniyle hepsi statik, ancak normal bir sınıf ve bir hizmet yapmaktan çekinmeyin. Açısal olarak tüm zaman boyunca tek bir örneği saklayıp saklamadığından emin değilim (Açısal2'ye yeni).

Ve ben böyle kullanıyorum:

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

Bazı Observablehileler kullanmak daha akıllıca bir yol olabilir varsayalım ama bu benim amacım için iyi oldu.


0

Sadece bu önbellek katmanını kullanın, ihtiyacınız olan her şeyi yapar ve hatta ajax istekleri için önbelleği yönetir.

http://www.ravinderpayal.com/blogs/12Jan2017-Ajax-Cache-Mangement-Angular2-Service.html

Kullanımı bu kadar kolay

@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"}]};
}

Katman (enjekte edilebilir açısal servis olarak)

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

Bu var .publishReplay(1).refCount();ya.publishLast().refCount(); Açısal Http gözlemlenebilir istek sonra tamamlandığından beri.

Bu basit sınıf sonucu önbelleğe alır, böylece .value öğesine birçok kez abone olabilir ve yalnızca 1 istekte bulunabilirsiniz. Ayrıca, yeni istekte bulunmak ve verileri yayınlamak için .reload () öğesini de kullanabilirsiniz.

Aşağıdaki gibi kullanabilirsiniz:

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

ve kaynak:

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

Birden fazla aboneyle http sunucusundan alınan verileri yönetmeye yardımcı olan basit bir Önbellek sınıfı <> oluşturabilirsiniz:

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

}

kullanım

Önbelleklenebilir <> nesnesini bildir (muhtemelen hizmetin bir parçası olarak):

list: Cacheable<string> = new Cacheable<string>();

ve işleyici:

this.list.getHandler = () => {
// get data from server
return this.http.get(url)
.map((r: Response) => r.json() as string[]);
}

Bir bileşenden çağrı:

//gets data from server
List.getData().subscribe(…)

Abone olduğunuz birkaç bileşene sahip olabilirsiniz.

Daha fazla ayrıntı ve kod örneği burada: http://devinstance.net/articles/20171021/rxjs-cacheable


0

Sadece ngx-cacheable kullanabilirsiniz ! Senaryonuza daha iyi uyuyor.

Bunu kullanmanın yararı

  • Dinlenme API'sını yalnızca bir kez çağırır, yanıtı önbelleğe alır ve aşağıdaki istekler için aynısını döndürür.
  • Oluşturma / güncelleme / silme işleminden sonra API'yı gerektiği gibi çağırabilir.

Yani, servis sınıfınız böyle bir şey olurdu -

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

İşte daha fazla referans için bağlantı.


-4

Zaten sahip olduğunuz kodu çalıştırmayı denediniz mi?

Gözlemlenebilir'den kaynaklanan vaatten oluşturduğunuz için getJSON(), ağ isteği herhangi bir abone olmadan önce yapılır. Ve ortaya çıkan söz tüm aboneler tarafından paylaşılıyor.

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
// ...

Açısal 2'yi özel yapmak için soruyu düzenledim
Açısal Üniversite
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.