Каков правильный способ обмена результатами сетевого звонка с угловым Http в RxJs 5?

Используя Http, мы вызываем метод, который выполняет сетевой вызов и возвращает HTTP-наблюдаемый:

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

Если мы возьмем это наблюдаемое и добавим к нему несколько подписчиков:

let network$ = getCustomer();

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

Мы хотим, чтобы это не вызывало множественные сетевые запросы.

Это может показаться необычным сценарием, но на самом деле это довольно часто: например, если вызывающий абонент подписывается на наблюдаемое, чтобы отобразить сообщение об ошибке, и передает его в шаблон с использованием async-канала, у нас уже есть два подписчика.

Каков правильный способ сделать это в RxJs 5?

А именно, это работает нормально:

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

Но разве это идиоматический способ сделать это в RxJs 5, или мы должны сделать что-то другое вместо этого?

Примечание. Согласно HttpClient 5 новому HttpClient, часть .map(res => res.json()) во всех примерах теперь бесполезна, так как результат JSON теперь принимается по умолчанию.

Ответ 1

Загрузите данные и, если они доступны в кеше, верните это в противном случае, сделайте запрос 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;
    }
  }
}

Пример Plunker

Это artile https://blog.thoughtram.io/angular/2018/03/05/advanced-caching-with-rxjs.html - отличное объяснение, как кэшировать с shareReplay.

Ответ 2

Per @Cristian предложение, это один из способов, который хорошо работает для HTTP-наблюдаемых, которые только издаются, а затем завершаются:

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

Ответ 3

ОБНОВЛЕНИЕ: Ben Lesh говорит следующую небольшую версию после 5.2.0, вы сможете просто вызвать shareReplay() для поистине кеширования.

РАНЕЕ.....

Во-первых, не используйте share() или publishReplay (1).refCount(), они одинаковы и проблема с ним заключается в том, что он делится только, если соединения выполняются, когда наблюдаемый активен, если вы подключаетесь после его завершения он снова создает новый наблюдаемый, перевод, а не кеширование.

Birowski дал правильное решение выше, которое должно использовать ReplaySubject. ReplaySubject будет кэшировать значения, которые вы ему даете (bufferSize), в нашем случае 1. Он не будет создавать новые наблюдаемые, такие как share(), когда refCount достигнет нуля, и вы создадите новое соединение, что является правильным поведением для кэширования.

Здесь функция многократного использования

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")

Ответ 4

в соответствии с этим article

Оказывается, мы можем легко добавить кеширование к наблюдаемому, добавив publishReplay (1) и refCount.

поэтому внутри оператора if просто добавить

.publishReplay(1)
.refCount();

to .map(...)

Ответ 5

rxjs 5.4.0 имеет новый метод shareReplay.

Автор явно говорит "идеально подходит для обработки таких вещей, как кеширование результатов AJAX"

rxjs PR # 2443 Подвиг (shareReplay): добавляет shareReplay вариант publishReplay

shareReplay возвращает наблюдаемый, который является источником многоадресной передачи над ReplaySubject. Этот объект повторного воспроизведения перерабатывается по ошибке из источника, но не по завершении источника. Это делает shareReplay идеальным для обработки таких вещей, как кеширование результатов AJAX, так как оно повторяется. Однако повторное поведение отличается от доли в том, что оно не будет повторять наблюдаемый источник, а будет повторять исходные наблюдаемые значения.

Ответ 6

Я снял вопрос, но я постараюсь и пойду на это.

//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 the first subscriber
customer$.subscribe(val => console.log('subscriber 1: ' + val));

//here 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, который является наблюдаемым, который также может подписаться на другое Наблюдаемое и (и это важно), удерживая последнее значение и переиздавая его на любой из это подписчики (ReplaySubject).

Ответ 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();
        });
    }
  }
}

FYI, ограничение 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();
        });
    }
  }
}

Ответ 8

Реализация, которую вы выберете, будет зависеть от того, хотите ли вы отказаться от подписки(), чтобы отменить свой 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;
}

Ответ 9

Данные кэшируемого HTTP-ответа с использованием Rxjs Observer/Observable + Caching + Subscription

Смотрите код ниже

* отказ: я новичок в rxjs, поэтому имейте в виду, что я могу злоупотреблять наблюдаемым/наблюдательным подходом. Мое решение - это просто конгломерат других решений, которые я нашел, и является следствием того, что не удалось найти простое хорошо документированное решение. Таким образом, я предоставляю свое полное решение для кода (как мне бы хотелось найти) в надежде, что оно поможет другим.

* Обратите внимание: этот подход основан на GoogleFirebaseObservables. К сожалению, мне не хватает надлежащего опыта/времени, чтобы воспроизвести то, что они сделали под капотом. Но ниже приведен упрощенный способ предоставления асинхронного доступа к некоторым данным, имеющим кэш-память.

Ситуация. Компоненту "список товаров" задается отображение списка продуктов. Сайт представляет собой одностраничное веб-приложение с некоторыми кнопками меню, которые будут "фильтровать" продукты, отображаемые на странице.

Решение: компонент "подписывается" на метод обслуживания. Метод службы возвращает массив объектов продукта, к которым компонент обращается через обратный вызов подписки. Метод службы завершает свою деятельность во вновь созданном Observer и возвращает наблюдателя. Внутри этого наблюдателя он ищет кэшированные данные и передает их обратно подписчику (компоненту) и возвращается. В противном случае он выдает HTTP-запрос для извлечения данных, подписывается на ответ, где вы можете обрабатывать эти данные (например, сопоставлять данные с вашей собственной моделью), а затем передавать данные обратно подписчику.

Код

продукт-list.component.ts

import { Component, OnInit, Input } from '@angular/core';
import { ProductService } from '../../../services/product.service';
import { Product, ProductResponse } from '../../../models/Product';

@Component({
  selector: 'app-product-list',
  templateUrl: './product-list.component.html',
  styleUrls: ['./product-list.component.scss']
})
export class ProductListComponent implements OnInit {
  products: Product[];

  constructor(
    private productService: ProductService
  ) { }

  ngOnInit() {
    console.log('product-list init...');
    this.productService.getProducts().subscribe(products => {
      console.log('product-list received updated products');
      this.products = products;
    });
  }
}

product.service.ts

import { Injectable } from '@angular/core';
import { Http, Headers } from '@angular/http';
import { Observable, Observer } from 'rxjs';
import 'rxjs/add/operator/map';
import { Product, ProductResponse } from '../models/Product';

@Injectable()
export class ProductService {
  products: Product[];

  constructor(
    private http:Http
  ) {
    console.log('product service init.  calling http to get products...');

  }

  getProducts():Observable<Product[]>{
    //wrap getProducts around an Observable to make it async.
    let productsObservable$ = Observable.create((observer: Observer<Product[]>) => {
      //return products if it was previously fetched
      if(this.products){
        console.log('## returning existing products');
        observer.next(this.products);
        return observer.complete();

      }
      //Fetch products from REST API
      console.log('** products do not yet exist; fetching from rest api...');
      let headers = new Headers();
      this.http.get('http://localhost:3000/products/',  {headers: headers})
      .map(res => res.json()).subscribe((response:ProductResponse) => {
        console.log('productResponse: ', response);
        let productlist = Product.fromJsonList(response.products); //convert service observable to product[]
        this.products = productlist;
        observer.next(productlist);
      });
    }); 
    return productsObservable$;
  }
}

product.ts(модель)

export interface ProductResponse {
  success: boolean;
  msg: string;
  products: Product[];
}

export class Product {
  product_id: number;
  sku: string;
  product_title: string;
  ..etc...

  constructor(product_id: number,
    sku: string,
    product_title: string,
    ...etc...
  ){
    //typescript will not autoassign the formal parameters to related properties for exported classes.
    this.product_id = product_id;
    this.sku = sku;
    this.product_title = product_title;
    ...etc...
  }



  //Class method to convert products within http response to pure array of Product objects.
  //Caller: product.service:getProducts()
  static fromJsonList(products:any): Product[] {
    let mappedArray = products.map(Product.fromJson);
    return mappedArray;
  }

  //add more parameters depending on your database entries and constructor
  static fromJson({ 
      product_id,
      sku,
      product_title,
      ...etc...
  }): Product {
    return new Product(
      product_id,
      sku,
      product_title,
      ...etc...
    );
  }
}

Вот пример вывода, который я вижу при загрузке страницы в Chrome. Обратите внимание, что при начальной загрузке продукты извлекаются из http (вызов в мой сервис node rest, который выполняется локально на порту 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. В моем приложении angular каждый раз, когда я перехожу к другому виду продуктов, компонент списка продуктов перезагружается. ProductService, похоже, является общим экземпляром, поэтому локальный кеш "products: Product []" в ProductService сохраняется во время навигации, а последующие вызовы "GetProducts()" возвращают кешированное значение. Наконец, я прочитал комментарии о том, как наблюдаемые/подписки нужно закрыть, когда вы закончите, чтобы предотвратить утечку памяти. Я не включил это здесь, но это что-то иметь в виду.

Ответ 10

Я предполагаю, что @ngx-cache/core может быть полезен для поддержки функций кеширования для HTTP-вызовов, особенно если HTTP-вызов сделанных на платформах браузера и сервера.

Скажем, мы имеем следующий метод:

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

Вы можете использовать декоратор Cached @ngx-cache/core, чтобы сохранить возвращаемое значение из метода, вызывающего HTTP-вызов в cache storage (storageможно настроить, проверьте реализацию на ng-seed/universal) - прямо на первом выполнении. При следующем вызове метода (независимо от платформы браузер или сервер) значение извлекается из 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());
}

Также существует возможность использовать методы кэширования (has, get, set) с помощью 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';
  }
}

Вот список пакетов, как для клиентского, так и для серверного кеширования:

Ответ 11

rxjs 5.3.0

Я не был доволен .map(myFunction).publishReplay(1).refCount()

С несколькими подписчиками .map() в некоторых случаях выполняет myFunction дважды (я ожидаю, что он будет выполняться только один раз). Кажется, что одно исправление publishReplay(1).refCount().take(1)

Еще одна вещь, которую вы можете сделать, просто не использовать refCount() и немедленно сделать Observable hot:

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

Это запустит HTTP-запрос независимо от подписчиков. Я не уверен, что отмена подписки перед завершением HTTP GET отменяет ее или нет.

Ответ 12

Мы хотим, чтобы это не вызывало множественные сетевые запросы.

Мой личный фаворит - использовать async методы для вызовов, которые делают сетевые запросы. Сами методы не возвращают значение, вместо этого они обновляют BehaviorSubject в той же службе, на которую будут подписаны компоненты.

Теперь зачем использовать BehaviorSubject вместо Observable? Потому как,

  • После подписки BehaviorSubject возвращает последнее значение, тогда как регулярное наблюдение запускается только при получении onnext.
  • Если вы хотите получить последнее значение BehaviorSubject в ненаблюдаемом коде (без подписки), вы можете использовать метод getValue().

Пример:

customer.service.ts

public customers$: BehaviorSubject<Customer[]> = new BehaviorSubject([]);

public async getCustomers(): Promise<void> {
    let customers = await this.httpClient.post<LogEntry[]>(this.endPoint, criteria).toPromise();
    if (customers) 
        this.customers$.next(customers);
}

Затем, где требуется, мы можем просто подписаться на customers$.

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

Или, возможно, вы хотите использовать его непосредственно в шаблоне

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

Итак, теперь, пока вы не сделаете еще один звонок getCustomers, данные сохраняются в customers$ BehaviorSubject.

Итак, что, если вы хотите обновить эти данные? просто позвоните getCustomers()

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

Используя этот метод, нам не нужно явно сохранять данные между последующими сетевыми вызовами, как это описано в BehaviorSubject.

PS: Обычно, когда компонент уничтожается, это хорошая практика, чтобы избавиться от подписки, для чего вы можете использовать метод, предложенный в этом ответе.

Ответ 13

Просто позвоните share() после map и перед любой подпиской.

В моем случае у меня есть общий сервис (RestClientService.ts), который выполняет оставшийся вызов, извлекает данные, проверяет наличие ошибок и возвращает их к конкретной службе внедрения (f.ex.: ContractClientService.ts), наконец эта конкретная реализация возвращается к 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;
   });
  }

}

Ответ 14

Я написал класс кэша,

/**
 * 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, но это было прекрасно для моих целей.

Ответ 15

Просто используйте этот уровень кэша, он делает все, что вам нужно, и даже управляйте кешем для запросов 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"}]};
}

Слой (как услуга angular для инъекций)

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

Ответ 16

It .publishReplay(1).refCount(); или .publishLast().refCount();, так как Angular 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);
  }

}

Ответ 17

Вы можете создать простой класс 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

Ответ 18

Вы также можете использовать официальный механизм кэширования.

Angular Кэш

Ответ 19

Отличные ответы.

Или вы можете сделать это:

Это из последней версии rxjs. Я использую 5.5.7 версию RxJS

import {share} from "rxjs/operators";

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

Ответ 20

Пробовали ли вы использовать код, который у вас уже есть?

Поскольку вы создаете 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
// ...