Как реализовать глобальный загрузчик в Angular

У меня есть глобальный загрузчик, который реализован следующим образом:

Основной модуль:

router.events.pipe(
  filter(x => x instanceof NavigationStart)
).subscribe(() => loaderService.show());

router.events.pipe(
  filter(x => x instanceof NavigationEnd || x instanceof NavigationCancel || x instanceof NavigationError)
).subscribe(() => loaderService.hide());

ЗагрузчикСервис:

@Injectable({
    providedIn: 'root'
})
export class LoaderService {

    overlayRef: OverlayRef;
    componentFactory: ComponentFactory<LoaderComponent>;
    componentPortal: ComponentPortal<LoaderComponent>;
    componentRef: ComponentRef<LoaderComponent>;

    constructor(
        private overlay: Overlay,
        private componentFactoryResolver: ComponentFactoryResolver
    ) {
        this.overlayRef = this.overlay.create(
            {
                hasBackdrop: true,
                positionStrategy: this.overlay.position().global().centerHorizontally().centerVertically()
            }
        );

        this.componentFactory = this.componentFactoryResolver.resolveComponentFactory(LoaderComponent);

        this.componentPortal = new ComponentPortal(this.componentFactory.componentType);
    }

    show(message?: string) {
        this.componentRef = this.overlayRef.attach<LoaderComponent>(this.componentPortal);
        this.componentRef.instance.message = message;
    }

    hide() {
        this.overlayRef.detach();
    }
}

При работе с Angular 7.0.2 поведение (которое я хотел):

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

Я обновился до Angular 7.2, теперь поведение такое:

  • Показывать загрузчик при разрешении данных, прикрепленных к маршруту, и при загрузке отложенного модуля
  • Показывать наложение без LoaderComponent при переходе к маршруту без какого-либо распознавателя

Я добавил несколько журналов событий NavigationStart и NavigationEnd и обнаружил, что NavigationEnd срабатывает сразу после NavigationStart (что нормально), а Overlay исчезает примерно через 0,5 с после этого.

Я прочитал CHANGELOG.md, но не нашел ничего, что могло бы объяснить эту проблему. Любая идея приветствуется.

Изменить:

После дальнейших исследований я восстановил предыдущее поведение, установив package.json следующим образом:

"@angular/cdk": "~7.0.0",
"@angular/material": "~7.0.0",

вместо этого:

"@angular/cdk": "~7.2.0",
"@angular/material": "~7.2.0",

Я обнаружил ошибочный коммит, который был выпущен в версии 7.1.0, и разместил свою проблему на соответствующем Проблема с GitHub. Он исправляет анимацию затухания Overlay.

Каков совместимый с v7.1+ способ получить желаемое поведение? По моему мнению, лучше всего было бы: показывать загрузчик только тогда, когда это необходимо, но NavigationStart не содержит необходимой информации. Я хотел бы избежать какого-либо поведения debounce.


person Guerric P    schedule 10.01.2019    source источник
comment
возможно ли, что loaderService.hide() выполняется без триггера?   -  person David    schedule 15.01.2019
comment
Вы спрашиваете, звонят ли они из другого места?   -  person Guerric P    schedule 15.01.2019
comment
Это может быть вариант, который я никогда не рассматривал, но я имел в виду, что его можно просто выполнить без какого-либо триггера и что используемая вами нотация просто интерпретируется как код для выполнения, а не как структура ООП с функциями.   -  person David    schedule 15.01.2019
comment
@ Дэвид, извини, я действительно не понимаю твоей точки зрения   -  person Guerric P    schedule 15.01.2019
comment
Не обращайте внимания, мое предположение, вероятно, в любом случае неверно. Попробуйте отладить, почему срабатывает LoaderService::hide() или исчезает ли оверлей без участия LoaderService::hide().   -  person David    schedule 15.01.2019
comment
Я обнаружил ошибочный запрос на вытягивание в Angular CDK: github.com/angular/material2/pull/10145   -  person Guerric P    schedule 16.01.2019
comment
Эй, @YoukouleleY, хороший детектив. Я только что добавил +1 к вашему комментарию к этому запросу на вытягивание. Но для вас может быть быстрее получить решение, представив проблему и сославшись на этот запрос на вытягивание. Не уверен, насколько команда Angular уделяет внимание комментариям к закрытым PR, и, как правило, свежие проблемы предпочтительнее для сортировки и т. д. Просто мои 0,02 доллара.   -  person Dean    schedule 17.01.2019
comment
@Dean, поправьте меня, если я ошибаюсь, но проблемы GitHub предназначены для запросов на изменение. В моем случае я хотел бы вернуть то, что было сделано, но я полагаю, что это исправление - то, что нужно большинству людей.   -  person Guerric P    schedule 17.01.2019
comment
Хорошая точка зрения. Думаю, я не уверен, что вы бы классифицировали это как ошибку как таковую. В их руководстве по содействию есть положение для отправки сообщения об ошибке, и оно похоже, что это действительно может быть ошибка.   -  person Dean    schedule 17.01.2019
comment
@Dean, спасибо за понимание. Рассмотрю возможность отправки вопроса, если SO не поможет   -  person Guerric P    schedule 17.01.2019


Ответы (4)


Вот к чему я пришел после того, как понял, что delay был хорошим решением с точки зрения UX, потому что он позволял показывать загрузчику только тогда, когда время загрузки стоит отображать загрузчика.

Мне не нравится это решение, потому что оно подразумевает совместное использование состояния между двумя Observables, когда Observables — это чистые каналы, а не побочные эффекты и общие состояния.

counter = 0;

router.events.pipe(
  filter(x => x instanceof NavigationStart),
  delay(200),
).subscribe(() => {
  /*
  If this condition is true, then the event corresponding to the end of this NavigationStart
  has not passed yet so we show the loader
  */
  if (this.counter === 0) {
    loaderService.show();
  }
  this.counter++;
});

router.events.pipe(
  filter(x => x instanceof NavigationEnd || x instanceof NavigationCancel || x instanceof NavigationError)
).subscribe(() => {
  this.counter--;
  loaderService.hide();
});
person Guerric P    schedule 22.05.2019
comment
Я думаю, что лучше создать уникальную наблюдаемую. Использование в html pipe async не обязательно - person Eliseo; 26.03.2021

То, как мы реализуем загрузчик в нашей системе со списком исключений:

export class LoaderInterceptor implements HttpInterceptor {
  requestCount = 0;

  constructor(private loaderService: LoaderService) {
  }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

    if (!(REQUEST_LOADER_EXCEPTIONS.find(r => request.url.includes(r)))) {
      this.loaderService.setLoading(true);
      this.requestCount++;
    }

    return next.handle(request).pipe(
      tap(res => {
        if (res instanceof HttpResponse) {
          if (!(REQUEST_LOADER_EXCEPTIONS.find(r => request.url.includes(r)))) {
            this.requestCount--;
          }
          if (this.requestCount <= 0) {
            this.loaderService.setLoading(false);
          }
        }
      }),
      catchError(err => {
        this.loaderService.setLoading(false);
        this.requestCount = 0;
        throw err;
      })
    );
  }
}

и служба загрузчика просто (задержка 300 мс не позволяет загрузчику просто мигать на экране, когда ответ быстрый):

export class LoaderService {
  loadingRequest = new BehaviorSubject(false);
  private timeout: any;

  setLoading(val: boolean): void {
    if (!val) {
      this.timeout = setTimeout(() => {
        this.loadingRequest.next(val);
      }, 300);
    } else {
      clearTimeout(this.timeout);
      this.loadingRequest.next(val);
    }
  }
}
person hardfi    schedule 28.05.2019
comment
Это работает только для http-запросов, а не для ленивых модулей. - person Guerric P; 28.05.2019

Пытаясь улучшить идею @Guerric P, я полагаю, если бы вы определили наблюдаемую, например:

  loading$ = this.router.events.pipe(
    filter(
      x =>
        x instanceof NavigationStart ||
        x instanceof NavigationEnd ||
        x instanceof NavigationCancel ||
        x instanceof NavigationError
    ),
    map(x => (x instanceof NavigationStart ? true : false)),
    debounceTime(200),
    tap(x => console.log(x))
  );

И у тебя есть

<div *ngIf="loading$|async">Loadding....</div>

Вы должны увидеть загрузку... когда начинаете навигацию и не видите, значит все загружено.

Это может быть в компоненте, а этот компонент в main-app.component, нет необходимости в службе или Factory в этот стек если вы удалите debounce, вы увидите true,false в консоли ПРИМЕЧАНИЕ. Вы можете удалить оператор tap, это только для проверки

Обновление, заставляющее один и тот же загрузчик работать при загрузке данных из службы.

Представьте, что у нас есть сервис с загрузкой свойства, которое было субъектом.

@Injectable({
  providedIn: 'root',
})
export class DataService {
    loading:Subject<boolean>=new Subject<boolean>();
    ...
}

Мы можем объединить ранее наблюдаемое и это, поэтому наша загрузка $ в app.component становится

  loading$ = merge(this.dataService.loading.pipe(startWith(false)),
    this.router.events.pipe(
    filter(
      x =>
        x instanceof NavigationStart ||
        x instanceof NavigationEnd ||
        x instanceof NavigationCancel ||
        x instanceof NavigationError
    ),
    map(x => (x instanceof NavigationStart ? true : false))
  )).pipe(
    debounceTime(200),
    tap(x => console.log(x))
  );

Итак, мы можем сделать, например.

this.dataService.loading.next(true)
this.dataService.getData(time).subscribe(res=>{
  this.dataService.loading.next(false)
  this.response=res;
})

ПРИМЕЧАНИЕ. Вы можете проверить stackblitz, однокомпонентный не задерживает, поэтому загрузка не отображается, двухкомпонентный, задерживает, потому что CanActivate Guard и потому что вызывает службу в ngOnInit

ПРИМЕЧАНИЕ 2. В этом примере мы вручную вызываем this.dataService.loading.next(true|false). Мы можем улучшить его, создав оператор

person Eliseo    schedule 25.03.2021
comment
В вашем примере должен быть показан загрузчик, который использует оверлей Angular, потому что первоначальная проблема заключается в кратком отображении оверлея, когда он не нужен. - person Guerric P; 31.03.2021
comment
Геррик, я поставил простой div с *ngIf, вы можете использовать любой компонент или div с оверлеем. Искренне, мне не нравится идея использовать счетчик или подписываться на два наблюдаемых, но это личное мнение. Используйте уникальную наблюдаемую и откат, избегайте этого, но, конечно, ваш код тоже хороший подход. - person Eliseo; 31.03.2021
comment
Мне это тоже не нравится, поэтому я прошу еще один ответ с моей наградой. Div с *ngIf не ведет себя как оверлей, потому что оверлей исчезает, начиная с v7, в предыдущих версиях мы всегда могли показывать/скрывать его через очень короткие промежутки времени, он не был виден, но начиная с v7 он есть. - person Guerric P; 31.03.2021
comment
Ваш debounceTime применяется к событиям trueи false, что может привести к пропуску событий, которые, тем не менее, необходимы. - person Guerric P; 31.03.2021
comment
@GuerricP, я обновляю stackblitz. Я использую AuthGuard во втором компоненте. Вы видите загрузку перед изменением второго компонента. Я думаю, что в случае ленивой загрузки должно работать. Я надеюсь, что пример поможет мне объяснить лучше. - person Eliseo; 01.04.2021
comment
Одна проблема с вашим кодом, он скрывает загрузчик с задержкой в ​​200 мс - person Guerric P; 01.04.2021
comment
правда, я попытался заменить debounceTime(200) на debounce(x=>x==true?timer(200):EMPTY) , но во втором компоненте есть мерцание, потому что в ngOnInit есть вызов службы - person Eliseo; 01.04.2021

Последнее обновление моего комментария всегда ждет 200 миллисекунд. Конечно, мы не хотим ждать в конце навигации или в конце наблюдаемого. Таким образом, мы можем заменить debounceTime(200) на что-то вроде debounce(x => x ? timer(200) : EMPTY), но это приведет к тому, что в ngOnInit у нас есть компонент с задержкой поиска, щелчком загрузчика.

Поэтому я решил использовать данные в маршрутизаторе, чтобы указать, какие компоненты имеют ngOnInit.

Представьте себе что-нибудь вроде

const routes: Routes = [
  { path: 'one-component', component: OneComponent },
  { path: 'two-component', component: TwoComponent,
               canActivate:[FoolGuard],data:{initLoader:true}},
    { path: 'three-component', component: ThreeComponent,
               canActivate:[FoolGuard],data:{initLoader:true} },
  ];

Я создаю службу, которая учитывает, когда маршрутизатор имеет данные initLoader, и включаю нулевой оператор

/*Nils' operator: https://nils-mehlhorn.de/posts/indicating-loading-the-right-way-in-angular
*/
export function prepare<T>(callback: () => void): (source: Observable<T>) => Observable<T> {
  return (source: Observable<T>): Observable<T> => defer(() => {
    callback();
    return source;
  });
}
export function indicate<T>(indicator: Subject<boolean>): (source: Observable<T>) => Observable<T> {
  return (source: Observable<T>): Observable<T> => source.pipe(
    prepare(() => indicator.next(true)),
    finalize(() => indicator.next(false))
  )
}

Оператор Нильса работает так, что если у вас есть субъект и наблюдаемое

  myObservable.pipe(indicate(mySubject)).subscribe(res=>..)

отправьте истину субъекту в начале звонка и ложь и конец звонка

Ну, я могу сделать такую ​​услугу, как

/*Service*/
@Injectable({
  providedIn: "root"
})
export class DataService {
  loading: Subject<boolean> = new Subject<boolean>();
  constructor(private router: Router){}

  //I use the Nils' operator
  getData(time: number) {
    return of(new Date()).pipe(delay(time),indicate(this.loading));
  }

  getLoading(): Observable<any> {
    let wait=true;
    return merge(
      this.loading.pipe(map(x=>{
        wait=x
        return x
        })),
      this.router.events
        .pipe(
          filter(
            x =>
              x instanceof NavigationStart ||
              x instanceof ActivationEnd ||
              x instanceof NavigationCancel ||
              x instanceof NavigationError || 
              x instanceof NavigationEnd
          ),
          map(x => {
            if (x instanceof ActivationEnd) {
              wait=x.snapshot.data.wait|| false;
              return true;
            }
            return x instanceof NavigationStart ? true : false;
          })
        ))
        .pipe(
          debounce(x=>wait || x?timer(200):EMPTY),
        )
  }

Это приводит к тому, что задержка происходит, когда StartNavigation или путь имеет в данных ожидание: true. См. в stackblitz этот компонент -three имеет в пути data:{wait:true} (возможно, мы забыли удалить его), но не имеет ngOnInit. Это делает, что у нас есть задержка

ПРИМЕЧАНИЕ: другой сервис может использовать загрузчик, только внедрить dataService и использовать оператор nils.

this.anotherService.getData().pipe(
   indicate(this.dataService.loading)
).subscribe(res=>{....})
person Eliseo    schedule 02.04.2021