在RxJs 5中分享Angular Http网络调用结果的正确方法是什么?

What is the correct way to share the result of an Angular Http network call in RxJs 5?

通过使用HTTP,我们调用一个进行网络调用并返回HTTP Observable的方法:

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

如果我们将此可观测数据加上多个订户:

1
2
3
4
let network$ = getCustomer();

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

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

这似乎是一个不寻常的场景,但实际上非常常见:例如,如果调用方订阅了Observable以显示错误消息,并使用异步管道将其传递给模板,那么我们已经有两个订阅方了。

在RXJS5中,正确的方法是什么?

也就是说,这似乎很管用:

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

但这是在RXJS5中实现这一点的惯用方法吗,还是我们应该改为做其他事情呢?

注:根据角5新的HttpClient,所有示例中的.map(res => res.json())部分现在都是无用的,因为json结果现在默认为。


缓存数据,如果可用,则返回此值,否则发出HTTP请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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;

  constructor(private http:Http) {}

  getData() {
    if(this.data) {
      // if `data` is available just return it as `Observable`
      return Observable.of(this.data);
    } else if(this.observable) {
      // if `this.observable` is set then the request is in progress
      // return the `Observable` for the ongoing request
      return this.observable;
    } else {
      // example header (not necessary)
      let headers = new Headers();
      headers.append('Content-Type', 'application/json');
      // create the request, store the `Observable` for subsequent subscribers
      this.observable = this.http.get(this.url, {
        headers: headers
      })
      .map(response =>  {
        // when the cached data is available we don't need the `Observable` reference anymore
        this.observable = null;

        if(response.status == 400) {
          return"FAILURE";
        } else if(response.status == 200) {
          this.data = new Data(response.json());
          return this.data;
        }
        // make it shared so more than one subscriber can get the result
      })
      .share();
      return this.observable;
    }
  }
}

抢劫犯的例子

这篇文章https://blog.thoughtram.io/angular/2018/03/05/advanced-caching-with-rxjs.html很好地解释了如何使用shareReplay进行缓存。


根据@cristian的建议,这是一种适用于HTTP观测的方法,只发射一次,然后完成:

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


更新:BenLesh说,5.2.0之后的下一个次要版本,您可以调用shareReplay()来真正缓存。

以前…..

首先,不要使用share()或publishreplay(1).refcount(),它们是相同的,问题在于,只有当observable处于活动状态时进行连接,它才会共享;如果在observable完成后进行连接,它会再次创建一个新的observable,即转换,而不是真正的缓存。

Birowski给出了正确的解决方案,即使用replaysubject。replaySubject将缓存您在本例1中给出的值(bufferSize)。一旦refcount达到零并且您建立了一个新的连接,它将不会创建一个新的observable like share(),这是缓存的正确行为。

这里有一个可重用的函数

1
2
3
4
5
6
7
8
9
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();
}

以下是如何使用它

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
  constructor(private _http: Http, ) { }

  refresh = () => {
    if (this._cache) {
      return this._cache;
    }
    return this._cache = cacheable(this._http.get('YOUR URL'));
  }
}

下面是一个更高级的可缓存函数版本,这个函数允许有自己的查阅表格+提供自定义查阅表格的能力。这样,您就不必检查这个了。_高速缓存,就像上面的例子一样。另外请注意,不是将Observable作为第一个参数传递,而是传递一个返回Observable的函数,这是因为Angular的HTTP立即执行,所以通过返回一个延迟执行的函数,我们可以决定如果它已经在缓存中,就不调用它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let cacheableCache: { [key: string]: Observable } = {};
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;
}

用途:

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


RXJS 5.4.0有一个新的shareReplay方法。

  • Rx图书共享重播()
  • reactivex.io/rxjs上没有文档

作者明确表示"非常适合处理缓存Ajax结果之类的事情"

rxjs pr_2443 feat(sharereplay):添加shareReplay变体publishReplay

shareReplay returns an observable that is the source multicasted over
a ReplaySubject. That replay subject is recycled on error from the
source, but not on completion of the source. This makes shareReplay
ideal for handling things like caching AJAX results, as it's
retryable. It's repeat behavior, however, differs from share in that
it will not repeat the source observable, rather it will repeat the
source observable's values.


根据这篇文章

It turns out we can easily add caching to the observable by adding publishReplay(1) and refCount.

所以在if语句中只附加

1
2
.publishReplay(1)
.refCount();

.map(...)


我提出了这个问题,但我会试着试试看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//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$)

我们没有订阅getCustomer()的API响应,我们订阅了一个可观察的可替换主题,它也可以订阅一个不同的可观察对象,并且(这一点很重要)保留它的最后一个发射值,并将其重新发布给它的任何(replaysubject)订户。


我找到了一种将HTTP GET结果存储到session store中并用于会话的方法,这样它就不会再调用服务器了。

我用它来调用GitHub API以避免使用限制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Injectable()
export class HttpCache {
  constructor(private http: Http) {}

  get(url: string): Observable {
    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();
        });
    }
  }
}

仅供参考,会话存储限制为5米(或4.75米)。所以,它不应该像这样用于大型数据集。

------编辑---------如果您想用f5刷新数据,它使用内存数据而不是sessionstorage;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Injectable()
export class HttpCache {
  cached: any = {};  // this will store data
  constructor(private http: Http) {}

  get(url: string): Observable {
    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();
        });
    }
  }
}


您选择的实现将取决于您是否希望unsubscribe()取消HTTP请求。

在任何情况下,typescript修饰符都是标准化行为的好方法。这是我写的:

1
2
3
4
  @CacheObservableArgsKey
  getMyThing(id: string): Observable {
    return this.http.get('things/'+id);
  }

装饰器定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
 * 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;
}


使用rxjs observer/observable+caching+subscription可缓存HTTP响应数据

见下面的代码

*免责声明:我是RXJS的新手,因此请记住,我可能误用了Observable/Observer方法。我的解决方案纯粹是我发现的其他解决方案的集合体,并且是由于没有找到一个简单的、有据可查的解决方案而导致的。因此,我提供了完整的代码解决方案(正如我希望找到的那样),希望它能帮助其他人。

*注意,这种方法松散地基于GoogleFireBaseObservables。不幸的是,我缺乏适当的经验/时间来复制他们在幕后的所作所为。但下面是提供对某些可缓存数据的异步访问的简单方法。

情景:"产品列表"组件的任务是显示产品列表。该网站是一个单页Web应用程序,带有一些菜单按钮,可以"过滤"页面上显示的产品。

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

代码

产品列表.组件.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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;
    });
  }
}

产品.服务.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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(型号)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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上运行的节点REST服务)。然后,当我单击导航到产品的"筛选"视图时,这些产品将在缓存中找到。

我的Chrome日志(控制台):

1
2
3
4
5
6
7
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

…[单击菜单按钮以筛选产品]…

1
2
3
4
app.component.ts:19 app.component url: /products/chocolatechip
product-list.component.ts:18 product-list init...
product.service.ts:24 ## returning existing products
product-list.component.ts:20 product-list received updated products

结论:这是迄今为止我发现的实现可缓存HTTP响应数据的最简单方法。在Angular应用程序中,每次我导航到不同的产品视图时,都会重新加载产品列表组件。productService似乎是共享实例,因此在导航期间保留productService中"products:product[]"的本地缓存,随后调用"getProducts()"将返回缓存的值。最后一点要注意的是,我已经阅读了一些评论,这些评论是关于当您完成工作以防止"内存泄漏"时,如何关闭可观察项/订阅的。我这里没有包括这个,但要记住这一点。


我假设@ngx cache/core对于维护HTTP调用的缓存功能很有用,特别是在浏览器和服务器平台上都进行了HTTP调用时。

假设我们有以下方法:

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

您可以使用@ngx cache/core的Cached修饰器来存储在cache storage上进行HTTP调用的方法返回的值(storage可以配置,请在第一次执行时检查在ng seed/universal)上的实现)。下一次调用该方法时(无论是在浏览器或服务器平台上),都会从cache storage中检索该值。

1
2
3
4
5
6
7
8
import { Cached } from '@ngx-cache/core';

...

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

还可以使用缓存API使用缓存方法(hasgetset

任何类别的TS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
...
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';
  }
}

以下是客户端和服务器端缓存的包列表:

  • @NGX缓存/核心:缓存实用程序
  • @NGX缓存/平台浏览器:SPA/浏览器平台实现
  • @NGX缓存/平台服务器:服务器平台实现
  • @NGX缓存/FS存储:存储实用程序(服务器平台需要)

RXJS版本5.4.0(2017-05-09)增加了对共享重播的支持。

Why use shareReplay?

You generally want to use shareReplay when you have side-effects or taxing computations that you do not wish to be executed amongst multiple subscribers. It may also be valuable in situations where you know you will have late subscribers to a stream that need access to previously emitted values. This ability to replay values on subscription is what differentiates share and shareReplay.

您可以很容易地修改Angular服务来使用它,并返回一个带有缓存结果的Observable,该结果只会使HTTP调用一次(假设第一次调用成功)。

角度服务示例

这里有一个非常简单的客户服务,使用shareReplay

客户服务.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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时进行。

另外,这里的假设是,初始返回的数据在应用程序实例的生命周期中不会过时。


What we want to do, is ensure that this does not cause multiple network requests.

我个人最喜欢使用async方法进行发出网络请求的呼叫。方法本身不返回值,而是更新同一服务中的BehaviorSubject,组件将订阅该服务。

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

  • 订阅后,BehaviorSubject返回最后一个值,而常规的Observable仅在接收到onnext时触发。
  • 如果要在不可观察的代码(不带订阅)中检索行为主题的最后一个值,可以使用getValue()方法。

例子:

客户服务.ts

1
2
3
4
5
6
7
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$

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

或者您可能想直接在模板中使用它

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

所以现在,在您再次调用getCustomers之前,数据保留在customers$行为主题中。

那么,如果您想刷新这些数据呢?打个电话给getCustomers()

1
2
3
4
5
6
7
8
9
public async refresh(): Promise<void> {
    try {
      await this.customerService.getCustomers();
    }
    catch (e) {
      // request failed, handle exception
      console.error(e);
    }
}

使用这个方法,我们不必在随后的网络调用之间显式地保留数据,因为它是由BehaviorSubject处理的。

PS:通常,当一个组件被销毁时,最好去掉订阅,因为您可以使用这个答案中建议的方法。


答案很好。

或者你可以这样做:

这是RXJS的最新版本。我使用的是RXJS的5.5.7版本

1
2
3
import {share} from"rxjs/operators";

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

RXJS 5.3.0版

我对.map(myFunction).publishReplay(1).refCount()不满意

对于多个订户,.map()在某些情况下执行myFunction两次(我希望它只执行一次)。一个解决办法似乎是publishReplay(1).refCount().take(1)

你能做的另一件事,就是不要使用refCount(),马上把可观察到的东西变热:

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

这将启动HTTP请求,而不考虑订阅服务器。我不确定在HTTP GET完成之前取消订阅是否会取消它。


它是.publishReplay(1).refCount();.publishLast().refCount();,因为角HTTP观测在请求后完成。

这个简单的类缓存结果,这样您可以多次订阅.value并只发出1个请求。还可以使用.reload()发出新请求并发布数据。

你可以像这样使用它:

1
2
3
4
5
6
7
8
9
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);
});

来源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
export class RestResource {

  static readonly LOADING: string = 'RestResource_Loading';
  static readonly ERROR: string = 'RestResource_Error';
  static readonly IDLE: string = 'RestResource_Idle';

  public value: Observable;
  public status: Observable<string>;
  private loadStatus: Observer;

  private reloader: Observable;
  private reloadTrigger: Observer;

  constructor(requestObservableFn: () => Observable) {
    this.status = Observable.create((o) => {
      this.loadStatus = o;
    });

    this.reloader = Observable.create((o: Observer) => {
      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);
  }

}


只需使用这个缓存层,它就可以完成您所需要的一切,甚至可以为Ajax请求管理缓存。

http://www.ravinderpayal.com/blogs/12jan2017-ajax-cache-management-angular2-service.html

很容易使用

组件({selector:'主页',模板URL:'./html/home.component.html',样式URL:'.'/css/home.component.css'],})导出类homecomponent{构造函数(AjaxService:AjaxService){ajaxService.postcache("/api/home/articles").subscribe(values=>console.log(values);this.articles=values;);}文章=1:数据:标题:"第一",排序"文本:"描述


我写了一个缓存类,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
 * 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>();
    }

}

由于我们如何使用它,它都是静态的,但是可以随意地将它设置为一个普通的类和服务。但我不确定Angular2是否始终保持一个实例。

我就是这样使用它的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
            let httpService: Http = this.http;
            function fetcher(url: string): Observable {
                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 = StaticCache.getOrFetch(url, fetcher);
                if (!cachedObservable)
                    throw new Error("Failed loading link:" + url);
                return cachedObservable;
            }

我想可能会有一种更聪明的方法,它会使用一些江户的(5)把戏,但就我的目的而言,这是很好的。


您可以构建简单的类可缓存<>来帮助管理从具有多个订阅服务器的HTTP服务器检索到的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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();
    }

}

用法

声明可缓存<>对象(可能是服务的一部分):

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

和处理程序:

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

从组件调用:

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

您可以订阅多个组件。

更多详细信息和代码示例如下:http://devinstance.net/articles/20171021/rxjs-cacheable


只需在映射之后和任何订阅之前调用share()。

在我的例子中,我有一个通用服务(restclientservice.ts),它正在进行REST调用、提取数据、检查错误并将observable返回到具体的实现服务(例如contractclientservice.ts),最后这个具体的实现将observable返回到de contractcomponent.ts,而这个服务订阅更新视图。

restclientservice.ts:恢复客户服务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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) {
    ...
  }

}

合同服务.ts:

1
2
3
4
5
6
7
8
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);
  }

}

合同组成部分:

1
2
3
4
5
6
7
8
9
export class ContractComponent implements OnInit {

  getAllItems() {
    this.rcService.getAllItems().subscribe((data) => {
      this.items = data;
   });
  }

}


您可以简单地使用NGX可缓存!它更适合你的场景。

The benefit of using this

  • It calls rest API only once, caches the response & returns the same for following requests.
  • Can call API as required after create/ update/ delete operation.

所以,你的服务班应该是这样的-

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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
    }
}

这是更多参考的链接。


这正是我为之创建的库NGX RXcache。

在https://github.com/adriandavidbrand/ngx-rxcache上查看,并在https://stackblitz.com/edit/angular-jxqaiv上查看一个工作示例。


你试过运行你已有的代码吗?

因为您是根据getJSON()产生的承诺构造可观察的,所以网络请求在任何人订阅之前发出。由此产生的承诺由所有订户共享。

1
2
3
4
5
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
// ...