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


Answers

@Cristian 제안에 따르면, 이것은 한 번만 방출 한 다음 완료하는 HTTP 관찰 가능 항목에 대해 잘 작동하는 한 가지 방법입니다.

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

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

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에서 이것을하는 관용적 인 방법입니까, 아니면 대신 다른 것을해야합니까?

참고 : 앵귤러 5 새 HttpClient 당, 모든 예제에서 .map(res => res.json()) 부분은 이제 쓸모가 없습니다. JSON 결과는 기본적으로 가정됩니다.




좋은 대답.

아니면 이렇게 할 수 있습니다 :

This is from latest version of rxjs. I am using 5.5.7 version of rxjs

import {share} from "rxjs/operators";

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




업데이트 : Ben Lesh는 5.2.0 이후의 다음 부 릴리스를 말하면서 shareReplay ()를 호출하여 진정으로 캐시 할 수 있습니다.

이전 .....

첫째로, share () 또는 publishReplay (1) .refCount ()를 사용하지 마십시오. 그것들은 동일하고 문제가 있습니다. 관측 가능 상태 인 동안 연결이 이루어지면 공유 할 수 있습니다. , 새로운 관측 가능 객체를 다시 생성합니다. 실제로는 캐싱하지 않습니다.

Birowski는 ReplaySubject를 사용하는 위의 올바른 솔루션을 제공했습니다. ReplaySubject는 우리가 case 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을 첫 번째 인수로 전달하는 대신 관측 가능 함수를 반환하는 함수를 전달한다는 점에 주목하십시오. 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")



나는 캐시 클래스를 썼다.

/**
 * 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 트릭을 사용하는 좀 더 똑똑한 방법이있을 수 있다고 생각하지만 이것은 내 목적에 잘 Observable .




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

My personal favourite is to make use of async methods for calls that make network requests. The methods themselves don't return a value, instead they update a BehaviorSubject within the same service, which components will subscribe to.

Now Why use a BehaviorSubject instead of an Observable ? Because,

  • Upon subscription BehaviorSubject returns the last value whereas A regular observable only triggers when it receives an onnext .
  • If you want to retrieve the last value of the BehaviorSubject in a non-observable code (without a subscription), you can use the getValue() method.

예:

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

Then, wherever required, we can just subscribe to customers$ .

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

Or maybe you want to use it directly in a template

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

So now, until you make another call to getCustomers , the data is retained in the customers$ BehaviorSubject.

So what if you want to refresh this data? just make a call to getCustomers()

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

Using this method, we don't have to explicitly retain the data between subsequent network calls as it's handled by the BehaviorSubject .

PS: Usually when a component gets destroyed it's a good practice to get rid of the subscriptions, for that you can use the method suggested in this answer.




나는 그 질문에 별표를 붙 였지만, 나는 이것에 노력하고 시도 할 것이다.

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

여기에 proof :)

하나의 테이크 어웨이가 있습니다 : getCustomer().subscribe(customer$)

우리는 getCustomer() 의 api 응답을 구독하지 않습니다. 우리는 또 다른 Observable을 구독 할 수있는 관찰 가능한 ReplaySubject를 구독하고 있습니다. 그리고 이것은 (가장 중요한 것은) 마지막으로 내 보낸 값을 보유하고 다음 중 하나에 다시 게시합니다. 그것은 (ReplaySubject의) 가입자입니다.




It's .publishReplay(1).refCount(); or .publishLast().refCount(); since Angular Http observables complete after request.

This simple class caches the result so you can subscribe to .value many times and makes only 1 request. You can also use .reload() to make new request and publish data.

You can use it like:

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

and the source:

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

}



공식 캐시 메커니즘을 사용할 수도 있습니다.

각도 캐시




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

아래 코드 참조

* 면책 조항 : 저는 rxjs에 익숙하지 않으므로, 관측 가능 / 관찰자 접근법을 잘못 사용하고 있다는 것을 명심하십시오. 내 솔루션은 순전히 내가 찾은 다른 솔루션을 모아 놓은 것으로 단순하게 잘 문서화 된 솔루션을 찾지 못한 결과입니다. 따라서 나는 다른 사람들을 돕기를 희망하면서 완전한 코드 솔루션을 제공하고있다.

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

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

솔루션 : 구성 요소는 서비스 메소드에 "가입"합니다. service 메소드는 구성 요소가 구독 콜백을 통해 액세스하는 제품 객체의 배열을 반환합니다. 서비스 메소드는 새로 생성 된 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의 Product : Product [] '의 로컬 캐시가 유지되고 이후에 "GetProducts ()"를 호출하면 캐시 된 값이 반환됩니다. 마지막으로, '메모리 누출'을 막기 위해 관찰 / 구독을 어떻게 닫아야하는지에 대한 의견을 읽었습니다. 나는 이것을 여기에 포함시키지 않았지만, 명심해야 할 것이있다.




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

다음과 같은 방법이 있다고 가정 해 보겠습니다.

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

@ngx-cache/coreCached 데코레이터를 사용하여 Cached 저장소에서 HTTP 호출을 만드는 메소드의 반환 값을 저장할 수 있습니다 ( 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());
}

캐싱 API 를 사용하여 캐싱 메소드 ( has , get , set )를 사용할 수도 있습니다.

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';
  }
}

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




Related