Показывать экран загрузки при навигации между маршрутами в Angular 2

Ответы:

196

Текущий маршрутизатор Angular предоставляет события навигации. Вы можете подписаться на них и внести соответствующие изменения в пользовательский интерфейс. Не забывайте учитывать другие события, такие как NavigationCancelи NavigationErrorостанавливать счетчик в случае сбоя переходов маршрутизатора.

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 NgZoneи Rendererвключать / выключать счетчик, который будет обходить обнаружение изменений Angular, когда мы меняем состояние счетчика. Я нашел это, чтобы сделать анимацию более плавной по сравнению с использованием трубки *ngIfили asyncтрубки.

Это похоже на мой предыдущий ответ с некоторыми изменениями:

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>
borislemke
источник
1
Отлично, я ценю ваш подход. Он ударил меня , прежде чем я заменил мой длинный метод с этой привлекательной идеей, чем я должен был вернуться потому что я потерял контроль над router navigationи , it triggering the spinnerа затем не в состоянии остановить его. navigationInterceptorкажется решением, но я не уверен, что что-то еще ждет, чтобы сломаться. async requestsЯ думаю, если смешать с этим, это снова вызовет проблему.
Анкит Сингх
1
Может, это сработало в какой-то момент? Сейчас он не работает с Angular 2.4.8. Страница синхронная. Spinner не отображается до тех пор, пока не отобразится вся страница / родительский компонент, и это в NavigationEnd. Это побеждает точку прядильщика
techguy2000
1
Использование непрозрачности - не лучший выбор. Визуально это нормально, но в Chrome, если загружаемое изображение / значок находится поверх кнопки или текста, вы не можете получить доступ к кнопке. Я изменил его использовать displayлибо noneили inline.
im1dermike
2
Мне нравится такой подход, молодец! Но у меня проблема, и я не понимаю, почему, если я не буду переключать анимацию в NavigationEnd, я вижу загрузку счетчика, но если я переключаюсь на false, маршруты меняются так быстро, что я даже не вижу никаких анимаций :( Я пробовал даже с регулированием сети, чтобы замедлить соединение, но оно по-прежнему остается прежним :( Никакой загрузки. Не могли бы вы дать мне какие-либо предложения по этому поводу, пожалуйста. Я контролирую анимацию, добавляя и удаляя класс в элементе загрузки. Спасибо
d123546
1
Я получил эту ошибку, когда скопировал ваш код 'md-spinner' is not a known element:. Я новичок в Angular. Подскажите, пожалуйста, в чем может быть ошибка?
Ману Чадха
40

ОБНОВЛЕНИЕ: 3 Теперь, когда я обновился до нового маршрутизатора, подход @borislemke не будет работать, если вы используете CanDeactivateохранник. Я унижаюсь до своего старого метода, ie:этот ответ

UPDATE2 : события в маршрутизатор нового маршрутизатора выглядит многообещающим и ответ на @borislemke , кажется, охватывает основной аспект реализации вращателя я havent't тестировал , но я рекомендую его.

ОБНОВЛЕНИЕ1: я написал этот ответ в эпоху Old-Router, когда раньше было только одно событие, route-changedо котором сообщалось через router.subscribe(). Я также почувствовал перегрузку приведенного ниже подхода и попытался сделать это, используя только router.subscribe(), но это привело к обратным результатам, потому что не было возможности обнаружить canceled navigation. Поэтому мне пришлось вернуться к длительному подходу (двойной работе).


Если вы разбираетесь в Angular2, это то, что вам нужно


Boot.ts

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-Component (подпишется на 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();
  }
}
Анкит Сингх
источник
см. это также. Я не знаю, насколько анимация поддерживает angular по умолчанию.
Анкит Сингх
вы можете помочь мне написать услугу для спиннера? Я могу делать анимацию и многое другое в CSS, но было бы неплохо, если бы вы помогли мне с сервисом.
Лукас
см. и эту реализацию , то же самое, но не совсем
Анкит Сингх
1
Я добавил код, а spinner-serviceвам просто нужны другие части, чтобы он работал. И помните, что это дляangular2-rc-1
Анкит Сингх
1
на самом деле, это потрясающе и работает, мой совет для целей тестирования: вы можете отложить остановку счетчика с помощью setTimeout (() => this.spinner.stop (), 5000) в ngOnInit
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-выхода , поэтому с помощью простого селектора css мы можем показать и скрыть загрузку.

Milad
источник
<router-outlet> не позволяет нам передавать значение от компонента к родительскому компоненту, поэтому будет немного сложно скрыть загрузочный div.
Правин Рана
1
Также может быть очень неприятно, если в вашем приложении есть много изменений маршрута, чтобы каждый раз мгновенно видеть счетчик. Лучше использовать RxJs и установить противодействующий таймер, чтобы он появлялся только после небольшой задержки.
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');
                   });

Мне нужно было это скрыть нижний колонтитул моей страницы, который в противном случае появлялся бы вверху страницы. Также, если вам нужен загрузчик только для первой страницы, вы можете использовать это.

Simon_Weaver
источник
0

Вы также можете использовать это существующее решение . Демо здесь . Похоже на полосу загрузки YouTube. Я просто нашел его и добавил в свой проект.

Джонатан Дракон
источник
это помогает с резолверами? Моя проблема в том, что, хотя резолверы возвращают данные, я не могу нигде показать счетчик, потому что фактический целевой компонент ngOninit еще не был вызван !! Моя идея заключалась в том, чтобы показать счетчик в ngOnInit и скрыть его, как только разрешенные данные возвращаются из подписки на маршрут
kuldeep