RxJS:如果请求缓慢,则显示加载

时间:2016-02-18 17:23:53

标签: javascript promise spinner rxjs

我想过使用RxJS来优雅地解决这个问题,但在尝试了各种方法之后,我无法找到如何做到这一点......

我的需要很常见:我做一个休息电话,即。我有一个承诺。 如果响应很快,我只想使用结果。 如果它很慢,我想显示一个微调器,直到请求完成。 这是为了避免微调器的闪烁,然后是数据。

也许可以通过制作两个observable来完成:一个带有promise,另一个带有超时并显示微调器作为副作用。 我尝试switch()但没有取得多大成功,也许是因为另一个可观察者没有产生价值。

有人实施了类似的东西吗?

5 个答案:

答案 0 :(得分:2)

这是我使用过的一个例子。我们在此假设您获得了要作为Observable发送到服务器的数据,称为query$。然后进入的查询将触发loadResults函数,该函数应返回一个promise并将结果放入results$ observable。

现在的诀窍是使用observable$.map(() => new Date())来获取最后一次发射值的时间戳。

然后我们可以比较最后一个查询的时间戳和从服务器进来的最后一个响应。

由于您还希望不仅显示加载动画,而是希望在显示动画之前等待750毫秒,因此我们会介绍延迟时间戳。请参阅下面的评论以获得更多解释。

最后,我们有isLoading$ Observable,其中包含truefalse。订阅它,以获得何时显示/隐藏加载动画的通知。

const query$ = ... // From user input.

const WAIT_BEFORE_SHOW_LOADING = 750;

const results$ = query$.flatMapLatest(loadResults);

const queryTimestamp$ = query$.map(() => new Date());
const resultsTimestamp$ = results$.map(() => new Date());
const queryDelayTimestamp$ = (
    // For every query coming in, we wait 750ms, then create a timestamp.
    query$
    .delay(WAIT_BEFORE_SHOW_LOADING)
    .map(() => new Date())
);

const isLoading$ = (
    queryTimestamp$.combineLatest(
        resultsTimestamp$,
        queryDelayTimestamp$,
        (queryTimestamp, resultsTimestamp, delayTimestamp) => {
            return (
                // If the latest query is more recent than the latest
                // results we got we can assume that
                // it's still loading.
                queryTimestamp > resultsTimestamp &&
                // But only show the isLoading animation when delay has passed
                // as well.
                delayTimestamp > resultsTimestamp
            );
        }
    )
    .startWith(false)
    .distinctUntilChanged()
);

答案 1 :(得分:2)

基于@ PhiLho的answer,我写了一个可管理的运算符,它正是这样做的:

export function executeDelayed<T>(
    fn : () => void,
    delay : number,
    thisArg? : any
) : OperatorFunction<T, T> {
    return function executeDelayedOperation(source : Observable<T>) : Observable<T> {
        let timerSub = timer(delay).subscribe(() => fn());
        return source.pipe(
            tap(
                () => {
                    timerSub.unsubscribe();
                    timerSub = timer(delay).subscribe(() => fn());
                },
                undefined,
                () => {
                    timerSub.unsubscribe();
                }
            )
        );
    }
}

基本上它返回一个函数,它得到Observable source 然后使用给定的timer启动delay 如果此计时器发出next - 事件,则调用该函数 但是,如果来源发出nexttimer会被取消,而新的则会被取消。
在源complete中,timer最终被取消。 然后可以像这样使用此运算符:

this.loadResults().pipe(
    executeDelayed(
        () => this.startLoading(),
        500
    )
).subscribe(results => this.showResult())

我自己并没有对很多操作员进行过操作,因此这种操作符实现可能并不是最好的,但它确实有用 有关如何优化它的任何建议都欢迎:)

修改 正如@DauleDK所提到的,在这种情况下错误不会停止计时器,fn将在delay之后调用。如果那不是你想要的,你需要在onError中添加一个tap - 回调,调用timerSub.unsubscribe()

export function executeDelayed<T>(
    fn : () => void,
    delay : number,
    thisArg? : any
) : OperatorFunction<T, T> {
    return function executeDelayedOperation(source : Observable<T>) : Observable<T> {
        let timerSub = timer(delay).subscribe(() => fn());
        return source.pipe(
            tap(
                () => {
                    timerSub.unsubscribe();
                    timerSub = timer(delay).subscribe(() => fn());
                },
                () => timerSub.unsubscribe(),   // unsubscribe on error
                () => timerSub.unsubscribe()
            )
        );
    }
}

答案 2 :(得分:1)

好的,在我的通勤中考虑更多,我找到了解决方案......

您可以在http://plnkr.co/edit/Z3nQ8q

找到我的实验基地

简而言之,解决方案是实际订阅observable处理微调器(而不是试图以某种方式组合它)。 如果Rest请求的结果在observable触发之前,我们只是取消了微调器的一次性(订阅),所以它什么都不做。 否则,可观察者会发射并显示其旋转器。然后我们可以在收到回复后隐藏它。

代码:

function test(loadTime)
{
  var prom = promiseInTime(loadTime, { id: 'First'}); // Return data after a while
  var restO = Rx.Observable.fromPromise(prom);

  var load = Rx.Observable.timer(750);
  var loadD = load.subscribe(
    undefined,
    undefined,
    function onComplete() { show('Showing a loading spinner'); });

  restO.subscribe(
    function onNext(v) { show('Next - ' + JSON.stringify(v)); },
    function onError(e) { show('Error - ' + JSON.stringify(e)); loadD.dispose(); },
    function onComplete() { show('Done'); loadD.dispose(); }
  );
}

test(500);
test(1500);

不确定这是否是使用RxJS执行此操作的惯用方法,但似乎有效... 当然,欢迎其他解决方案。

答案 3 :(得分:0)

这是我的解决方案:

exports.onCreatePost = functions.firestore.document("/Posts/{userId}/UserPosts/{postId}").onCreate(async (snapshot, context) => {
    const postCreated = snapshot.data();
    const userId = context.params.userId;
    const postId = context.params.postId;

    const followersQuerySnapshot = await admin.firestore().collection("Followers").doc(userId).collection("FollowerIds").get();
    const promises = followersQuerySnapshot.docs.map(  // followersQuerySnapshot.docs is an Array of QueryDocumentSnapshots
      doc => admin
       .firestore()
       .collection("Feed")
       .doc(doc.id)
       .collection("FeedPosts")
       .doc(postId)
       .set(postCreated)
      );

    return Promise.all(promises);  // <= Important, here return the promise returned by Promise.all()
    });

});

答案 4 :(得分:-1)

在获取数据之前,即。创建微调器,为函数设置超时,从而创建微调器。让我们说你愿意等半秒钟,直到显示微调器...它会是这样的:

spinnerTimeout = setTimeout(showSpinner, 500)
fetch(url).then(data => {
  if (spinner) {
    clearTimeout(spinnerTimeout) //this is critical
    removeSpinnerElement()
  }
  doSomethingWith(data)
});

编辑:如果不明显,如果数据到达的时间早于500毫秒(ish),clearTimer将停止执行showSpinner。