RxJs 5에서 Angular Http 네트워크 호출 결과를 공유하는 올바른 방법은 무엇입니까?


303

Http를 사용하여 네트워크 호출을 수행하고 http 옵저버 블을 반환하는 메소드를 호출합니다.

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

이 관찰 가능 항목을 가져 와서 여러 구독자를 추가하는 경우 :

let network$ = getCustomer();

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

우리가 원하는 것은 이것이 여러 네트워크 요청을 일으키지 않도록하는 것입니다.

이것은 비정상적인 시나리오처럼 보이지만 실제로는 매우 일반적입니다. 예를 들어 호출자가 오류 메시지를 표시하기 위해 옵저버 블에 가입하고 비동기 파이프를 사용하여 템플릿에 전달하는 경우 이미 두 명의 가입자가 있습니다.

RxJs 5에서 올바른 방법은 무엇입니까?

즉, 이것은 잘 작동하는 것 같습니다 :

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

그러나 이것이 RxJs 5에서 이것을하는 관용적 인 방법입니까, 아니면 다른 것을해야합니까?

참고 : Angular 5 new 에 따라 JSON 결과가 기본적으로 가정되므로 모든 예제 HttpClient.map(res => res.json())부분은 이제 쓸모가 없습니다.


1
> share는 publish (). refCount ()와 동일합니다. 사실 그렇지 않습니다. 다음 토론을보십시오 : github.com/ReactiveX/rxjs/issues/1363
Christian

1
문제에 따르면 편집 된 질문은 코드의 문서가 업데이트되어야하는 것처럼 보입니다-> github.com/ReactiveX/rxjs/blob/master/src/operator/share.ts
Angular University

나는 '의존한다'고 생각한다. 그러나 데이터를 로컬로 b / c로 캐시 할 수없는 호출의 경우 매개 변수 변경 / 조합으로 인해 의미가 없을 수 있습니다. .share ()는 절대적으로 올바른 것으로 보입니다. 그러나 로컬로 캐시 할 수 있다면 ReplaySubject / BehaviorSubject에 관한 다른 답변 중 일부도 좋은 솔루션입니다.
JimB

데이터를 캐시해야 할뿐만 아니라 캐시 된 데이터를 업데이트 / 수정해야한다고 생각합니다. 일반적인 경우입니다. 예를 들어, 캐시 된 모델에 새 필드를 추가하거나 필드 값을 업데이트하려는 경우. 아마도 CRUD 메소드 로 싱글 톤 DataCacheService 를 만드는 것이 더 좋은 방법일까요? 같이 가게돌아 오는 . 어떻게 생각해?
slideshowp2

ngx-cacheable을 간단하게 사용할 수 있습니다 ! 시나리오에 더 적합합니다. 아래 내 답변을 참조하십시오
Tushar Walzade

답변:


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()또한 사용할 수도 있지만 콜백이 끝나면 올바른 값이 반환되도록해야합니다.
Günter Zöchbauer

3
호출 사이트 .subscribe()에 값이 필요하지 않은 경우 반환 값 null에 따라 얻을 수 있기 때문에 그렇게 할 수 this.extractData있지만 IMHO는 코드의 의도를 잘 표현하지 못합니다.
Günter Zöchbauer

2
this.extraData끝이 좋아 extraData() { if(foo) { doSomething();}}당신이 원하는하지 않을 수 있습니다 반환되는 마지막 표정의 다른 결과를.
Günter Zöchbauer

9
@ 건터, 코드 주셔서 감사합니다, 그것은 작동합니다. 그러나 나는 왜 당신이 데이터와 Observable을 별도로 추적하는지 이해하려고 노력하고 있습니다. 이와 같이 Observable <Data> 만 캐싱하여 동일한 효과를 효과적으로 얻지 못합니까? if (this.observable) { return this.observable; } else { this.observable = this.http.get(url) .map(res => res.json().data); return this.observable; }
July.Tech

3
@HarleenKaur 강력한 JSON 검사 및 자동 완성을 위해 수신 된 JSON이 직렬화 해제 된 클래스입니다. 그것을 사용할 필요는 없지만 일반적입니다.
Günter Zöchbauer

44

@Cristian 제안에 따르면, 이것은 HTTP Observable에 잘 작동하는 한 가지 방법으로 한 번만 방출 한 다음 완료됩니다.

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

이 방법을 사용하는 데 몇 가지 문제가 있습니다. 반환 된 관찰 가능 항목을 취소하거나 다시 시도 할 수 없습니다. 이것은 당신에게 문제가되지 않을 수도 있지만 다시 발생할 수 있습니다. 이것이 문제인 경우, share운영자는 합리적인 선택이 될 수 있습니다 (불쾌한 경우도 있음). 옵션에 대한 자세한 내용은이 블로그 게시물의 의견 섹션을 참조하십시오 : blog.jhades.org/…
Christian

1
작은 설명 ... 공유 대상 소스 옵저버를 엄격하게 publishLast().refCount()취소 할 수는 없지만, 옵저버 블에 대한 모든 구독을 refCount취소 한 후에는 순 효과는 소스 옵저버 블을 구독 취소하여 "비행"인 경우 취소합니다.
Christian

@Christian Hey, "취소하거나 재 시도 할 수 없습니다"라는 말의 의미를 설명 할 수 있습니까? 감사.
undefined

37

업데이트 : Ben Lesh에 따르면 5.2.0 이후의 다음 마이너 릴리스에서는 shareReplay ()를 호출하여 실제로 캐시 할 수 있습니다.

이전 ...

첫째, share () 또는 publishReplay (1) .refCount ()를 사용하지 마십시오. 동일하고 문제는, Observable이 활성화 된 상태에서 연결이 완료된 경우에만 공유하고 완료 한 후 연결하면 공유한다는 것입니다 실제로 캐싱이 아닌 새로운 관측 가능 번역을 다시 생성합니다.

Birowski는 ReplaySubject를 사용하는 올바른 솔루션을 제공했습니다. ReplaySubject는 우리의 경우 1에 주어진 값 (bufferSize)을 캐시합니다. refCount가 0에 도달하고 새로운 연결을 설정하면 캐싱에 대한 올바른 동작 인 share ()와 같은 새로운 관찰 가능 값을 생성하지 않습니다.

재사용 가능한 기능은 다음과 같습니다.

export function cacheable<T>(o: Observable<T>): Observable<T> {
  let replay = new ReplaySubject<T>(1);
  o.subscribe(
    x => replay.next(x),
    x => replay.error(x),
    () => replay.complete()
  );
  return replay.asObservable();
}

사용 방법은 다음과 같습니다.

import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import { cacheable } from '../utils/rxjs-functions';

@Injectable()
export class SettingsService {
  _cache: Observable<any>;
  constructor(private _http: Http, ) { }

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

다음은 캐시 가능한 기능의 고급 버전입니다.이 기능은 자체 조회 테이블 + 사용자 정의 조회 테이블을 제공 할 수 있습니다. 이런 식으로 위의 예와 같이 this._cache를 확인할 필요가 없습니다. 또한 Observable을 첫 번째 인수로 전달하는 대신 Observable을 반환하는 함수를 전달합니다. 이것은 Angular의 Http가 즉시 실행되기 때문입니다. 우리의 캐시.

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

용법:

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

이 솔루션을 RxJs 연산자로 사용하지 않을 이유가 const data$ = this._http.get('url').pipe(cacheable()); /*1st subscribe*/ data$.subscribe(); /*2nd subscribe*/ data$.subscribe();있습니까? 따라서 다른 연산자처럼 작동합니다.
Felix

31

rxjs 5.4.0 에는 새로운 shareReplay 메소드가 있습니다.

저자는 "AJAX 결과 캐싱과 같은 것을 다루기에 이상적" 이라고 명시 적으로 말한다

rxjs PR # 2443 feat (shareReplay) :의 shareReplay변형 추가publishReplay

shareReplay는 ReplaySubject를 통해 멀티 캐스트 된 소스 인 Observable을 반환합니다. 해당 재생 주제는 소스의 오류에 따라 재활용되지만 소스의 완료에는 없습니다. 이를 통해 shareReplay는 재시도 가능하기 때문에 AJAX 결과 캐싱과 같은 것을 처리하는 데 이상적입니다. 그러나 반복 동작이지만 소스 관찰 가능을 반복하지 않고 소스 관찰 가능 값을 반복한다는 점에서 공유와 다릅니다.


이것과 관련이 있습니까? 이 문서는 2014 년부터입니다. github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/…
Aaron Hoffman

4
.shareReplay (1, 10000)을 관찰 가능 항목에 추가하려고 시도했지만 캐싱 또는 동작 변경이 발견되지 않았습니다. 사용 가능한 실례가 있습니까?
Aydus-Matthew

changelog github.com/ReactiveX/rxjs/blob/…를 보면 이전에 나타 났고 v5에서 제거되었고 5.4로 다시 추가되었습니다. rx-book 링크는 v4를 참조하지만 현재 LTS v5.5.6 및 v6에 있습니다. rx-book 링크가 오래되었다고 생각합니다.
Jason Awbrey

25

기사 에 따르면

publishReplay (1) 및 refCount를 추가하여 Observable에 캐싱을 쉽게 추가 할 수 있습니다.

그래서 if 문 안에 추가하십시오.

.publishReplay(1)
.refCount();

.map(...)


11

rxjs 버전 5.4.0 (2017-05-09)shareReplay 지원이 추가 되었습니다 .

shareReplay를 사용하는 이유는 무엇입니까?

일반적으로 여러 구독자간에 실행하지 않으려는 부작용 또는 과세 계산이있는 경우 shareReplay를 사용하려고합니다. 또한 이전에 방출 된 값에 액세스해야하는 스트림에 늦은 구독자가있을 경우 상황에서 유용 할 수 있습니다. 서브 스크립 션에서 가치를 재생하는이 기능은 공유와 shareReplay를 차별화하는 것입니다.

이것을 사용하기 위해 각도 서비스를 쉽게 수정하고 캐시 된 결과로 옵저버 블을 반환하여 http 호출을 한 번만 할 수 있습니다 (첫 번째 호출이 성공했다고 가정).

각도 서비스 예

다음은를 사용하는 매우 간단한 고객 서비스입니다 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이 "콜드" 이므로 생성자는이 호출을 수행 할 수 있습니다.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()을 구독하지 않고 다른 Observable을 구독 할 수있는 ReplaySubject를 구독하고 있으며 (이것은 중요합니다) 마지막으로 방출 된 값을 유지하고 (ReplaySubject의 ) 가입자.


1
이 접근 방식은 rxjs를 잘 활용하고 사용자 지정 논리를 추가 할 필요가 없으므로 감사합니다.
Thibs

7

http get 결과를 sessionStorage에 저장하고 세션에 사용하여 서버를 다시 호출하지 않는 방법을 찾았습니다.

사용 제한을 피하기 위해 github API를 호출하는 데 사용했습니다.

@Injectable()
export class HttpCache {
  constructor(private http: Http) {}

  get(url: string): Observable<any> {
    let cached: any;
    if (cached === sessionStorage.getItem(url)) {
      return Observable.of(JSON.parse(cached));
    } else {
      return this.http.get(url)
        .map(resp => {
          sessionStorage.setItem(url, resp.text());
          return resp.json();
        });
    }
  }
}

참고로 sessionStorage 한도는 5M (또는 4.75M)입니다. 따라서 큰 데이터 세트에는 이와 같이 사용하면 안됩니다.

------ edit -------------
sessionStorage 대신 메모리 데이터를 사용하는 F5로 데이터를 새로 고치려면;

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

세션 저장소에 저장하면 앱을 떠날 때 세션 저장소가 어떻게 파괴됩니까?
Gags

그러나 이것은 사용자에게 예기치 않은 동작을 초래합니다. 사용자가 브라우저의 F5 또는 새로 고침 버튼을 누르면 서버에서 새로운 데이터를 기대합니다. 그러나 실제로 그는 localStorage에서 오래된 데이터를 얻고 있습니다. 버그 리포트, 지원 티켓 등 수신 ... 이름에서 알 수 있듯이 전체 세션에서 일관성 sessionStorage있을 것으로 예상되는 데이터에만 사용합니다 .
마틴 슈나이더

@ MA-Maddin은 "사용 제한을 피하기 위해 사용했습니다"라고 언급했습니다. F5를 사용하여 데이터를 새로 고치려면 sessionStorage 대신 메모리를 사용해야합니다. 이 방법으로 답을 편집했습니다.
allenhwkim

네, 유스 케이스 일 수 있습니다. 모든 사람들이 캐시 에 대해 이야기 하고 OP가 getCustomer그의 예를 가지고 있기 때문에 방금 촉발되었습니다 . ;) 따라서 위험을 보지 못할 수도있는 ppl에 경고하고 싶었습니다. :
Martin Schneider

5

unsubscribe ()가 HTTP 요청을 취소하도록 하려는지 여부에 따라 선택한 구현이 달라집니다.

어떤 경우, 타이프 라이터의 장식이 동작을 표준화의 좋은 방법입니다. 이것은 내가 쓴 것입니다 :

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

데코레이터 정의 :

/**
 * Decorator that replays and connects to the Observable returned from the function.
 * Caches the result using all arguments to form a key.
 * @param target
 * @param name
 * @param descriptor
 * @returns {PropertyDescriptor}
 */
export function CacheObservableArgsKey(target: Object, name: string, descriptor: PropertyDescriptor) {
  const originalFunc = descriptor.value;
  const cacheMap = new Map<string, any>();
  descriptor.value = function(this: any, ...args: any[]): any {
    const key = args.join('::');

    let returnValue = cacheMap.get(key);
    if (returnValue !== undefined) {
      console.log(`${name} cache-hit ${key}`, returnValue);
      return returnValue;
    }

    returnValue = originalFunc.apply(this, args);
    console.log(`${name} cache-miss ${key} new`, returnValue);
    if (returnValue instanceof Observable) {
      returnValue = returnValue.publishReplay(1);
      returnValue.connect();
    }
    else {
      console.warn('CacheHttpArgsKey: value not an Observable cannot publishReplay and connect', returnValue);
    }
    cacheMap.set(key, returnValue);
    return returnValue;
  };

  return descriptor;
}

안녕 @Arlo-위의 예제는 컴파일되지 않습니다. Property 'connect' does not exist on type '{}'.라인에서 returnValue.connect();. 정교하게 할 수 있습니까?
발굽

4

Rxjs Observer / Observable + 캐싱 + 구독을 사용하여 캐시 가능한 HTTP 응답 데이터

아래 코드를 참조하십시오

* 면책 조항 : rxjs를 처음 사용하므로 관찰 / 관찰 방식을 잘못 사용하고있을 수 있습니다. 내 솔루션은 순전히 내가 찾은 다른 솔루션의 대기업이며, 잘 문서화 된 간단한 솔루션을 찾지 못한 결과입니다. 따라서 다른 사람들에게 도움이되기를 바라며 완전한 코드 솔루션을 찾고 있습니다.

*이 접근법은 GoogleFirebaseObservables를 기반으로합니다. 불행히도 나는 그들이 처한 일을 복제 할 적절한 경험 / 시간이 부족합니다. 그러나 다음은 일부 캐시 가능한 데이터에 대한 비동기 액세스를 제공하는 간단한 방법입니다.

상황 : '제품 목록'구성 요소는 제품 목록을 표시하는 작업입니다. 이 사이트는 페이지에 표시된 제품을 '필터링'하는 일부 메뉴 버튼이있는 단일 페이지 웹 앱입니다.

솔루션 : 컴포넌트가 서비스 메소드를 "구독"합니다. 서비스 메소드는 컴포넌트가 구독 콜백을 통해 액세스하는 제품 오브젝트의 배열을 리턴합니다. 서비스 메소드는 새로 작성된 Observer에서 활동을 랩핑하고 관찰자를 리턴합니다. 이 옵저버 내에서 캐시 된 데이터를 검색하여 구독자 (구성 요소)에게 다시 전달하여 반환합니다. 그렇지 않으면 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는 공유 인스턴스 인 것 같으므로 탐색 중에 ProductService에서 'products : Product []'의 로컬 캐시가 유지되며 "GetProducts ()"에 대한 후속 호출은 캐시 된 값을 리턴합니다. 마지막으로, '메모리 누수'를 막기 위해 옵저버 블 / 서브 스크립 션을 닫는 방법에 대한 의견을 읽었습니다. 나는 이것을 여기에 포함시키지 않았지만 명심해야 할 것입니다.


1
참고-이후 RxJS BehaviorSubjects와 관련된보다 강력한 솔루션을 찾았습니다.이 솔루션은 코드를 단순화하고 '오버 헤드'를 크게 줄입니다. products.service.ts에서 1. import 'BehaviorSubject} from'rxjs '; 2. 'products : Product []'를 'product $ : BehaviorSubject <Product []> = new BehaviorSubject <Product []> ([]);로 변경하십시오. 3. 이제 아무 것도 반환하지 않고 간단히 http를 호출 할 수 있습니다. http_getProducts () {this.http.get (...). map (res => res.json ()). subscribe (products => this.product $ .next (products))};
ObjectiveTC

1
로컬 변수 'product $'는 behaviorSubject이며 최신 제품을 EMIT하고 저장합니다 (3 부에서 product $ .next (..) 호출에서). 이제 구성 요소에 서비스를 정상적으로 주입하십시오. productService.product $ .value를 사용하여 가장 최근에 할당 된 product $ 값을 얻습니다. 또는 product $가 새 값을 수신 할 때마다 조치를 수행하려면 product $를 구독하십시오 (예 : product $ .next (...) 함수가 3 부에서 호출 됨).
ObjectiveTC

1
예를 들어, products.component.ts에서 ... this.productService.product $ .takeUntil (this.ngUnsubscribe) .subscribe ((products) => {this.category); filterProducts = this.productService.getProductsByCategory (this.category); this.products = 필터링 된 제품; });
ObjectiveTC

1
옵저버 블 구독 취소에 대한 중요한 참고 사항 : ".takeUntil (this.ngUnsubscribe)". 이 스택 오버플로 질문 / 응답을 참조하십시오.이 이벤트는 이벤트에서 탈퇴하는 '사실'권장 방법을 보여줍니다. stackoverflow.com/questions/38008334/…
ObjectiveTC

1
옵저버 블이 데이터를 한 번만 수신하는 경우 .first () 또는 .take (1) 대신 사용할 수 있습니다. 관찰 가능 항목의 다른 모든 '무한 스트림'은 'ngOnDestroy ()'에서 구독 취소해야하며, 그렇지 않으면 중복 '관측 가능'콜백으로 끝날 수 있습니다. stackoverflow.com/questions/28007777/…
ObjectiveTC

3

@ ngx-cache / core 는 특히 HTTP 호출이 브라우저서버 플랫폼 에서 수행되는 경우 http 호출에 대한 캐싱 기능을 유지하는 데 유용 할 수 있다고 가정합니다 .

다음과 같은 방법이 있다고 가정 해 봅시다.

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

당신은 사용할 수 Cached의 장식 @ NGX 캐시 / 코어를 상기는 HTTP 호출을하는 방법에서 반환 된 값을 저장 cache storage( (가) storage구성 할 수에서 구현하시기 바랍니다 겨 씨앗 / 보편적 첫 번째 실행에 오른쪽 -). 다음에 메소드가 호출되면 ( 브라우저 또는 서버 플랫폼 에 상관없이 ) 값이에서 검색됩니다 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';
  }
}

다음은 클라이언트 측 및 서버 측 캐싱을위한 패키지 목록입니다.


1

rxjs 5.3.0

나는 행복하지 않았다 .map(myFunction).publishReplay(1).refCount()

여러 가입자 가있는 경우 경우에 따라 두 번 .map()실행 myFunction됩니다 (한 번만 실행될 것으로 예상합니다). 하나의 수정은publishReplay(1).refCount().take(1)

당신이 할 수있는 또 다른 일은 사용하지 않고 refCount()Observable을 즉시 뜨겁게 만드는 것입니다.

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

구독자에 관계없이 HTTP 요청이 시작됩니다. HTTP GET이 완료되기 전에 구독을 취소하면 취소 여부가 확실하지 않습니다.


1

우리가 원하는 것은 이것이 여러 네트워크 요청을 일으키지 않도록하는 것입니다.

개인적으로 가장 좋아하는 것은 async네트워크 요청을하는 통화 에 메소드를 사용 하는 것입니다. 메소드 자체는 값을 반환하지 않고 대신BehaviorSubject 동일한 서비스 내에서 컴포넌트 .

이제 왜 ? 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.

추신 : 일반적으로 구성 요소가 파괴되면 구독을 제거하는 것이 좋습니다 . 답변 에서 제안 된 방법을 사용할 수 있습니다 .


1

좋은 답변입니다.

또는 당신은 이것을 할 수 있습니다 :

최신 버전의 rxjs에서 가져온 것입니다. 내가 사용하고 5.5.7 버전 RxJS을

import {share} from "rxjs/operators";

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

0

그냥 전화 주 ()지도 및 이전 등록 .

제 경우에는 나머지 서비스를 만들고, 데이터를 추출하고, 오류를 확인하고, 구체적 구현 서비스 (f.ex .: ContractClientService.ts)로 관찰 가능한 것을 반환하는 일반 서비스 (RestClientService.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;
   });
  }

}

0

캐시 클래스를 작성했습니다.

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

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

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

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

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

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

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

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

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

}

우리가 그것을 사용하는 방법 때문에 모두 정적이지만, 일반 클래스와 서비스로 자유롭게 만드십시오. angular가 전체적으로 단일 인스턴스를 유지하는지 확실하지 않습니다 (Angular2에 새로운).

그리고 이것이 내가 사용하는 방법입니다.

            let httpService: Http = this.http;
            function fetcher(url: string): Observable<any> {
                console.log("    Fetching URL: " + url);
                return httpService.get(url).map((response: Response) => {
                    if (!response) return null;
                    if (typeof response.json() !== "array")
                        throw new Error("Graph REST should return an array of vertices.");
                    let items: any[] = graphService.fromJSONarray(response.json(), httpService);
                    return array ? items : items[0];
                });
            }

            // If data is a link, return a result of a service call.
            if (this.data[verticesLabel][name]["link"] || this.data[verticesLabel][name]["_type"] == "link")
            {
                // Make an HTTP call.
                let url = this.data[verticesLabel][name]["link"];
                let cachedObservable: Observable<any> = StaticCache.getOrFetch(url, fetcher);
                if (!cachedObservable)
                    throw new Error("Failed loading link: " + url);
                return cachedObservable;
            }

나는 좀 더 영리한 방법이있을 수 있다고 가정 Observable합니다.


0

이 캐시 계층을 사용하면 필요한 모든 작업을 수행하고 심지어 아약스 요청에 대한 캐시를 관리 할 수 ​​있습니다.

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

사용하기가 훨씬 쉽습니다.

@Component({
    selector: 'home',
    templateUrl: './html/home.component.html',
    styleUrls: ['./css/home.component.css'],
})
export class HomeComponent {
    constructor(AjaxService:AjaxService){
        AjaxService.postCache("/api/home/articles").subscribe(values=>{console.log(values);this.articles=values;});
    }

    articles={1:[{data:[{title:"first",sort_text:"description"},{title:"second",sort_text:"description"}],type:"Open Source Works"}]};
}

층은 (주입 가능한 각도 서비스로서)

import { Injectable }     from '@angular/core';
import { Http, Response} from '@angular/http';
import { Observable }     from 'rxjs/Observable';
import './../rxjs/operator'
@Injectable()
export class AjaxService {
    public data:Object={};
    /*
    private dataObservable:Observable<boolean>;
     */
    private dataObserver:Array<any>=[];
    private loading:Object={};
    private links:Object={};
    counter:number=-1;
    constructor (private http: Http) {
    }
    private loadPostCache(link:string){
     if(!this.loading[link]){
               this.loading[link]=true;
               this.links[link].forEach(a=>this.dataObserver[a].next(false));
               this.http.get(link)
                   .map(this.setValue)
                   .catch(this.handleError).subscribe(
                   values => {
                       this.data[link] = values;
                       delete this.loading[link];
                       this.links[link].forEach(a=>this.dataObserver[a].next(false));
                   },
                   error => {
                       delete this.loading[link];
                   }
               );
           }
    }

    private setValue(res: Response) {
        return res.json() || { };
    }

    private handleError (error: Response | any) {
        // In a real world app, we might use a remote logging infrastructure
        let errMsg: string;
        if (error instanceof Response) {
            const body = error.json() || '';
            const err = body.error || JSON.stringify(body);
            errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
        } else {
            errMsg = error.message ? error.message : error.toString();
        }
        console.error(errMsg);
        return Observable.throw(errMsg);
    }

    postCache(link:string): Observable<Object>{

         return Observable.create(observer=> {
             if(this.data.hasOwnProperty(link)){
                 observer.next(this.data[link]);
             }
             else{
                 let _observable=Observable.create(_observer=>{
                     this.counter=this.counter+1;
                     this.dataObserver[this.counter]=_observer;
                     this.links.hasOwnProperty(link)?this.links[link].push(this.counter):(this.links[link]=[this.counter]);
                     _observer.next(false);
                 });
                 this.loadPostCache(link);
                 _observable.subscribe(status=>{
                     if(status){
                         observer.next(this.data[link]);
                     }
                     }
                 );
             }
            });
        }
}

0

그건 .publishReplay(1).refCount();.publishLast().refCount(); 각도 HTTP를 관찰 가능한부터 요청 후 완료합니다.

이 간단한 클래스는 결과를 캐시하므로 .value를 여러 번 구독 할 수 있으며 하나의 요청 만합니다. .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 서버에서 검색된 데이터를 관리하는 데 도움이되는 간단한 클래스 Cacheable <>을 작성할 수 있습니다.

declare type GetDataHandler<T> = () => Observable<T>;

export class Cacheable<T> {

    protected data: T;
    protected subjectData: Subject<T>;
    protected observableData: Observable<T>;
    public getHandler: GetDataHandler<T>;

    constructor() {
      this.subjectData = new ReplaySubject(1);
      this.observableData = this.subjectData.asObservable();
    }

    public getData(): Observable<T> {
      if (!this.getHandler) {
        throw new Error("getHandler is not defined");
      }
      if (!this.data) {
        this.getHandler().map((r: T) => {
          this.data = r;
          return r;
        }).subscribe(
          result => this.subjectData.next(result),
          err => this.subjectData.error(err)
        );
      }
      return this.observableData;
    }

    public resetCache(): void {
      this.data = null;
    }

    public refresh(): void {
      this.resetCache();
      this.getData();
    }

}

용법

Cacheable <> 객체를 선언합니다 (아마도 서비스의 일부로).

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

핸들러 :

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

구성 요소에서 전화 :

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

여러 구성 요소를 구독 할 수 있습니다.

자세한 내용과 코드 예제는 다음과 같습니다. http://devinstance.net/articles/20171021/rxjs-cacheable


0

ngx-cacheable을 간단하게 사용할 수 있습니다 ! 시나리오에 더 적합합니다.

이것을 사용하는 이점

  • rest API를 한 번만 호출하고 응답을 캐시하며 다음 요청에 대해 동일하게 리턴합니다.
  • 작성 / 업데이트 / 삭제 조작 후 필요에 따라 API를 호출 할 수 있습니다.

따라서 서비스 클래스 는 다음과 같습니다.

import { Injectable } from '@angular/core';
import { Cacheable, CacheBuster } from 'ngx-cacheable';

const customerNotifier = new Subject();

@Injectable()
export class customersService {

    // relieves all its caches when any new value is emitted in the stream using notifier
    @Cacheable({
        cacheBusterObserver: customerNotifier,
        async: true
    })
    getCustomer() {
        return this.http.get('/someUrl').map(res => res.json());
    }

    // notifies the observer to refresh the data
    @CacheBuster({
        cacheBusterNotifier: customerNotifier
    })
    addCustomer() {
        // some code
    }

    // notifies the observer to refresh the data
    @CacheBuster({
        cacheBusterNotifier: customerNotifier
    })
    updateCustomer() {
        // some code
    }
}

자세한 내용은 다음 링크를 참조하십시오.


-4

이미 가지고있는 코드를 실행 해 보셨습니까?

에서 발생하는 약속에서 Observable을 구성하고 getJSON()있으므로 네트워크 요청은 누구나 구독하기 전에 이루어집니다. 결과 약속은 모든 가입자가 공유합니다.

var promise = jQuery.getJSON(requestUrl); // network call is executed now
var o = Rx.Observable.fromPromise(promise); // just wraps it in an observable
o.subscribe(...); // does not trigger network call
o.subscribe(...); // does not trigger network call
// ...

Angular 2 특정
Angular University
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.