Прежде всего, это хороший вопрос, Лукас!
Предисловие: хотя есть и другие способы добиться того, о чем вы спрашиваете, я просто хотел сделать свой ответ более подробным шагомпошаговое руководство
Для удобства давайте представим, что у нас есть метод, который выполняет запрос и возвращает нам Observable строковых сообщений:
const makeARequest: () => Observable<{ msg: string }>;
Теперь мы можем объявитьнаши наблюдаемые, которые будут содержать результат:
// 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, который сделает два запроса, а это не то, что нам нужно.Чтобы решить эту проблему, мы можем просто поделиться этой наблюдаемой среди подписчиков:
result$ = makeARequest().pipe(
catchError(() => { // an error from the request will be handled here
return of({ error: 'Oops' });
}),
share()
)
# 3
Если данные поступают позже, чем через 1 секунду, показательдолжно отображаться как минимум в течение 1 секунды (чтобы предотвратить мигание счетчика, данные должны отображаться впоследствии)
Сначала у нас есть способ включить индикатор загрузки на , хотяв настоящее время мы не отключаем 1057 *.Давайте используем событие в потоке 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) )
)
Теперь у нас есть переключение индикатора загрузки включен и выключен , хотя нам нужно избавиться от мигания индикатора загрузки и показать его хотя бы в течение 1 секунды.Я предполагаю, что самый простой способ - это ОбъединитьПоследние значения переключателя off и таймера 2 секунд :
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) )
)
ПРИМЕЧАНИЕ: этот подход может дать нам избыточный переключатель off на 2 с, если результат был получен до 2-й секунды.Мы разберемся с этим позже.
# 4
Если вызов не состоялся позднее, чем через 1 секунду, индикатор должен отображаться как минимум в течение 1 секунды
Наше решение для # 3 уже имеет анти-флэш-код, а в # 2 мы обработали случай, когда поток выдает ошибку, так что у нас все хорошо.
# 5
Если вызов занимает более 10 секунд, вызов следует отменить (и отобразить сообщение об ошибке)
Чтобы помочь нам отменить длительные запросы, у нас есть тайм-аут оператор: он выдаст ошибку, если наблюдаемая исходная точка не выдаст значение в течение заданного времени
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 to 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>
Надеюсь, это поможет