4

I'm new to angular 2+ and RxJS, trying to get used to RxJS.

I show load spinner on route transitions, but only if it takes more then certain amount time, lats say 160ms
I have a load spinner as a separate component, with subscription to ngrx store, so I show/hide load spinner based on value in the sore (showSpinner)

In my app root component, I subscribe to router change events, and dispatch actions (SHOW_SPINNER/HIDE_SPINNER)
So the question is, Is there a simpler way to achieve it?

Here are parts of my code

....

export const navigationStreamForSpinnerStatuses = {
  NAVIGATION_STARTED: 'NAVIGATION_STARTED',
  NAVIGATION_IN_PROGRESS: 'NAVIGATION_IN_PROGRESS',
  NAVIGATION_ENDED: 'NAVIGATION_ENDED'
};

....

private navigationStartStream;
private navigationStartStreamWithDelay;
private navigationFinishStream;

constructor(private store: Store<IAppState>, private router: Router) {
  this.navigationStartStream = router.events
    .filter(event => {
      return event instanceof NavigationStart;
    })
    .map(() => navigationStreamForSpinnerStatuses.NAVIGATION_STARTED);

  this.navigationStartStreamWithDelay = this.navigationStartStream
    .delay(160)
    .map(() => navigationStreamForSpinnerStatuses.NAVIGATION_IN_PROGRESS);

  this.navigationFinishStream = router.events
    .filter(event => {
      return event instanceof NavigationEnd || event instanceof NavigationCancel || event instanceof NavigationError;
    })
    .map(() => navigationStreamForSpinnerStatuses.NAVIGATION_ENDED);

  this.navigationStartStream
    .merge(this.navigationFinishStream)
    .merge(this.navigationStartStreamWithDelay)
    .pairwise()
    .subscribe([previousStatus, currentStatus] => {
      if (previousStatus !== navigationStreamForSpinnerStatuses.NAVIGATION_ENDED && currentStatus === navigationStreamForSpinnerStatuses.NAVIGATION_IN_PROGRESS) {
        this.store.dispatch({ type: StateLoaderSpinnerActionsTypes.SHOW_SPINNER });
      } else if (previousStatus === navigationStreamForSpinnerStatuses.NAVIGATION_IN_PROGRESS && currentStatus === navigationStreamForSpinnerStatuses.NAVIGATION_ENDED) {
        this.store.dispatch({ type: StateLoaderSpinnerActionsTypes.HIDE_SPINNER });
      }
    });
}
4

2 回答 2

2

我的方法是基于 Http 请求,这可能并非在所有情况下都有效,因为某些应用程序只会使用 WebSockets 或根本不使用外部请求。但是对于大量应用程序,他们通过 HttpClient (angular 4.3+) 获取所有数据。我写了一个 HttpInterceptor 就是这样做的。

import { HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } from '@angular/common/http'
import { Inject, Injectable, RendererFactory2 } from '@angular/core'
import { Observable, timer } from 'rxjs'
import { filter, takeUntil, tap } from 'rxjs/operators'
import { DOCUMENT } from '@angular/common'

const reqIsSpinnable = (req: HttpRequest<any>) => {
  return req.url.includes('api/')
}

@Injectable()
export class HttpSpinnerInterceptor implements HttpInterceptor {
  constructor(@Inject(DOCUMENT) private doc: HTMLDocument, private rdf: 
    RendererFactory2) { }

  // tslint:disable-next-line:no-null-keyword
  readonly rdr = this.rdf.createRenderer(null, null)

  get spinnerElement() {
    return this.doc.querySelector('#core-spin')
  }

  startSpin() {
    this.rdr.setStyle(this.spinnerElement, 'display', 'block')
  }

  closeSpin() {
    this.rdr.setStyle(this.spinnerElement, 'display', 'none')
  }

  intercept(req: HttpRequest<any>, next: HttpHandler):
    Observable<any> {
    const responseTimer$ = next.handle(req).pipe(filter(e => e instanceof HttpResponse))
    timer(120).pipe(takeUntil(responseTimer$)).subscribe(() => this.startSpin())

    return next.handle(req).pipe(tap(evt => {
      if (reqIsSpinnable(req) && evt instanceof HttpResponse) {
        this.closeSpin()
      }
    }))

} }

于 2018-04-17T17:06:19.177 回答
2

takeUntil如果微调计时器在时间之前返回,请使用操作员取消微调计时器。此外,创建一个 hot observabletimer用于在经过一定时间后触发一个动作。

takeUntil从源 observable 序列返回值,直到另一个 observable 序列或 Promise 产生值。

https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/takeuntil.md

timer返回一个可观察的序列,该序列在到期时间过后以及每个周期之后产生一个值。

https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/timer.md

您可以通过直接在每个流上处理调度来简化您的逻辑。

this.navigationEnd$ = router.events
  .filter(event => event instanceof NavigationEnd || event instanceof NavigationCancel || event instanceof NavigationError);

this.navigationStart$ = router.events
  .filter(event => event instanceof NavigationStart)
  .subscribe(_ => {
    Observable.timer(160)
      .takeUntil(this.navigationEnd$)
      .subscribe(_ => this.store.dispatch({ type: StateLoaderSpinnerActionsTypes.SHOW_SPINNER });
  });

this.navigationEnd$.subscribe(_ => this.store.dispatch({ type: StateLoaderSpinnerActionsTypes.HIDE_SPINNER });

所以我们所做的是听导航的开始并开始一个timer160 毫秒。如果导航结束事件发生在计时器之前,则不会显示任何微调器 ( takeUntil)。否则,将调度 store 操作并显示微调器。无论微调器是否显示,导航完成后,我们都会调度隐藏微调器操作。

于 2017-07-07T14:51:57.293 回答