在Angular 2中的路线之间导航时显示加载屏幕


Answers:


196

当前的Angular Router提供导航事件。您可以订阅这些内容并相应地更改UI。请记住要计入其他事件,例如,NavigationCancelNavigationError在路由器转换失败时停止旋转器。

app.component.ts-您的根组件

...
import {
  Router,
  // import as RouterEvent to avoid confusion with the DOM Event
  Event as RouterEvent,
  NavigationStart,
  NavigationEnd,
  NavigationCancel,
  NavigationError
} from '@angular/router'

@Component({})
export class AppComponent {

  // Sets initial value to true to show loading spinner on first load
  loading = true

  constructor(private router: Router) {
    this.router.events.subscribe((e : RouterEvent) => {
       this.navigationInterceptor(e);
     })
  }

  // Shows and hides the loading spinner during RouterEvent changes
  navigationInterceptor(event: RouterEvent): void {
    if (event instanceof NavigationStart) {
      this.loading = true
    }
    if (event instanceof NavigationEnd) {
      this.loading = false
    }

    // Set loading state to false in both of the below events to hide the spinner in case a request fails
    if (event instanceof NavigationCancel) {
      this.loading = false
    }
    if (event instanceof NavigationError) {
      this.loading = false
    }
  }
}

app.component.html-您的根视图

<div class="loading-overlay" *ngIf="loading">
    <!-- show something fancy here, here with Angular 2 Material's loading bar or circle -->
    <md-progress-bar mode="indeterminate"></md-progress-bar>
</div>

提高性能的答案:如果您关心性能,有更好的方法,则实现起来会有些繁琐,但是提高性能值得进行额外的工作。代替使用*ngIf有条件地显示微调器,我们可以利用Angular的功能NgZoneRenderer打开/关闭微调器,这将在我们更改微调器的状态时绕过Angular的更改检测。与使用*ngIfasync管道相比,我发现这可以使动画更流畅。

这与我以前的答案有些相似,但有一些调整:

app.component.ts-您的根组件

...
import {
  Router,
  // import as RouterEvent to avoid confusion with the DOM Event
  Event as RouterEvent,
  NavigationStart,
  NavigationEnd,
  NavigationCancel,
  NavigationError
} from '@angular/router'
import {NgZone, Renderer, ElementRef, ViewChild} from '@angular/core'


@Component({})
export class AppComponent {

  // Instead of holding a boolean value for whether the spinner
  // should show or not, we store a reference to the spinner element,
  // see template snippet below this script
  @ViewChild('spinnerElement')
  spinnerElement: ElementRef

  constructor(private router: Router,
              private ngZone: NgZone,
              private renderer: Renderer) {
    router.events.subscribe(this._navigationInterceptor)
  }

  // Shows and hides the loading spinner during RouterEvent changes
  private _navigationInterceptor(event: RouterEvent): void {
    if (event instanceof NavigationStart) {
      // We wanna run this function outside of Angular's zone to
      // bypass change detection
      this.ngZone.runOutsideAngular(() => {
        // For simplicity we are going to turn opacity on / off
        // you could add/remove a class for more advanced styling
        // and enter/leave animation of the spinner
        this.renderer.setElementStyle(
          this.spinnerElement.nativeElement,
          'opacity',
          '1'
        )
      })
    }
    if (event instanceof NavigationEnd) {
      this._hideSpinner()
    }
    // Set loading state to false in both of the below events to
    // hide the spinner in case a request fails
    if (event instanceof NavigationCancel) {
      this._hideSpinner()
    }
    if (event instanceof NavigationError) {
      this._hideSpinner()
    }
  }

  private _hideSpinner(): void {
    // We wanna run this function outside of Angular's zone to
    // bypass change detection,
    this.ngZone.runOutsideAngular(() => {
      // For simplicity we are going to turn opacity on / off
      // you could add/remove a class for more advanced styling
      // and enter/leave animation of the spinner
      this.renderer.setElementStyle(
        this.spinnerElement.nativeElement,
        'opacity',
        '0'
      )
    })
  }
}

app.component.html-您的根视图

<div class="loading-overlay" #spinnerElement style="opacity: 0;">
    <!-- md-spinner is short for <md-progress-circle mode="indeterminate"></md-progress-circle> -->
    <md-spinner></md-spinner>
</div>

1
太好了,谢谢您的做法。它之前一直打我,我取代了我漫长的方法与此有吸引力的想法,比我早恢复监守我失去了控制权router navigation,并it triggering the spinner再没有能够阻止它。navigationInterceptor似乎是一种解决方案,但我不确定是否有其他问题在等待打破。async requests我认为,如果与之混用,将会再次引入该问题。
Ankit Singh

1
也许这在某些时候可行?Angular 2.4.8现在不起作用。页面是同步的。直到整个页面/父组件都呈现,并且在NavigationEnd处才呈现Spinner。那挫败了微调器的观点
techguy2000

1
使用不透明度不是一个好选择。外观上很好,但是在Chrome中,如果加载的图像/图标位于按钮或文本的顶部,则无法访问该按钮。我改变了它使用display两种noneinline
im1dermike'5

2
我喜欢这种方法,干得好!但是我有一个问题,我不明白为什么,如果我不会在NavigationEnd上切换动画,我会看到微调框加载,但是如果我切换为false,路线会变化得如此之快,以至于我什至看不到任何动画:(甚至网络throteling减慢连接,但它仍然在所有相同:(没有加载你能给我什么建议,请我控制加在负载元素移除类动画由于此上。
d123546

1
复制您的代码时出现此错误'md-spinner' is not a known element:。我对Angular很陌生。你能告诉我可能是什么错误吗?
Manu Chadha

39

更新:3现在我已经升级到新的路由器,如果您使用防护,@ borislemke的方法将不起作用CanDeactivate。我正在退化为我的旧方法,ie:这个答案

更新2:在新的路由器看好和路由器事件,答案@borislemke似乎涵盖微调实施的主要方面,我havent't测试,但我推荐它。

UPDATE1:我在的时代写了这个答案Old-Router,当时过去只有一个事件route-changed通过router.subscribe()。我也感到下面的方法过于繁琐router.subscribe(),并尝试仅使用它来完成它,但事与愿违,因为无法检测到canceled navigation。所以我不得不回到冗长的方法(双重工作)。


如果您知道如何在Angular2中解决问题,这就是您所需要的


引导程序

import {bootstrap} from '@angular/platform-browser-dynamic';
import {MyApp} from 'path/to/MyApp-Component';
import { SpinnerService} from 'path/to/spinner-service';

bootstrap(MyApp, [SpinnerService]);

根组件-(MyApp)

import { Component } from '@angular/core';
import { SpinnerComponent} from 'path/to/spinner-component';
@Component({
  selector: 'my-app',
  directives: [SpinnerComponent],
  template: `
     <spinner-component></spinner-component>
     <router-outlet></router-outlet>
   `
})
export class MyApp { }

微调组件(将订阅Spinner-service以相应地更改active的值)

import {Component} from '@angular/core';
import { SpinnerService} from 'path/to/spinner-service';
@Component({
  selector: 'spinner-component',
  'template': '<div *ngIf="active" class="spinner loading"></div>'
})
export class SpinnerComponent {
  public active: boolean;

  public constructor(spinner: SpinnerService) {
    spinner.status.subscribe((status: boolean) => {
      this.active = status;
    });
  }
}

Spinner-Service(引导此服务)

定义微调器组件要订阅的可观察对象以更改更改状态,并具有知道和设置微调器活动/不活动的功能。

import {Injectable} from '@angular/core';
import {Subject} from 'rxjs/Subject';
import 'rxjs/add/operator/share';

@Injectable()
export class SpinnerService {
  public status: Subject<boolean> = new Subject();
  private _active: boolean = false;

  public get active(): boolean {
    return this._active;
  }

  public set active(v: boolean) {
    this._active = v;
    this.status.next(v);
  }

  public start(): void {
    this.active = true;
  }

  public stop(): void {
    this.active = false;
  }
}

所有其他路线的组成部分

(样品):

import { Component} from '@angular/core';
import { SpinnerService} from 'path/to/spinner-service';
@Component({
   template: `<div *ngIf="!spinner.active" id="container">Nothing is Loading Now</div>`
})
export class SampleComponent {

  constructor(public spinner: SpinnerService){} 

  ngOnInit(){
    this.spinner.stop(); // or do it on some other event eg: when xmlhttp request completes loading data for the component
  }

  ngOnDestroy(){
    this.spinner.start();
  }
}

看到这个。我不知道默认情况下多少动画会进行角度支持。
安吉·辛格

您能帮我为微调器编写服务吗?我可以自己在CSS中制作动画,但如果您可以帮助我的话,那会很好。
卢卡斯

也可以看到这种实现,但不完全相同
Ankit Singh

1
spinner-service现在添加了一些代码,您只需要其他部分即可使其工作。请记住,这是为了angular2-rc-1
Ankit Singh

1
实际上,这很棒而且有效,出于测试目的,我的提示是可以使用ngOnInit中的setTimeout(()=> this.spinner.stop(),5000)延迟微调器的停止操作
Jhonatas Kleinkauff

10

为什么不只使用简单的CSS:

<router-outlet></router-outlet>
<div class="loading"></div>

并按照您的风格:

div.loading{
    height: 100px;
    background-color: red;
    display: none;
}
router-outlet + div.loading{
    display: block;
}

甚至我们可以为第一个答案这样做:

<router-outlet></router-outlet>
<spinner-component></spinner-component>

然后只是

spinner-component{
   display:none;
}
router-outlet + spinner-component{
    display: block;
}

这里的窍门是,新的路由和组件将始终出现在router-outlet之后,因此使用简单的CSS选择器就可以显示和隐藏负载。


<router-outlet>不允许我们将值从组件传递到父组件,因此隐藏加载div会有些复杂。
Praveen Rana's

1
如果您的应用程序进行了大量更改以立即每次看到微调框,也可能会非常烦人。最好使用RxJ并设置一个反跳计时器,以便仅在短暂延迟后才显示。
Simon_Weaver

2

如果只有第一条路线需要特殊的逻辑,则可以执行以下操作:

AppComponent

    loaded = false;

    constructor(private router: Router....) {
       router.events.pipe(filter(e => e instanceof NavigationEnd), take(1))
                    .subscribe((e) => {
                       this.loaded = true;
                       alert('loaded - this fires only once');
                   });

我需要隐藏我的页面页脚,否则该页脚将显示在页面顶部。另外,如果您只想为第一页加载程序,则可以使用它。


0

您也可以使用此现有解决方案。演示在这里。看起来就像youtube加载栏。我刚刚找到它并将其添加到我自己的项目中。


对解析器有帮助吗?我的问题是,尽管解析器带回了数据,但我无法在任何地方显示微调器,因为尚未调用实际的目标组件ngOninit!我的想法是在ngOnInit中显示微调框,并在从路由订阅返回解析的数据后将其隐藏起来
kuldeep
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.