【问题标题】:How to implement a global loader in Angular如何在 Angular 中实现全局加载器
【发布时间】:2019-01-10 19:34:23
【问题描述】:

我有一个全局加载器,它是这样实现的:

核心模块:

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

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

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

【问题讨论】:

  • loaderService.hide() 是否有可能在没有触发器的情况下执行?
  • 你问的是不是从别处调用的?
  • 这可能是我从未考虑过的一个选项,但我的意思是,它可以在没有任何触发器的情况下执行,并且您使用的符号仅被解释为要执行的代码,而不是带有函数的 OOP 结构。
  • @David 对不起,我真的不明白你的意思
  • 我在 Angular CDK 中发现了错误的拉取请求:github.com/angular/material2/pull/10145

标签: javascript angular rxjs angular-cdk


【解决方案1】:

在意识到delay 在用户体验方面是一个很好的解决方案之后,我最终得到了这个结果,因为它允许加载器仅在加载时间值得显示加载器时才显示。

我不喜欢这个解决方案,因为它意味着在两个 Observable 之间共享一个状态,而 Observable 是关于纯管道而不是副作用和共享状态。

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

【讨论】:

  • 我认为最好创建一个独特的 observable。不需要在 html 管道中使用异步订阅
【解决方案2】:

我们在系统中使用异常列表实现加载器的方式:

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

并且加载器服务只是(300ms延迟防止加载器在响应快速时仅在屏幕上闪烁):

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

【讨论】:

  • 这仅适用于 http 请求,不适用于惰性模块
【解决方案3】:

尝试改进@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)”。我们可以通过创建运算符来改进它

【讨论】:

  • 您的示例应该显示一个使用 Angular 叠加层的加载器,因为最初的问题是叠加层在不需要时会短暂显示。
  • Guerric,我放了一个带有 *ngIf 的简单 div,您可以使用任何带有叠加层的组件或 div。老实说,我不喜欢使用“计数器”或订阅两个 observables 的想法,但这是个人意见。使用独特的 observable 和 debounce,避免它,但当然,你的代码也是一个很好的方法。
  • 我也不喜欢它,这就是为什么我用我的赏金请求另一个答案。带有 *ngIf 的 div 不像叠加层,因为叠加层从 v7 开始逐渐淡出,在以前的版本中,我们总是可以在很短的时间间隔内显示/隐藏它,它是不可见的,但从 v7 开始
  • 您的 debounceTime 适用于 truefalse 事件,这可能会导致跳过所需的事件
  • @GuerricP,我更新了 stackblitz。我在“第二个组件”中使用了 AuthGuard。在更改第二个组件之前,您会看到“加载”。我认为在延迟加载的情况下应该可以工作。我希望这个例子能帮助我更好地解释。
【解决方案4】:

我的评论的最后一次更新总是等待 200 毫秒。当然,我们不想在 Navigation 结束或 observable 结束时等待。所以我们可以用debounce(x =&gt; x ? timer(200) : EMPTY) 替换debounceTime(200),但这使得我们在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))
  )
}

如果你有一个 Subject 和一个 observable,Nils 的操作符就会起作用

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

在通话开始时向主题发送 true,在通话结束时发送 false 和结束

好吧,我可以提供类似的服务

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

注意:其他服务可以使用加载器,只注入“dataService”并使用 nils 操作符

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

【讨论】:

    猜你喜欢
    • 2021-10-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-03-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多