Angular 5 - 缓存的服务实现

时间:2018-01-06 11:32:21

标签: angular typescript rxjs angular-services

我正在寻找实施Angular服务的正确方法。

我的服务

const endpoint = 'http://127.0.0.1:8000/api/brands/'

@Injectable()
export class BrandService {

  private brands:Observable<Array<Brand>>;

  constructor(private http: Http) { }

  list(): Observable<Array<Brand>> {
    if(!this.brands){
      this.brands = this.http.get(endpoint).
                          .map(response => response.json())
                          .publishReplay(1) // publishReplay(1) tells rxjs to cache the most recent value which is perfect for single value http calls
                          .refCount(); // refCount() is used to keep the observable alive for as long as there are subscribers
    }    
    return this.brands;
  }

  clearCache() {
    this.brands = null;
  }

  create(brand: Brand): Observable<Brand> {
    Object.entries(brand).forEach(([key, value]) => {
      formData.append(key, value);
    });
    return this.http.post(endpoint+'create/', formData)
      .map(response => response.json())
      .catch(this.handleError);
  }

  get(id): Observable<Brand> {
    return this.http.get(endpoint+id)
        .map(response => response.json())
        .catch(this.handleError);
  }

  private handleError(error:any, caught:any): any {
    console.log(error, caught);
  }

}

我设法使用带有Observable对象的publishReplay方法创建了一个缓存机制。 现在,我希望我的服务每分钟自动更新列表。 我尝试使用setInterval(this.clearCache, 1000*60)并清除它,但我要完成的是更新列表并在列表发生更改时每分钟通知所有订阅者。

了解限制服务器请求的所有数据的最佳做法是什么?

更新1(验证者问题)

正如马丁所说,我改变了列表方法如下:

list(): Observable<Array<Brand>> {
    if(!this.brands){
      this.brands = Observable.timer(0, 60 * 1000)
                        .switchMap(() => {
                          console.log('REQUESTING DATA....')
                          return this.http.get(endpoint);
                        })
                        .map(response => response.json())
                        .publishReplay(1)
                        .refCount();
    }
    return this.brands;
  }

它的工作正常,除了验证器。

以下验证员之前正在工作:

private brandNameValidator(control: FormControl) {
    return this.brandService.list().map(res => {
      return res.filter(brand => 
            brand.name.toLowerCase() === control.value.toLowerCase() && (!this.editMode || brand.id != this.brand.id)
        ).length>0 ? { nameAlreadyExist: true } : null;
    });
  }

现在,该字段仍处于PENDING状态。

更新2(验证方解决方案)

我使用Promise对象解决了这个问题:

private brandNameValidator(control: FormControl) {
    return new Promise (resolve => {
      let subscription = this.brandService.list().subscribe(res => {
        let brandsFound = res.filter(brand => 
          brand.name.toLowerCase() === control.value.toLowerCase() && (!this.editMode || brand.id != this.brand.id)
        )
        if (brandsFound.length>0) {
          resolve({ nameAlreadyExist: true });
        } else {
          resolve(null);
        }
        subscription.unsubscribe();
      })
    });
}

更新3(强制列表更新)

创建新品牌后,我想强制更新清单。当我知道列表已经更新时,我不想等下一分钟。

  create(brand: Brand): Observable<Brand> {
    Object.entries(brand).forEach(([key, value]) => {
      formData.append(key, value);
    });
    return this.http.post(endpoint+'create/', formData)
      .map(response => {
        // TODO - Need to update this.brands, but I cannot use the next() method since it isn't a Subject object, but an Observable.
        // All observers need to updated about the addition
        return response.json();
      })
      .catch(this.handleError);
  }

2 个答案:

答案 0 :(得分:4)

你可以这样做:

Observable..timer(0, 60 * 1000)
  .switchMap(() => this.http.get(endpoint))
  .map(response => response.json())
  .publishReplay(1)
  .refCount();

答案 1 :(得分:0)

就个人而言,我倾向于使用elm / flux / redux / ng-rx patern来管理我的服务状态。

我不确定回答你的问题,但这是我的做法(不确定get()和create()中的所有内容都有效,因为我没有测试过整个问题。)

type Brand = string // or more probably a more complex object...
type State = { brands: Brand[]/*, someMoreInfo: any*/ }

// Signature of Actions that can modify the state
type UpdateFn = (state: State) => State 

class MyService implements OnDestroy {
  updater$: Subject<UpdateFn> // Where you send update of the state
  state$: BehaviorSubject<State> // Where you listen to update on the state

  // Where you listen update of the brands (in the state$)
  brands$: Observable<Brand[]> 

  // Just to trigger the first update (if required before the first minute)
  firstUpdate$ = new Subject<void>()

  autoUpdateSub = null

  constructor(initialValue = { brands: []/*, someMoreInfo: {}*/ }) {

    this.state$ = new BehaviorSubject<State>(initialValue)
    this.brands$ = this.state$.pluck('brands')

    this.updater$ = new Subject<UpdateFn>()
    const dispatcher = (state: State, op: UpdateFn) => op(state)

    // Where the magic happen
    // scan and dispatcher execute the Action function received on the 
    // last state and genrate a new state that is sent inside the state$
    // subject (everyone that has subscribte to state$ will receive the 
    // state update).
    this.updater$.scan(dispatcher, initialValue).subscribe(this.state$)

    this.autoUpdateSub =
      //Force update on the fist list() or every minute (not perfect)
      Observable.merge(
        Observable.interval(60 1000),
        this.firstUpdate$.take(1) 
      ).subscribe(_ => this.forceUpdate())

  }

  ngOnDestroy() {
    if (this.autoUpdateSub) this.autoUpdateSub.unsubscribe()
  }


  forceUpdate(): Observable<Brand[]> {
    console.log('update')
    this.http.get(endpoint)
      .map(response => response.json())
      .map((brands: Brands[]) => {
        // pass in a function that returns the new list
        return previousState => {
          const newState = previousState
          newState.brands = brands
          return newState
        }
      })
      .subscribe(brandsUpdateFn => this.updater$.next(brandsUpdateFn))

    return this.brands$;
  }

  list(): Observable<Brand[]> {
    console.log('list')
    this.firstUpdate$.next()
    return this.brands$
  }

  get(id): Observable<Brand> {
    // get the brands (launch a brand request or return the cache)
    return this.brands$
      .switchMap(brands => {
        // find the brand from the index (you have to code it...)
        const index = findBrandByIndex(id, brands)

        // if the brand is found, return it inside an observable
        // otherwise request it
        return index ? Observable.of(brands[index])
          : this.http.get(endpoint + id)
            .map(response => response.json())
      })
      .catch(this.handleError);
  }

  create(brand: Brand): Observable<Brand> {
    Object.entries(brand).forEach(([key, value]) => {
      formData.append(key, value);
    });
    return this.http.post(endpoint + 'create/', formData)
      .map(response => response.json())
      //Optimistic response : add the brand to the store, and force refetch data
      .do(_ => {
        // Add temporary the value to the store
        this.updater$.next((previousState) => {
          const newState = previousState
          newState.brands = [...previousState.brands, brand]
          return newState
        })
        // force fetch the values from the server
        this.forceUpdate()
      })
      .catch(this.handleError);
  }

  private handleError(error:any, caught:any): any {
    console.log(error, caught);
  }
}