我想使用RxJS(版本6)实现加载指示。在异步数据调用完成之前,组件中将显示一个加载指示器(微调器)。我要执行一些规则(这些规则是否正确,可能是另一个问题,也许要发表评论):
我正在Angular项目中实现此功能,但我相信这不是Angular特有的。
我已经找到了这个难题的一些片段,但是我需要帮助将它们组装在一起。
In this SO answer中存在一种操作员的实现方式,它会延迟显示加载指示器。
this article中介绍了Angular的一个不错但不完整的实现。
在this Medium article中描述了在最短的时间内显示加载指示符。
答案 0 :(得分:1)
首先,卢卡斯,这是一个很好的问题!
前言:虽然还有其他方法可以实现您的要求,但我只是想让我的答案更像是详细的分步教程
为方便起见,让我们想象一下,我们有一个执行请求并向我们返回字符串消息的Observable的方法:
const makeARequest: () => Observable<{ msg: string }>;
现在我们可以声明将包含结果的Observables:
// Our result will be either a string message or an error
const result$: Observable<{ msg: string } | { error: string }>;
和加载指示:
// This stream will control a loading indicator visibility
// if we get a true on the stream -- we'll show a loading indicator
// on false -- we'll hide it
const loadingIndicator$: Observable<boolean>;
现在,解决#1
如果数据在1秒钟内成功到达,则不应显示任何指示符(并且数据应正常呈现)
我们可以将timer设置1秒钟,然后将该计时器事件转换为true
值,这意味着将显示加载指示器。 takeUntil
将确保如果result$
在1秒之前到来-我们将不会显示加载指示器:
const showLoadingIndicator$ = timer(1000).pipe(
mapTo(true), // turn the value into `true`, meaning loading is shown
takeUntil(result$) // emit only if result$ wont emit before 1s
);
#2
如果呼叫在1秒内失败,则不应显示任何指示符(并且应显示错误消息)
尽管第一部分将由#1解决,但要显示错误消息,我们需要从源流中捕获错误并将其转换为某种{ error: 'Oops' }
。 catchError运算符将使我们做到这一点:
result$ = makeARequest().pipe(
catchError(() => {
return of({ error: 'Oops' });
})
)
您可能已经注意到我们在两个地方都使用了result$
。这意味着我们将对同一请求Observable进行两个预订,这将发出两个请求,这不是我们想要的。为了解决这个问题,我们可以简单地share在订户中观察到这一点:
result$ = makeARequest().pipe(
catchError(() => { // an error from the request will be handled here
return of({ error: 'Oops' });
}),
share()
)
#3
如果数据晚于1秒钟到达,则应显示至少1秒钟的指示器(为防止旋转器闪烁,应在以后渲染数据)
首先,我们有一种方法可以打开加载指示器 ,尽管我们目前没有将其关闭 。让我们使用result$
流上的事件作为通知,我们可以隐藏加载指示符。收到结果后,我们可以隐藏指标:
// this we'll use as an off switch:
result$.pipe( mapTo(false) )
因此我们可以merge
进行开/关切换:
const showLoadingIndicator$ = merge(
// ON in 1second
timer(1000).pipe( mapTo(true), takeUntil(result$) ),
// OFF once we receive a result
result$.pipe( mapTo(false) )
)
现在,我们需要切换加载指示器 on 和 off ,尽管我们需要摆脱闪烁的加载指示器并至少显示1秒钟。我猜,最简单的方法是将{em> off 开关的值combineLatest和2秒的timer:
const showLoadingIndicator$ = merge(
// ON in 1second
timer(1000).pipe( mapTo(true), takeUntil(result$) ),
// OFF once we receive a result, yet at least in 2s
combineLatest(result$, timer(2000)).pipe( mapTo(false) )
)
注意::如果结果是在第二秒之前收到的,则此方法可能会在2秒时为我们提供冗余的 off 开关。我们待会再处理。
#4
如果呼叫在1秒钟内失败,则指示器应至少显示1秒钟
我们对#3的解决方案已经具有防闪代码,并且在#2中,我们已经处理了流抛出错误的情况,因此在这里很好。
#5
如果通话时间超过10秒,则应取消通话(并显示错误消息)
为帮助我们取消长时间运行的请求,我们有一个timeout运算符:如果可观察的源在给定时间内不发出值,它将抛出一个错误
result$ = makeARequest().pipe(
timeout(10000), // 10 seconds timeout for the result to come
catchError(() => { // an error from the request or timeout will be handled here
return of({ error: 'Oops' });
}),
share()
)
我们快完成了,只需要一点改进。让我们以一个showLoadingIndicator$
值开始我们的false
流,这表明我们没有在开始时显示加载程序。并使用distinctUntilChanged
来省略冗余的 off 到 off 开关,这些开关由于我们在#3中的方法而可以获得。
总结所有事情,以下是我们已经取得的成就:
const { fromEvent, timer, combineLatest, merge, throwError, of } = rxjs;
const { timeout, share, catchError, mapTo, takeUntil, startWith, distinctUntilChanged, switchMap } = rxjs.operators;
function startLoading(delayTime, shouldError){
console.log('====');
const result$ = makeARequest(delayTime, shouldError).pipe(
timeout(10000), // 10 seconds timeout for the result to come
catchError(() => { // an error from the request or timeout will be handled here
return of({ error: 'Oops' });
}),
share()
);
const showLoadingIndicator$ = merge(
// ON in 1second
timer(1000).pipe( mapTo(true), takeUntil(result$) ),
// OFF once we receive a result, yet at least in 2s
combineLatest(result$, timer(2000)).pipe( mapTo(false) )
)
.pipe(
startWith(false),
distinctUntilChanged()
);
result$.subscribe((result)=>{
if (result.error) { console.log('Error: ', result.error); }
if (result.msg) { console.log('Result: ', result.msg); }
});
showLoadingIndicator$.subscribe(isLoading =>{
console.log(isLoading ? '⏳ loading' : ' free');
});
}
function makeARequest(delayTime, shouldError){
return timer(delayTime).pipe(switchMap(()=>{
return shouldError
? throwError('X')
: of({ msg: 'awesome' });
}))
}
<b>Fine requests</b>
<button
onclick="startLoading(500)"
>500ms</button>
<button
onclick="startLoading(1500)"
>1500ms</button>
<button
onclick="startLoading(3000)"
>3000ms</button>
<button
onclick="startLoading(11000)"
>11000ms</button>
<b>Error requests</b>
<button
onclick="startLoading(500, true)"
>Err 500ms</button>
<button
onclick="startLoading(1500, true)"
>Err 1500ms</button>
<button
onclick="startLoading(3000, true)"
>Err 3000ms</button>
<script src="https://unpkg.com/rxjs@6.5.2/bundles/rxjs.umd.min.js"></script>
希望这会有所帮助
答案 1 :(得分:1)
这是另一个版本。该查询使用timeout
于10s结束查询。并使用throttleTime
防止加载程序闪烁。它也只预订一次查询。它产生一个可观察的对象,该对象将发出showLoader
布尔值,并最终发出查询结果(或错误)。
// returns Observable<{showLoader: boolean, error: Error, result: T}>
function dataWithLoader(query$) {
const timedQuery$ = query$.pipe(
// give up on the query with an error after 10s
timeout(10000),
// convert results into a successful result
map(result => ({result, showLoader: false})),
// convert errors into an error result
catchError(error => ({error, showLoader: false})
);
// return an observable that starts with {showLoader: false}
// then emits {showLoader: true}
// followed by {showLoader: false} when the query finishes
// we use throttleTime() to ensure that is at least a 1s
// gap between emissions. So if the query finishes quickly
// we never see the loader
// and if the query finishes _right after_ the loader shows
// we delay its result until the loader has been
// up for 1 second
return of({showLoader: false}, {showLoader: true}).pipe(
// include the query result after the showLoader true line
concat(timedQuery$),
// throttle emissions so that we do not get loader appearing
// if data arrives within 1 second
throttleTime(1000, asyncScheduler, {leading:true, trailing: true}),
// this hack keeps loader up at least 1 second if data arrives
// right after loader goes up
concatMap(x => x.showLoader ? EMPTY.pipe(delay(1000), startWith(x)) : of(x))
);
}
答案 2 :(得分:0)
您可以尝试按照以下方式构建蒸汽。
(假设data$
是您可观察到的数据,它在数据出现而出错时发出,而在失败时发出)
import { timer, merge, of } from 'rxjs';
import { mapTo, map, catchError, takeUntil, delay, switchMap } from 'rxjs/operators'
const startTime = new Date();
merge(
data$.pipe(
takeUntil(timer(10000)),
map((data) => ({ data, showSpinner: false, showError: false })),
catchError(() => of({ data: null, showSpinner: false, showError: true })),
switchMap((result) => {
const timeSinceStart = (new Date).getTime() - startTime.getTime();
return timeSinceStart > 1000 && timeSinceStart < 2000 ? of(result).pipe(delay(2000 - timeSinceStart)) : of(result)
}),
)
timer(1000).pipe(
mapTo({ data: null, showSpinner: true, showError: false }),
takeUntil(data$)
),
timer(10000).pipe(
mapTo({ data: null, showSpinner: false, showError: true }),
takeUntil(data$)
)
).subscribe(({ data, showSpinner, showError }) => {
// assign the values to relevant properties so the template can
// show either data, spinner, or error
});
答案 3 :(得分:0)
编辑:我的旧答案有错误...
我现在构建了一个可运行的管道运算符,但是它很大。也许有人可以提供一些改进:)
preDelay
是显示加载指示符之前的毫秒数。
postDelay
是加载指示器至少可见的毫秒数。
const prePostDelay = (preDelay: number, postDelay: number) => (source: Observable<boolean>) => {
let isLoading = false; // is some loading in progress?
let showingSince = 0; // when did the loading start?
return source.pipe(
flatMap(loading => {
if (loading) { // if we receive loading = true
if (!isLoading) { // and loading isn't already running
isLoading = true; // then set isLoading = true
return timer(preDelay).pipe( // and delay the response
flatMap(_ => {
if (isLoading) { // when delay is over, check if we're still loading
if (showingSince === 0) { // and the loading indicator isn't visible yet
showingSince = Date.now(); // then set showingSince
return of(true); // and return true
}
}
return EMPTY; // otherwise do nothing
})
);
}
} else { // if we receive loading = false
if (isLoading) {
isLoading = false;
// calculate remaining time for postDelay
const left = postDelay - Date.now() + showingSince;
if (left > 0) { // if we need to run postDelay
return timer(left).pipe( // then delay the repsonse
flatMap(_ => {
if (!isLoading) { // when delay is over, check if no other loading progress started in the meantime
showingSince = 0;
return of(false);
}
return EMPTY;
})
);
} else { // if there's no postDelay needed
showingSince = 0;
return of(false);
}
}
}
return EMPTY; // else do nothing
})
);
}
用法:
loadingAction1 = timer(1000, 2000).pipe(
take(2),
map(val => val % 2 === 0)
);
loadingAction2 = timer(2000, 2000).pipe(
take(2),
map(val => val % 2 === 0)
);
loadingCount = merge([loadingAction1, loadingAction2]).pipe(
scan((acc, curr) => acc + (curr ? 1 : -1), 0)
);
loading = loadingCount.pipe(
map(val => val !== 0)
);
loading.pipe(
prePostDelay(500, 1000)
).subscribe(val => console.log("show loading indicator", val));