在RxJs 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(...);

我们要做的是确保这不会引起多个网络请求。

这似乎是一种不寻常的情况,但实际上非常普遍:例如,如果调用者订阅了可观察对象以显示错误消息,并使用异步管道将其传递给模板,则我们已经有两个订阅者。

在RxJs 5中这样做的正确方法是什么?

即,这似乎工作正常:

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

但这是RxJs 5中惯用的方式吗?还是我们应该做其他事情呢?

注意:根据Angular 5 new HttpClient.map(res => res.json())所有示例中的部分现在都已无用,因为默认情况下现在假定JSON结果。


1
>共享与publish()。refCount()相同。其实不是。参见以下讨论:github.com/ReactiveX/rxjs/issues/1363
Christian

1
编辑的问题,根据该问题,似乎代码上的文档需要更新-> github.com/ReactiveX/rxjs/blob/master/src/operator/share.ts
Angular University

我认为“取决于”。但是对于不能在本地b / c缓存数据的调用,由于参数更改/组合,这可能没有意义。.share()似乎绝对是对的。但是,如果您可以在本地缓存内容,则有关ReplaySubject / BehaviorSubject的其他一些答案也是不错的解决方案。
JimB

我认为我们不仅需要缓存数据,还需要更新/修改缓存的数据。这是很常见的情况。例如,如果我想向缓存的模型添加新字段或更新字段的值。也许 用CRUD方法创建单例DataCacheService是更好的方法吗?像商店终极版。你怎么看?
slideshowp2

您可以简单地使用ngx-cacheable!它更适合您的情况。请在下面
提及

Answers:


231

缓存数据,如果缓存了可用数据,则返回该数据,否则发出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返回的内容),但是恕我直言,这并不能很好地表达代码的意图。
君特Zöchbauer

2
this.extraData结束时,extraData() { if(foo) { doSomething();}}否则返回最后一个表达式的结果,这可能不是您想要的。
君特Zöchbauer

9
@Günter,谢谢您的代码,它可以正常工作。但是,我试图理解为什么您分别跟踪数据和可观察对象。通过这样仅缓存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可观察对象的方法,该方法只发出一次,然后完成:

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

使用此方法存在两个问题-返回的observable无法取消或重试。对于您来说,这可能不是问题,但也许再次如此。如果这是一个问题,那么share操作员可能是一个合理的选择(尽管有一些讨厌的情况)。有关选项深潜讨论请参见本博客文章评论部分:blog.jhades.org/...
基督教

1
少量澄清...尽管严格地不能共享共享的源可观察对象publishLast().refCount(),但是一旦取消了对所返回的可观察对象的所有订阅refCount,则净结果是将取消订阅源可观察对象,如果它在“飞行中”,则将其取消
Christian

@Christian Hey,您能说“无法取消或重试”来解释您的意思吗?谢谢。
未定义的

37

更新:Ben Lesh说,在5.2.0之后的下一个次要发行版中,您只需调用shareReplay()即可真正进行缓存。

先前.....

首先,不要使用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操作:const data$ = this._http.get('url').pipe(cacheable()); /*1st subscribe*/ data$.subscribe(); /*2nd subscribe*/ data$.subscribe();?因此,它的行为更像其他任何运算符
Felix

31

rxjs 5.4.0具有新的shareReplay方法。

作者明确表示“理想的处理诸如缓存AJAX结果之类的东西”

rxjs PR#2443 feat(shareReplay):添加shareReplaypublishReplay

shareReplay返回一个可观察对象,它是通过ReplaySubject多播的源。该重放主题会在源错误时回收,但在源完成时不会回收。由于它可以重试,因此shareReplay非常适合处理诸如缓存AJAX结果之类的事情。但是,它的重复行为与share不同,因为它不会重复源可观察的值,而是会重复源可观察的值。



4
我尝试将.shareReplay(1,10000)添加到一个可观察对象中,但是我没有注意到任何缓存或行为更改。有可用的工作示例吗?
Aydus-Matthew

查看更改日志github.com/ReactiveX/rxjs/blob/…早些时候出现,在v5中被删除,在5.4中重新添加-rx -book链接确实引用了v4,但是它存在于当前的LTS v5.5.6和它在v6中。我想象那里的rx-book链接已经过期。
Jason Awbrey

25

根据这篇文章

事实证明,我们可以通过添加publishReplay(1)和refCount轻松地将缓存添加到可观察对象。

所以 在if语句中只是追加

.publishReplay(1)
.refCount();

.map(...)


11

rxjs版本5.4.0(2017-05-09)添加了对shareReplay的支持。

为什么要使用shareReplay?

如果您不想在多个订户中执行副作用或繁重的计算,则通常需要使用shareReplay。在您知道您的流的后期订阅者需要访问以前发出的值的情况下,这也可能很有价值。重播订阅值的这种能力是share和shareReplay的与众不同之处。

您可以轻松地修改角度服务以使用此服务,并返回带有可缓存结果的observable,该结果将仅使http调用一次(假设第一次调用成功)。

Angular服务示例

这是使用的非常简单的客户服务shareReplay

客户服务

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是“冷”的,因此在构造函数中执行此操作是可以接受的,因为http调用只会在第一次调用时进行subscribe

此处还假定初始返回的数据在应用程序实例的生存期内不会过时。


我非常喜欢这种模式,并希望在我在多个应用程序中使用的api服务共享库中实现该模式。一个示例是UserService,在应用程序的生命周期中,除几个地方外,其他地方都不需要使缓存无效,但是对于那些情况,我将如何使它无效而又不会导致先前的订阅变得孤立?
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(),我们订阅的是可观察的ReplaySubject,它也可以订阅另一个Observable,并且(这很重要)保持它的最后一个发出值并将其重新发布到其中的任何一个(ReplaySubject的)的订阅者。


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,我只会将其用于预期在整个会话中保持一致的数据。
马丁·施耐德

@ MA-Maddin如我所说:“我用它来避免使用限制”。如果要使用F5刷新数据,则需要使用内存而不是sessionStorage。答案已经用这种方法进行了编辑。
allenhwkim

是的,这可能是一个用例。因为每个人都在谈论Cache,OP getCustomer在他的示例中提到了,所以我才被触发。;)所以只是想警告一些可能看不到风险的人:)
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 +缓存+订阅的可缓存HTTP响应数据

请参阅下面的代码

*免责声明:我是rxjs的新手,所以请记住,我可能会滥用可观察/观察者方法。我的解决方案纯粹是我找到的其他解决方案的综合,并且是未能找到简单的有据可查的解决方案的结果。因此,我提供了完整的代码解决方案(正如我希望发现的那样),希望对其他人有所帮助。

*请注意,这种方法大致基于GoogleFirebaseObservables。不幸的是,我缺乏适当的经验/时间来复制他们在幕后所做的事情。但是以下是提供对某些可缓存数据的异步访问的简单方法。

情况:“产品列表”组件的任务是显示产品列表。该站点是一个单页Web应用程序,带有一些菜单按钮,这些按钮将“过滤”页面上显示的产品。

解决方案:组件“订阅”服务方法。service方法返回一个产品对象数组,组件可通过订阅回调访问该对象。service方法将其活动包装在新创建的Observer中,并返回观察者。在此观察器内部,它搜索高速缓存的数据,并将其传递回订阅者(组件)并返回。否则,它将发出http调用以检索数据,订阅响应,在此您可以处理该数据(例如,将数据映射到您自己的模型),然后将数据传递回订阅者。

编码

产品列表.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;
    });
  }
}

产品服务

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(..)调用)。现在,在您的组件中,正常注入服务。您可以使用productService.product $ .value获得最新分配的product $值。如果希望在product $收到新值时执行操作(即在第3部分中调用product $ .next(...)函数),请订阅product $。
ObjectiveTC

1
例如,在products.component.ts中... this.productService.product $ .takeUntil(this.ngUnsubscribe).subscribe((products)=> {this.category); 让filteredProducts = this.productService.getProductsByCategory(this.category); this.products =过滤后的产品;});
ObjectiveTC

1
有关取消订阅可观察对象的重要说明:“。takeUntil(this.ngUnsubscribe)”。看到这个堆栈溢出问题/答案,这似乎显示了“事实上的”推荐的方式,从事件退订:stackoverflow.com/questions/38008334/...
ObjectiveTC

1
如果可观察对象仅意图接收一次数据,则可以选择.first()或.take(1)。所有其他“可观对象”的“无限流”应在“ ngOnDestroy()”中取消订阅,否则,您可能会得到重复的“可观对象”回调。 stackoverflow.com/questions/28007777/…–
ObjectiveTC

3

我认为@ ngx-cache / core对于维护http调用的缓存功能很有用,尤其是在浏览器服务器平台上都进行HTTP调用的情况下。

假设我们有以下方法:

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

您可以使用Cached的装饰@ NGX缓存/内核返回的值作出的HTTP调用的方法存储cache storagestorage可配置的,请在执行NG-种子/通用) -第一执行权。下次调用该方法时(无论在浏览器还是服务器平台上),都将从中检索值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());
}

另外还有可能使用高速缓存方法(hasgetset使用)的缓存API

任何类

...
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()执行myFunction两次(我希望它仅执行一次)。一种解决方法似乎是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在同一服务中更新组件将订阅的服务。

现在,为什么要使用a BehaviorSubject而不是an Observable?因为,

  • 订阅后,BehaviorSubject返回最后一个值,而常规的observable仅在收到时触发onnext
  • 如果要以不可观察的代码(没有订阅)检索BehaviorSubject的最后一个值,则可以使用该getValue()方法。

例:

客户服务

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

只需在map之后和任何subscription之前调用share()即可

就我而言,我有一个通用服务(RestClientService.ts),该服务正在进行剩余调用,提取数据,检查错误并将可观察到的返回到具体的实现服务(f.ex .: ContractClientService.ts),最后是这个具体的实现将observable返回给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是否在整个时间内都保持单个实例(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

您可以构建简单的类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


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

您是否尝试过运行已有的代码?

因为您是根据产生的承诺来构造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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.