Индикация загрузки с задержкой и мерцанием в RxJS - PullRequest
2 голосов
/ 29 мая 2019

Я хочу реализовать индикацию загрузки с использованием RxJS (версия 6).Индикатор загрузки (счетчик) будет отображаться в компоненте до завершения асинхронного вызова данных.У меня есть некоторые правила для реализации (правильность этих правил может быть другим вопросом, возможно, оставить комментарий):

  • Если данные прибывают успешно раньше, чем через 1 секунду, индикатор не должен отображаться (иданные должны обрабатываться в обычном режиме)
  • Если вызов завершается неудачей раньше, чем через 1 секунду, индикатор не должен отображаться (и должно отображаться сообщение об ошибке)
  • Если данные поступают позже, чем через 1 секундуиндикатор должен отображаться в течение не менее 1 секунды (чтобы не допустить мигания счетчика, данные должны отображаться впоследствии)
  • Если вызов завершится не позднее, чем через 1 секунду, индикатор должен отображаться в течение не менее 1 секунды
  • Если вызов занимает более 10 секунд, вызов следует отменить (и отобразить сообщение об ошибке)

Я реализую это в проекте Angular, но я считаю, что это не Angular

Я нашел несколько частей этой головоломки, но мне нужна помощь, чтобы собрать их вместе.

В этом ответе SO есть реализация оператора, который задерживает показ индикатора загрузки.

Хорошая, но не полная реализация Angular описана в этой статье .

Показ индикатора загрузки в течение минимального промежутка времени описан в этой статье Medium .

Ответы [ 4 ]

1 голос
/ 29 мая 2019

Прежде всего, это хороший вопрос, Лукас!

Предисловие: хотя есть и другие способы добиться того, о чем вы спрашиваете, я просто хотел сделать свой ответ более подробным шагомпошаговое руководство

Для удобства давайте представим, что у нас есть метод, который выполняет запрос и возвращает нам 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>

Надеюсь, это поможет

1 голос
/ 29 мая 2019

Вот еще одна версия. Этот использует timeout, чтобы завершить запрос в 10 секунд. И использует 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))
   );
}
0 голосов
/ 09 июня 2019

РЕДАКТИРОВАТЬ: В моем старом ответе были ошибки ...

Теперь я создал конвейерный оператор, который работает, но он огромен.Может быть, кто-то может предложить некоторые улучшения :)

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));
0 голосов
/ 29 мая 2019

Вы можете попытаться создать пар следующим образом.

(Предполагая, что 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

});



Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...