Який правильний спосіб поділити результат виклику мережі Angular Http у RxJs 5?


303

Використовуючи 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, чи ми повинні робити щось інше?

Примітка: Відповідно до Angular 5 new HttpClient, .map(res => res.json())частина у всіх прикладах тепер марна, оскільки результат JSON тепер вважається за замовчуванням.


1
> частка ідентична публікації (). refCount (). Насправді це не так. Дивіться таку дискусію: github.com/ReactiveX/rxjs/isissue/1363
Крістіан

1
відредаговане питання, відповідно до випуску виглядає так, що документи щодо коду потрібно оновити -> github.com/ReactiveX/rxjs/blob/master/src/operator/share.ts
Angular University

Я думаю, що "це залежить". Але для дзвінків, де ви не можете кешувати дані локально b / c, це може не мати сенсу через зміни параметрів / комбінацій .share () здається абсолютно правильним. Але якщо ви можете кешувати речі локально, деякі інші відповіді щодо ReplaySubject / BehaviorSubject - це також хороші рішення.
JimB

Я думаю, що нам потрібні не лише кешування даних, а й оновлення / зміна кешованих даних. Це звичайний випадок. Наприклад, якщо я хочу додати нове поле до кешованої моделі або оновити значення поля. Може створити одноелементна DataCacheService з CRUD метод є кращим способом? Як магазин в Redux . Як ти гадаєш?
слайд-

Ви можете просто використовувати ngx-кешування ! Це краще відповідає вашому сценарію. Надішліть мою відповідь нижче
Тушар Вальзаде

Відповіді:


230

Кешуйте дані, і якщо вони є кешованими, поверніть це, інакше зробіть запит HTTP.

import {Injectable} from '@angular/core';
import {Http, Headers} from '@angular/http';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/observable/of'; //proper way to import the 'of' operator
import 'rxjs/add/operator/share';
import 'rxjs/add/operator/map';
import {Data} from './data';

@Injectable()
export class DataService {
  private url: string = 'https://cors-test.appspot.com/test';

  private data: Data;
  private observable: Observable<any>;

  constructor(private http: Http) {}

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

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

Приклад Плункер

Ця стаття https://blog.thoughtram.io/angular/2018/03/05/advanced-caching-with-rxjs.html - це чудове пояснення, як кешувати shareReplay.


3
do()навпаки map(), не змінює події. Ви можете також використовувати map(), але тоді вам потрібно було забезпечити повернення правильного значення в кінці зворотного дзвінка.
Гюнтер Зехбауер

3
Якщо для сайту для викликів, якому .subscribe()не потрібне значення, ви можете це зробити, тому що він може отримати просто null(залежно від того, що this.extractDataповертається), але IMHO це не добре виражає наміри коду.
Günter Zöchbauer

2
Коли this.extraDataзакінчується так, як extraData() { if(foo) { doSomething();}}інакше повертається результат останнього виразу, який може бути не тим, що ви хочете.
Гюнтер Зехбауер

9
@ Günter, дякую за код, він працює. Однак я намагаюся зрозуміти, чому ви відстежуєте дані та спостереження окремо. Чи не вдалося б ви досягти такого ж ефекту, кешуючи просто Спостерігається <Data>, як це? if (this.observable) { return this.observable; } else { this.observable = this.http.get(url) .map(res => res.json().data); return this.observable; }
липень.Тек

3
@HarleenKaur Це клас, до якого отримано деріаріалізацію отриманого JSON, щоб отримати чітку перевірку типу та автоматичне завершення. Використовувати його не потрібно, але це звичайно.
Гюнтер Зехбауер

44

За пропозицією @ Крістіана, це один із способів, який добре працює для HTTP-спостережуваних даних, які випромінюються лише один раз, а потім вони завершують:

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

Існує декілька проблем із використанням цього підходу - повернене спостережуване не може бути скасоване чи повторне. Це може не бути проблемою для вас, але знову ж таки, можливо. Якщо це проблема, то shareоператор може бути розумним вибором (хоча і з деякими неприємними крайовими випадками). Для глибокої дискусії щодо варіантів дивіться розділ коментарів у цьому дописі: blog.jhades.org/…
Крістіан

1
Невелике уточнення ... Хоча суворо джерело, на яке publishLast().refCount()можна поділити сповіщення, не може бути скасовано, як тільки всі підписки на спостережуване, повернене, refCountскасовані, чистий ефект, що спостерігається джерело, буде скасовано, скасувавши його, якщо воно там, де "запалити"
Крістіан

@ Християн Ей, чи можете ви пояснити, що ви маєте на увазі, сказавши "не можна скасувати чи повторно повторити"? Дякую.
визначено

37

ОНОВЛЕННЯ: Бен Леш каже, що наступного незначного випуску після 5.2.0 ви зможете просто викликати shareReplay (), щоб справді кешувати.

ПОПЕРЕДНЯ .....

По-перше, не використовуйте share () та не публікуйтеReplay (1) .refCount (), вони однакові, і проблема з ним полягає в тому, що він ділиться лише у випадку, коли з'єднання встановлені, поки спостережувальний активний, якщо ви підключитесь після його завершення. , це знову створює новий спостережуваний, переклад, не дуже кешований.

Вище Біровський дав правильне рішення, яке полягає у використанні 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, як у наведеному вище прикладі. Також зауважте, що замість передачі спостережуваного як першого аргументу ви передаєте функцію, яка повертає спостережувані, це тому, що Http Angular виконується відразу, тому, повертаючи ледачу виконану функцію, ми можемо вирішити не називати її, якщо вона вже в наш кеш.

let cacheableCache: { [key: string]: Observable<any> } = {};
export function cacheable<T>(returnObservable: () => Observable<T>, key?: string, customCache?: { [key: string]: Observable<T> }): Observable<T> {
  if (!!key && (customCache || cacheableCache)[key]) {
    return (customCache || cacheableCache)[key] as Observable<T>;
  }
  let replay = new ReplaySubject<T>(1);
  returnObservable().subscribe(
    x => replay.next(x),
    x => replay.error(x),
    () => replay.complete()
  );
  let observable = replay.asObservable();
  if (!!key) {
    if (!!customCache) {
      customCache[key] = observable;
    } else {
      cacheableCache[key] = observable;
    }
  }
  return observable;
}

Використання:

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

Чи є якісь причини не використовувати це рішення як оператор RxJs const data$ = this._http.get('url').pipe(cacheable()); /*1st subscribe*/ data$.subscribe(); /*2nd subscribe*/ data$.subscribe();:? Тож він поводиться більше, як будь-який інший оператор ..
Фелікс

31

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

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

rxjs PR # 2443 feat (shareReplay): додає shareReplayваріантpublishReplay

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


Це пов’язано з цим? Ці документи з 2014 року. github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/…
Аарон Хоффман

4
Я спробував додати .shareReplay (1, 10000) до спостережуваного, але я не помітив жодного кешування чи зміни поведінки. Чи є робочий приклад?
Айдус-Матвій

Дивлячись на змінну github.com/ReactiveX/rxjs/blob/… Це з'явилося раніше, було видалено в v5, додане ще в 5.4 - те, що посилання rx-книги посилається на v4, але воно існує в поточному LTS v5.5.6 і це в v6. Я думаю, посилання на rx-книгу застаріло.
Jason Awbrey

25

відповідно до цієї статті

Виявляється, ми можемо легко додати кешування до спостережуваного, додавши pubReplay (1) та refCount.

так всередині, якщо заяви просто додаються

.publishReplay(1)
.refCount();

до .map(...)


11

rxjs версія 5.4.0 (2017-05-09) додає підтримку для shareReplay .

Навіщо використовувати shareReplay?

Як правило, ви хочете використовувати shareReplay, коли у вас є побічні ефекти або податкові обчислення, які ви не хочете виконувати серед декількох підписників. Це також може бути цінним у ситуаціях, коли ви знаєте, що у вас будуть пізні підписки на потік, яким потрібен доступ до раніше випромінюваних значень. Ця здатність відтворювати значення підписки - це те, що відрізняє share і shareReplay.

Ви можете легко змінити кутовий сервіс, щоб використовувати це і повернути спостережуваний з кешованим результатом, який тільки коли-небудь зробить http-дзвінок одноразово (якщо припустити, що 1-й дзвінок був успішним).

Приклад кутового сервісу

Ось дуже просте обслуговування клієнтів, яке використовує shareReplay .

customer.service.ts

import { shareReplay } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';

@Injectable()
export class CustomerService {

    private readonly _getCustomers: Observable<ICustomer[]>;

    constructor(private readonly http: HttpClient) {
        this._getCustomers = this.http.get<ICustomer[]>('/api/customers/').pipe(shareReplay());
    }

    getCustomers() : Observable<ICustomer[]> {
        return this._getCustomers;
    }
}

export interface ICustomer {
  /* ICustomer interface fields defined here */
}

Зауважте, що призначення в конструкторі може бути перенесено на метод, getCustomersале оскільки спостережувані дані, повернуті з 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, який можна помітити, який також може підписатись на інше спостережуване, і (і це важливо) утримувати його останнє випромінене значення та повторно публікувати його в будь-якому з них (ReplaySubject's ) абонентів.


1
Мені подобається такий підхід, оскільки він добре використовує rxjs і не потрібно додавати власну логіку, дякую
Thibs

7

Я знайшов спосіб зберігати результат отримання http в sessionStorage і використовувати його для сеансу, щоб він більше ніколи не викликав сервер.

Я використовував його для виклику API github, щоб уникнути обмеження на використання.

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

Якщо ви будете зберігатись у сесійному сховищі, то як ви переконаєтесь, що зберігання сеансу знищено, коли ви вийдете з програми?
Гаги

але це вводить несподівану поведінку для користувача. Коли користувач натискає кнопку F5 або кнопку оновлення браузера, він очікує отримання нових даних від сервера. Але насправді він отримує застарілі дані з LocalStorage. Звіти про помилки, квитки на підтримку тощо, що надходять ... Як видно з назви sessionStorage, я використовував би це лише для даних, які, як очікується, будуть узгоджені протягом усього сеансу.
Мартін Шнайдер

@ MA-Maddin, як я зазначив, "я використовував це, щоб уникнути обмеження використання". Якщо ви хочете, щоб дані були оновлені за допомогою F5, вам потрібно використовувати пам'ять замість sessionStorage. Відповідь було відредаговано таким підходом.
allenhwkim

Так, це може бути корисним випадком. Мене щойно викликало, оскільки всі говорять про кеш, а ОП - getCustomerу своєму прикладі. ;) Тож просто хотілося попередити деякі ppl, які можуть не бачити ризиків :)
Мартін Шнайдер

5

Вибрана вами реалізація залежатиме від того, ви хочете скасувати підписку (), щоб скасувати запит 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

Кешовані дані відповіді HTTP за допомогою Rxjs Observer / Observable + Кешування + Підписка

Дивіться код нижче

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

* Зауважте, цей підхід грунтується на GoogleFirebaseObservables. На жаль, мені не вистачає належного досвіду / часу, щоб повторити те, що вони зробили під кришкою. Але далі - спрощений спосіб надання асинхронного доступу до деяких даних, здатних кешувати.

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

Рішення : компонент "підписується" на метод обслуговування. Метод обслуговування повертає масив продуктових об'єктів, до яких компонент отримує доступ через зворотний виклик підписки. Метод обслуговування завершує свою діяльність у новоствореному Спостережнику та повертає спостерігача. Всередині цього спостерігача він здійснює пошук кешованих даних і передає їх назад абоненту (компоненту) і повертається. В іншому випадку він надсилає http-дзвінок для отримання даних, підписується на відповідь, де ви можете обробити ці дані (наприклад, зіставити дані у вашу власну модель), а потім передати дані назад передплатнику.

Код

product-list.component.ts

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

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

  constructor(
    private productService: ProductService
  ) { }

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

product.service.ts

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

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

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

  }

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

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

product.ts (модель)

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

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

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



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

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

Ось зразок результату, який я бачу, коли завантажую сторінку в Chrome. Зауважте, що при початковому завантаженні продукти отримуються з http (дзвінок до моєї служби відпочинку вузлів, яка працює локально на порту 3000). Коли я натискаю, щоб перейти до "відфільтрованого" перегляду продуктів, продукти знаходяться в кеші.

Мій журнал Chrome (консоль):

core.es5.js:2925 Angular is running in the development mode. Call enableProdMode() to enable the production mode.
app.component.ts:19 app.component url: /products
product.service.ts:15 product service init.  calling http to get products...
product-list.component.ts:18 product-list init...
product.service.ts:29 ** products do not yet exist; fetching from rest api...
product.service.ts:33 productResponse:  {success: true, msg: "Products found", products: Array(23)}
product-list.component.ts:20 product-list received updated products

... [натиснув кнопку меню для фільтрації продуктів] ...

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

Висновок: Це найпростіший спосіб, який я знайшов (поки що) реалізувати кешовані дані відповіді http. У своєму кутовому додатку щоразу, коли переходжу до іншого погляду продуктів, компонент списку продуктів перезавантажується. ProductService, здається, є спільним екземпляром, тому локальний кеш 'products: Product []' у ProductService зберігається під час навігації, а наступні виклики до «GetProducts ()» повертають кешоване значення. Останнє зауваження, я читав коментарі про те, як потрібно закрити спостереження / підписки, коли ви закінчите, щоб запобігти "витоку пам'яті". Я тут не включив це, але це слід пам’ятати.


1
Примітка. З тих пір я знайшов більш потужне рішення за участю RxJS BehaviorSubjects, яке спрощує код і різко скорочує "накладні витрати". У products.service.ts, 1. імпортуйте {BehaviorSubject} з 'rxjs'; 2. змінити 'products: Product []' на 'product $: BehaviorSubject <Product []> = new BehaviorSubject <Product []> ([]);' 3. Тепер ви можете просто зателефонувати на http, не повертаючи нічого. http_getProducts () {this.http.get (...). map (res => res.json ()). підписатися (products => this.product $ .next (products))};
ObjectiveTC

1
Локальна змінна 'продукт $' - це поведінковий предмет, який буде як EMIT, так і зберігати останні продукти (з продукту $ .next (..) в частині 3). Тепер у своїх компонентах введіть послугу як звичайну. Ви отримуєте останнє присвоєне значення продукту $, використовуючи productService.product $ .value. Або підпишіться на продукт $, якщо ви хочете виконати дію, коли продукт $ отримує нове значення (тобто функція product $ .next (...) викликається в частині 3).
ObjectiveTC

1
Наприклад, у products.component.ts ... this.productService.product $ .takeUntil (this.ngUnsubscribe) .subscribe ((products) => {this.category); нехай filteredProducts = this.productService.getProductsByCategory (this.category); this.products = відфільтрованіПродукти; });
ObjectiveTC

1
Важлива примітка про скасування підписки на спостережувані: ".takeUntil (this.ngUnsubscribe)". Дивіться це запитання / відповідь про переповнення стека, який, як видається, показує рекомендований "фактичний" спосіб скасувати підписку на події: stackoverflow.com/questions/38008334/…
ObjectiveTC

1
Альтернативою є .first () або .take (1), якщо спостережувана призначена для отримання даних лише один раз. Усі інші "нескінченні потоки" спостережуваних даних повинні бути скасовані підпискою на "ngOnDestroy ()", а якщо цього не зробити, ви можете отримати подвійні зворотні зворотні виклики. stackoverflow.com/questions/28007777/…
ObjectiveTC

3

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

Скажімо, у нас є такий метод:

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

Ви можете використовувати Cachedдекоратор @ NGX-кеш / ядер для зберігання значення, що повертається з методу , що робить HTTP заходять в cache storage( може бути налаштованим, будь ласка , перевірте реалізацію на нг насіння / універсальні ) - справа на перше виконанні. Наступного разу, коли метод буде викликаний (незалежно від браузера або серверної платформи), значення буде отримано з .storagecache storage

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

...

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

Там також можливість використання методів кешування ( 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';
  }
}

Ось перелік пакунків як для кешування на стороні клієнта, так і для сервера:


1

rxjs 5.3.0

Я не був задоволений .map(myFunction).publishReplay(1).refCount()

З декількома підписниками, у деяких випадках .map()виконується myFunctionдвічі (я очікую, що він виконається лише один раз). Одне виправлення, здається, єpublishReplay(1).refCount().take(1)

Ще одна річ, яку ви можете зробити, - це просто не використовувати refCount()та робити оглядовий гарячий відразу:

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

Це запустить HTTP-запит незалежно від передплатників. Я не впевнений, чи скасує передплату перед завершенням HTTP GET, чи ні.


1

Ми хочемо зробити так, щоб це не спричиняло численних мережевих запитів.

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

Тепер Чому використовувати BehaviorSubjectзамість Observable? Тому що,

  • Після підписки BehaviorSubject повертає останнє значення, тоді як звичайне спостережуване спрацьовує лише тоді, коли воно отримує onnext.
  • Якщо ви хочете отримати останнє значення BehaviorSubject в коді, який не спостерігається (без підписки), ви можете скористатися getValue()методом.

Приклад:

customer.service.ts

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

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

Тоді, де потрібно, ми можемо просто підписатися customers$.

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

Або, можливо, ви хочете використовувати його безпосередньо в шаблоні

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

Отже, поки ви не зробите ще один дзвінок getCustomers, дані зберігаються в customers$BehaviorSubject.

Що робити, якщо ви хочете оновити ці дані? просто зателефонуйтеgetCustomers()

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

Використовуючи цей метод, нам не потрібно явно зберігати дані між наступними мережевими дзвінками, оскільки ним обробляється BehaviorSubject.

PS: Зазвичай, коли компонент знищується, це хороша практика позбутися від підписок, для цього ви можете використовувати метод, запропонований у цій відповіді.


1

Чудові відповіді.

Або ви могли це зробити:

Це з останньої версії rxjs. Я використовую 5.5.7 версію RxJS

import {share} from "rxjs/operators";

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

0

Просто зателефонуйте долі () після карти та перед будь-якою підпискою .

У моєму випадку я маю загальну службу (RestClientService.ts), яка здійснює дзвінок на відпочинок, витягує дані, перевіряє помилки та повертає видимість до конкретної служби впровадження (f.ex .: ContractClientService.ts), нарешті це конкретна реалізація повертає спостережуваний до de ContractComponent.ts, і цей підписується для оновлення подання.

RestClientService.ts:

export abstract class RestClientService<T extends BaseModel> {

      public GetAll = (path: string, property: string): Observable<T[]> => {
        let fullPath = this.actionUrl + path;
        let observable = this._http.get(fullPath).map(res => this.extractData(res, property));
        observable = observable.share();  //allows multiple subscribers without making again the http request
        observable.subscribe(
          (res) => {},
          error => this.handleError2(error, "GetAll", fullPath),
          () => {}
        );
        return observable;
      }

  private extractData(res: Response, property: string) {
    ...
  }
  private handleError2(error: any, method: string, path: string) {
    ...
  }

}

ContractService.ts:

export class ContractService extends RestClientService<Contract> {
  private GET_ALL_ITEMS_REST_URI_PATH = "search";
  private GET_ALL_ITEMS_PROPERTY_PATH = "contract";
  public getAllItems(): Observable<Contract[]> {
    return this.GetAll(this.GET_ALL_ITEMS_REST_URI_PATH, this.GET_ALL_ITEMS_PROPERTY_PATH);
  }

}

ContractComponent.ts:

export class ContractComponent implements OnInit {

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

}

0

Я написав кеш-клас,

/**
 * Caches results returned from given fetcher callback for given key,
 * up to maxItems results, deletes the oldest results when full (FIFO).
 */
export class StaticCache
{
    static cachedData: Map<string, any> = new Map<string, any>();
    static maxItems: number = 400;

    static get(key: string){
        return this.cachedData.get(key);
    }

    static getOrFetch(key: string, fetcher: (string) => any): any {
        let value = this.cachedData.get(key);

        if (value != null){
            console.log("Cache HIT! (fetcher)");
            return value;
        }

        console.log("Cache MISS... (fetcher)");
        value = fetcher(key);
        this.add(key, value);
        return value;
    }

    static add(key, value){
        this.cachedData.set(key, value);
        this.deleteOverflowing();
    }

    static deleteOverflowing(): void {
        if (this.cachedData.size > this.maxItems) {
            this.deleteOldest(this.cachedData.size - this.maxItems);
        }
    }

    /// A Map object iterates its elements in insertion order — a for...of loop returns an array of [key, value] for each iteration.
    /// However that seems not to work. Trying with forEach.
    static deleteOldest(howMany: number): void {
        //console.debug("Deleting oldest " + howMany + " of " + this.cachedData.size);
        let iterKeys = this.cachedData.keys();
        let item: IteratorResult<string>;
        while (howMany-- > 0 && (item = iterKeys.next(), !item.done)){
            //console.debug("    Deleting: " + item.value);
            this.cachedData.delete(item.value); // Deleting while iterating should be ok in JS.
        }
    }

    static clear(): void {
        this.cachedData = new Map<string, any>();
    }

}

Це все статично через те, як ми його використовуємо, але не соромтеся робити його нормальним класом та сервісом. Я не впевнений, чи кутовий зберігає одиничний екземпляр протягом усього часу (новий для 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();з моменту завершення спостереження 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);
  }

}

0

Ви можете створити простий кешований клас <>, який допомагає керувати даними, отриманими з 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();
    }

}

Використання

Оголосити об'єкт, кешований <> (імовірно, як частина послуги):

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-кешування ! Це краще відповідає вашому сценарію.

Перевага від використання цього

  • Він викликає 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

Ви спробували запустити код, який у вас уже є?

Оскільки ви будуєте Спостережуване з обіцянки, що виникла в результаті 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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.