Direkt zum Inhalt wechseln
DSC03033

Angular

Routenbasierte Daten mit Resolver laden

Björn Möllers

Link kopieren

Link kopiert

Resolver – einfache Daten laden

Der Resolver lädt Daten beim Öffnen einer Route. Es können die Routen-Parameter verwenden werden. Für die Route \product\42 kann ein Resolver die Produktdetails mit ID 42 laden. Der Resolver stellt sicher, dass alle dynamischen Daten beim Initialisieren der Komponente vorhanden sind.

Resolver haben wir im Happy Angular Podcast in der Folge 15 betrachtet. Hör dir Ihn an. Die Vorteile und Nachteile kurz zusammengefasst:

Vorteile:

  • Gute Strukturierung des Programmcodes
  • Aufteilung auf mehrere Resolver möglich
  • Wiederverwendbarkeit des Resolvers für andere Komponente

Nachteile:

  • Latenz, da alle Inhalte werden vor Initialisieren der Komponente geladen (Partieller Aufbau einer Liste ist nicht möglich)
  • ggf. werden Inhalte geladen, die nicht im Sichtbereich liegen
  • Resolver cachen die Daten nicht, wenn die Route erneut geöffnet wird (Navigation zu einer anderen Route und zurück).
  • Fehler Handling

Wir schauen uns auch an, wie die Nachteile überwunden werden können.
Aber zunächst betrachten wir die grundlegende Nutzung eines Resolvers.

Grundlegende Nutzung

Schauen wir uns beispielhaft einen Resolver zum Laden einer Liste an. Ein Resolver ist ein Service, der an einer Route gebunden wird. Deshalb schauen wir uns als Erstes die Routing-Konfiguration an. Durch resolve geben wir an, unter welcher Property der Resolver später erreichbar sein soll. In diesem Beispiel ist der ListResolver unter der Property list verfügbar.

const routes: Routes = [
  {
    path: 'overview', component: OverviewComponent, resolve: {
      list: ListResolver
    }
  },
  {path: '', pathMatch: 'full', redirectTo: 'overview'},
  {path: '**', component: PageNotFoundComponent}
];

@NgModule({
  imports: [
    HttpClientModule,
    RouterModule.forRoot(routes)
  ],
  exports: [RouterModule],
  providers: [
    ListResolver,
    ItemResolver,
    DelayResolver,
    PartialResolver,
    MatrjoschkaResolver
  ]
})
export class AppRoutingModule {
}

Der Resolver selbst benötigt das Interface Resolve und dieses Interface ist – über Generics – typisiert. Der untere Programmcode zeigt uns, dass der Resolver CountryListItem[] zurückliefet. Darüber hinaus, die Methode resolve lädt die Daten von einem Backend und transformiert das Ergebnis in die gewünschte Struktur. Eine einfache Art der Fehlerbehandlung ist in dem Beispiel zu finden: Sollte ein Fehler auftreten, wird zu einer Fehlerseite weitergeleitet.

@Injectable()
export class ListResolver implements Resolve {

  constructor(private http: HttpClient, private router: Router) {
  }

  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable {
    return this.http.get('https://restcountries.eu/rest/v2/all?fields=alpha3Code;name;population').pipe(
      map((list: Object[]) => list.map(item => CountryListItem.fromJson(item)))
    ).pipe(
      catchError(error => {
        this.router.navigateByUrl('/page-not-found');
        return of(undefined);
      })
    );
  }
}

Für die Verwaltung der Daten in der Angular Anwendung steht eine Entität-Klasse bereit. Die Methode fromJson vereinfacht die Transformation der API-Antwort. Diese hast du im vorherigen Teil kennengelernt.

export class CountryListItem {
  constructor(public id: string,
              public name: string,
              public population: number) {

  }

  static fromJson(obj: Object): CountryListItem {
    return new CountryListItem(obj['alpha3Code'], obj['name'], obj['population']);
  }
}

Für die Darstellung in einer Komponente ist die Abhängigkeit ActivatedRoute notwendig. Wir haben uns für die Sichtbarkeit public entschieden, da wir direkt im Template auf die Daten des Resolvers zugreifen werden.

@Component({
  selector: 'app-overview',
  templateUrl: './overview.component.html',
  styleUrls: ['./overview.component.scss']
})
export class OverviewComponent {

  displayedColumns: string[] = ['name', 'population'];

  constructor(public activatedRoute: ActivatedRoute) {
  }

}

Es stellt sich die Frage: Wie kann ich auf die Daten zugreifen?
Die ActivatedRoute präsentiert uns die Daten als Observable. Ein Observable kann im Template mit der Async-Pipe aufgelöst werden. Wir haben unsere Daten in der Routing-Konfiguration die Daten unter list bereitgestellt, welche wir hier aufrufen.

Zum Darstellen verwenden wir an dieser Stelle Material Design bzw. die Material Design Tabelle.

Die ganze Tabelle sieht so aus:

Nachteile überwinden

Jedoch hat die rudimentäre Nutzung einige Nachteile. Die Route wird erst aufgelöst, wenn alle Daten durch den Resolver geladen wurden und dann erst wird die Komponente dem Benutzer angezeigt. Diese Zeitverzögerung ist störend und auch merkbar bei geringen Latenzen.

Um es noch schlimmer zu machen, ist das beliebte „Stück-für-Stück“-Laden der Daten mit dieser Technik nicht möglich. Es hat denselben Effekt, wie wenn alles auf einmal geladen wurde: Die Komponente wird erst angezeigt, wenn die Liste komplett ist. Bei Resolver werden standardmäßig auch Daten geladen, die nicht Bestandteil des DOMs sind und somit unnötig die initiale Latenz vergrößern.

Und die Liste der Nachteile ist damit noch nicht zu Ende: Die geladen Daten werden nicht gecacht. Nach dem Verlassen der Route und erneuten Öffnen werden die Daten erneut geladen. Der Benutzer muss wieder auf die Anwendung warten.

Fehlerbehandlung? Nur rudimentäre vorhanden.

In der Summe haben Resolver eine viele Nachteile und sind dennoch so nützlich. Durch eine kleine Änderung können wir viele der Nachteile leicht lösen. Nur für wenige benötigen wir mehr Einsatz. Kennst du die russischen Figuren Matrjoschka? In jeder Figur steckt wieder eine Figur. Und genau nach diesem Konzept bekommt der Resolver ein Upgrade. Statt die Daten zu laden, laden wir einen Observable. Dadurch kann die Komponente sofort dargestellt werden. Die Inhalte werden nachgeladen. Der verwendete DOM-Baum definiert, welche Daten benötigt werden. Außerdem können wir unsere eigene Fehlerbehandlung – wie beim Netzwerkausfällen – entwerfen. Und Caching ist mit dieser kleinen Änderung möglich. Schauen wir uns ein Beispiel an.

Matrjoschka Resolver

In diesem Szenario gibt es keine heile Welt: Daten anfragen, laden und fertig – nicht dieses Mal! Es werden 20 Datensätze geladen – jeder mit einer Verzögerung von 50 ms. Und der letzte Datensatz verursacht immer einen Fehler, um unsere Fehlerbehandlung zu testen.
Wir haben festgelegt, dass bei einem Fehler die Daten versucht werden erneut zu laden. Um temporäre Situationen auszuschließen und den Server nicht zu überfordern, gibt es eine steigende Zeitverzögerung zwischen Versuchen. Der erste Wiederholungsversuch startet nach 2 Sekunden, der zweite nach 4 Sekunden, der dritte und letzter Versuch beginnt nach 6 Sekunden nach Auftreten eines Fehlers. Um das Beispiel nicht zu komplex zu machen, laden wir als Datensätze eine Zahlenliste.

import {Injectable} from '@angular/core';
import {ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot} from '@angular/router';
import {interval, Observable, of} from 'rxjs';
import {catchError, map, retryWhen, scan, take} from 'rxjs/operators';
import {genericRetryStrategy} from './generic-retry-strategy';

@Injectable()
export class MatrjoschkaResolver implements Resolve<observable> {

  constructor(private router: Router) {
  }

  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<observable> {
    const observable = interval(50).pipe(
      take(20),
      map(x => {
          if (x === 19) {
            throw x;
          } else {
            return x;
          }
        }
      ),
      scan((acc: number[], x: number) => {
        return acc.concat([x]);
      }, []),
      retryWhen(genericRetryStrategy({
          scalingDuration: 2000,
          excludedStatusCodes: [500]
        })
      ),
      catchError(error => {
          this.router.navigateByUrl('/page-not-found');
          return of(error);
        }
      )
    );
    return of(observable);
  }
}
</observable</observable

An dieser Stelle möchte ich auf das Interface Resolve<Observable<number[]>> und den Rückgabe-Typ der resolve-Methode hinweisen. Dieser kleine Unterschied erlaubt uns, die Resolver in vielen Situationen zu nutzen.

Das Darstellen der Daten im Template ist auch etwas anders, aber nicht viel. Es handelt sich um eine Erweiterung von dem obigen Beispiel und gleichzeitig eine Vereinfachung. Der Snapshot ist nur eine Momentaufnahme. Da jedoch die Daten nicht direkt beinhaltet, sondern nur die Referenz auf das Observable ist das völlig ausreichend. item ist die Zugriffsmöglichkeit auf den Resolver – siehe Routen-Konfiguration. Um die Daten aus der asynchronen Struktur des Observable’s zu bekommen, nutzen wir die Async-Pipe.

Eine Animation kann während des Ladens angezeigt werden. Das Caching der Daten erfolgt in diesem Beispiel nicht – wäre jedoch möglich.

Im nächsten Teil schauen wir uns an, wie wir Daten mit Redux laden können.

Den vollständigen Programmcode findest du unter https://github.com/dornsebastian/angular-resolver.

Den Happy Angular Podcast zum Resolver findest du hier.


Angular
Externe Daten
Resolver
routing