如何在Angular 7.1中实现全局加载程序

时间:2019-01-10 19:34:24

标签: angular angular7 angular-cdk

我有一个像这样实现的全局加载器:

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的叠加层

我在NavigationStartNavigationEnd事件上添加了一些日志,发现NavigationEnd之后NavigationStart立即被触发(这是正常现象),而覆盖消失约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 issue上。它修复了Overlay的淡出动画。

获得所需行为的符合v7.1 +的方式是什么? 根据我的看法,最好的方法是:仅在必要时显示加载程序,但是NavigationStart不包含所需的信息。 我想避免最终出现一些反跳行为。

4 个答案:

答案 0 :(得分:0)

在意识到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();
});

答案 1 :(得分:0)

我们在带有异常列表的系统中实现加载程序的方式:

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

答案 2 :(得分:0)

尝试改进@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 中,它不需要服务或工厂 在 this stackblitz 中,如果您删除去抖动,您将在控制台中看到 true、false 注意:您可以删除“tap”运算符,它仅用于检查

更新,在我们从服务加载数据时使用相同的加载器。

假设我们有一个服务,其属性“正在加载”是一个主题

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

我们可以将before observable和this合并,这样我们在app.component中的loading$就变成了

  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)”。我们可以改进它创建一个运算符

答案 3 :(得分:0)

我评论的最后更新总是等待 200 毫秒。当然,我们不想在 Navigation 或 observable 结束时等待。所以我们可以通过一些像 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

/*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))
  )
}

Nils 操作符的工作原理是,如果你有一个 Subject 和一个 observable

  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 或“路径”中有数据“wait:true”时。在 stackblitz 中看到“component-three”在路径 data:{wait:true} 中有(可能我们忘记删除它)但没有 ngOnInit。这使得我们有延迟

注意:另一个服务可以使用加载器,只需注入“dataService”并使用 nils 操作符

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