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

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

CoreModule:

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());

LoaderService:

@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 не содержит необходимой информации. Я хотел бы избежать в конечном итоге поведения отказов.

Ответы

Ответ 1

Я думаю, что эта статья может помочь вам https://nezhar.com/blog/create-a-loading-screen-for-angular-apps/

в основном вы должны реализовать различные перехватчики для загрузки компонентов и для запросов http

Ответ 2

Реализует HttpInterceptor - Вы можете добавить глобальный загрузчик.

Http Interceptor Reff:

Ответ 3

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

counter = 0;

router.events.pipe(
  filter(x => x instanceof NavigationStart),
  debounceTime(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();
});

Ответ 4

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

    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;
      })
    );
  }
}

и сервис загрузчика просто:

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

  constructor() {
  }

  setLoading(val: boolean): void {
    if (!val) {
      this.timeout = setTimeout(() => {
        this.loadingRequest.next(val);
      }, 300);
    } else {
      clearTimeout(this.timeout);
      this.loadingRequest.next(val);
    }
  }
}

Ответ 5

В этой сущности есть пример моего загрузчика.

https://gist.github.com/borjapazr/88e06f2e9159778ee7991cef14a130f2

app.component.ts

<custom-loader></custom-loader>

custom-loader.component.html

<div [class.hidden]="!show">
  <div *ngIf="show" class="loader-overlay">
    <div class="loader-bar"></div>
    <div class="loader-spinner"></div>
  </div>
</div>

custom-loader.component.css

.hidden {
  visibility: hidden;
}

.loader-overlay {
  position: fixed;
  background: rgba(0,0,0,0.4);
  bottom: 0;
  height: 100%;
  left: 0;
  right: 0;
  top: 0;
  width: 100%;
  display:block;
  margin: 0 auto;
  overflow: hidden;
  z-index: 9999;
}

.loader-bar {
  height: 4px;
  width: 100%;
  position: relative;
  overflow: hidden;
  background-color: #000000;

  &:before {
    display: block;
    position: absolute;
    content: "";
    left: -200px;
    width: 200px;
    height: 4px;
    background-color: #ffffff;
    animation: loading 2s linear infinite;
  }
}

.loader-spinner {
  position: fixed;
  left: 0px;
  top: 0px;
  width: 100%;
  height: 100%;
  z-index: 9999;
  background: url('/assets/img/loader/spinner.gif') 50% 50% no-repeat;
  opacity: 0.7;
  background-size: 75px 75px;
}

@keyframes loading {
  from {left: -200px; width: 30%;}
  50% {width: 30%;}
  70% {width: 70%;}
  80% {left: 50%;}
  95% {left: 120%;}
  to {left: 100%;}
}

custom-loader.component.ts

@Component({
  selector: 'custom-loader',
  templateUrl: './custom-loader.component.html',
  styleUrls: ['./custom-loader.component.scss']
})
export class CustomLoaderComponent implements OnInit, OnDestroy {

  public destroy$: Subject<boolean> = new Subject<boolean>();

  public show = false;

  constructor(private loaderService: CustomLoaderService) { }

  ngOnInit() {
    this.loaderService.getLoaderState()
    .pipe(
      takeUntil(this.destroy$)
    )
    .subscribe(state => {
      this.show = state.show;
      this.setScrollBarVisibility(!this.show);
    });
  }

  private setScrollBarVisibility(visible: boolean) {
    document.body.style.overflow = visible ? 'auto' : 'hidden';
  }

  ngOnDestroy() {
    this.destroy$.next(true);
    this.destroy$.unsubscribe();
  }

}

custom-loader.service.ts

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

  private loaderSubject = new Subject<ILoaderState>();
  private loaderState = this.loaderSubject.asObservable();
  private timeout: any;

  constructor() { }

  public getLoaderState(): Observable<ILoaderState> {
    return this.loaderState;
  }

  public show(delay?: number) {
    if (delay) {
      this.timeout = setTimeout(() => this.loaderSubject.next(<ILoaderState>{ show: true }), delay);
    } else {
      this.loaderSubject.next(<ILoaderState>{ show: true });
    }
  }

  public hide() {
    if (this.timeout) {
      clearTimeout(this.timeout);
    }
    this.loaderSubject.next(<ILoaderState>{ show: false });
  }

}