Kolumne: Die Angular-Abenteuer

Querschnittsfunktionen mit Angular implementieren
Keine Kommentare

Querschnittsfunktionen – das sind diese lästigen, meist technischen Anforderungen, die es immer und immer wieder zu berücksichtigen gilt. Beispiele dafür sind unter anderem Authentifizierung, Protokollierung oder die Behandlung von Fehlern. Natürlich möchte man die dafür nötigen Methodenaufrufe nicht ständig wiederholen müssen. Idealerweise werden sie automatisch aktiv. In diesem Artikel zeige ich drei Mechanismen von Angular, die genau das auf elegante Art erlauben: Guards, HTTP Interceptors und Direktiven. Alle gezeigten Beispiele können auf GitHub nachgelesen werden.

Guards

Mit Guards können sich Angular-Anwendungen über Routenwechsel informieren lassen. Dabei handelt es sich lediglich um Services mit vorgegebenen Methoden, die der Router zu bestimmten Zeitpunkten aufruft. Diese Methoden können auch ins Routing eingreifen: Ihr zurückgelieferter Wert bestimmt, ob der Router den angeforderten Routenwechsel tatsächlich durchführen darf. Kann die Methode ihre Entscheidung augenblicklich bekannt geben, liefert sie einen Boolean. Um die Entscheidung hinauszuzögern, liefert sie zunächst lediglich ein Observable<boolean> oder einen Promise<boolean>. Steht die Entscheidung später fest, kann sie über diese Mechanismen den Router benachrichtigen. Dieses Vorgehen ist beispielsweise notwendig, wenn zur Entscheidungsfindung ein Web-API zu konsultieren oder Rücksprache mit dem Benutzer zu halten ist.

Für unterschiedliche Arten von Guards definiert Angular auch unterschiedliche Interfaces, die es zu implementieren gilt (Tabelle 1).

Interface Methode Beschreibung
CanActivate canActivate Legt fest, ob die gewünschte Route aktiviert werden darf.
CanActivateChild canActivateChild Legt fest, ob bzw. welche Child Routes einer Route aktiviert werden dürfen.
CanLoad canLoad Legt fest, ob ein Modul per Lazy Loading geladen werden darf.
CanDeactivate canDeactivate Legt fest, ob eine Route deaktiviert werden darf.

Tabelle 1: Interfaces für Guards

Wie man sich beim Betrachten dieser Interfaces denken kann, lassen sich gleich einige Arten von Querschnittsfunktionen mit Guards implementieren. Das nachfolgende Beispiel dient dem Schützen von Routen. Möchte ein Benutzer eine Route aktivieren, für die ihm die Berechtigungen fehlen, soll er auf die Log-in-Seite zurückgesendet werden (Abb. 1).

Abb. 1: Routen mit Guards schützen

Abb. 1: Routen mit Guards schützen

Dies dient weniger der Sicherheit, zumal Sicherheit bei browserbasierten SPAs immer im Backend zu realisieren ist. Vielmehr fördert es die Benutzerfreundlichkeit, da die Anwendung den Benutzer, im Fall des Falles, hierdurch zur Anmeldung auffordern kann. Beim Guard handelt es sich um einen einfachen Service, der den Typ CanActivate implementiert. Neben dem hier aus Platzgründen nicht näher beschriebenen AuthService zum Anmelden von Benutzern lässt er sich auch den Router injizieren. Damit leitet er Benutzer mit fehlenden Berechtigungen auf eine andere Route weiter (Listing 1).

@Injectable({ providedIn: 'root'})
export class AuthGuard implements CanActivate {
  constructor(private authService: AuthService, private router: Router) {
  }
  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean  
  {
    if (this.authService.isLoggedIn) {
      return true;
    }
    else {
      this.router.navigate(['/home', {needLogin: true}]);
      return false;
    }
  }
}

Die von CanActivate vorgegebene gleichnamige Methode erhält vom Router einen ActiveRouteSnapshot, der über die gewünschte Route informiert, sowie einen RouterStateSnapshot, der über die aktuelle Route Auskunft gibt. Sie wendet sich an den AuthService, um herauszufinden, ob der aktuelle Benutzer angemeldet ist. Ist dem so, retourniert sie true und erlaubt somit die gewünschte Aktivierung. Ansonsten leitet sie mit der Methode navigate des Routers den Benutzer auf die Route „home“ weiter. Dabei übergibt sie einen Parameter, der die dahinterliegende HomeComponent wissen lässt, dass der Benutzer aufgrund fehlender Berechtigungen bei ihr gestrandet ist.

iJS React Cheat Sheet

Free: React Cheat Sheet

You want to improve your knowledge in React or just need some kind of memory aid? We have the right thing for you: the iJS React Cheat Sheet (written by Joel Lord). Now you will always know how to React!

Um den Guard für eine Route zu aktivieren, muss sie mit ihrer Eigenschaft canActivate lediglich darauf verweisen. Damit eine Route mehrere solche Guards nutzen kann, handelt es sich bei canActivate um ein Array (Listing 2).

const FLIGHT_BOOKING_ROUTES: Routes = [
  {
    path: '',
    component: FlightBookingComponent,
    children: [
      {
        path: 'flight-search',
        component: FlightSearchComponent
      },
      {
        path: 'passenger-search',
        component: PassengerSearchComponent,
        canActivate: [AuthGuard]

      },
      {
        path: 'flight-edit/:id',
        component: FlightEditComponent,
      }

    ]
  }
];

HTTP Interceptors

Der HttpClient von Angular bietet mit Interceptoren einen Einsprungspunkt. Jeder bereitgestellte Interceptor hat die Möglichkeit, ausgehende HTTP-Anfragen sowie eingehende HTTP-Antworten zu manipulieren. Auf diese Weise lassen sich Anfragen um Authentifizierungsinformationen erweitern, aber auch Caching-Strategien implementieren oder Datenformate wie XML oder CSV verarbeiten. Die Idee hinter diesen Interzeptoren folgt dem Muster der Chain of Resposibility (Abb. 2).

Abb. 2: Chain of Responsibility

Abb. 2: Chain of Responsibility

Dieses Muster sieht vor, dass eine Aktion mit Zusatzlogiken erweitert wird. Letztere werden in Klassen untergebracht, deren Objekte zu einer Kette zusammengeschlossen werden. Am Ende dieser Kette befindet sich die eigentliche Aktion. Jedes dieser Objekte kümmert sich um seine Aufgabe und kann an das nächste Kettenglied delegieren.

Ein Beispiel für einen Interceptor, der jede ausgehenden Anfrage um einen beispielhaften Authorization-Header erweitert, findet sich in Listing 3. Außerdem leitet er den Benutzer auf die Startseite um, wenn die Antwort auf einen Securityfehler schließen lässt.

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  
  constructor(private router: Router) {
  }
  
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    
    if (req.url.startsWith('http://www.angular.at')) {
      const headers = req.headers.set('Authorization', 'asfjsdfjjkjru==');
      req = req.clone({ headers });
    }

    return next.handle(req).pipe(
      catchError(resp => this.handleError(resp))
    );
  }

  handleError(resp: HttpErrorResponse): Observable<HttpEvent<any>> {

    if (resp.status === 401 || resp.status === 403) {
      this.router.navigate(['/home', {needsLogin: true}]);
    }
    return throwError(resp);

  }
}

Wie das Beispiel zeigt, handelt es sich bei Interceptors um Services, die das Interface HttpInterceptor implementieren. Es gibt lediglich die Methode intercept vor, an die Angular die aktuelle Anfrage sowie das nächste Glied der Kette weitergibt. Um die Kontrolle an dieses weiterzugeben, ruft der Interceptor die Methode next auf. Das Ergebnis dieser Methode ist ein Observable mit der HTTP-Antwort. Dieses lässt sich mit den üblichen RxJS-Operatoren bearbeiten.

Bevor der hier gezeigte Interceptor die Anfrage um einen Authorization-Header erweitert, prüft er, ob es sich um ein vertrauenswürdiges Web-API handelt. Auf diese Weise stellt er sicher, dass solche vertrauenswürdigen Informationen nicht in die falschen Hände geraten.

Außerdem ist hier zu beachten, dass das API rund um den HttpClient mit Immutables arbeitet. Das bedeutet, dass Interceptoren die Headers-Auflistung aber auch die Anfrage nicht verändern kann. Stattdessen müssen sie diese Objekte klonen und im Rahmen dessen verändern. Bei den Kopfzeilen kümmert sich darum die Methode set. Anstatt die Auflistung zu verändern, liefert sie eine Kopie mit der zusätzlichen Kopfzeile. Bei der Anfrage kommt für dieselbe Aufgabe die Methode clone zum Einsatz.

Damit Angular den Interceptor verwendet, muss die Anwendung ihn für das Token HTTP_INTERCEPTORS registrieren (Listing 4).

@NgModule({
  imports: [
    CommonModule
  ],
  declarations: [
    CityPipe,
  ],
  exports: [
    CityPipe,
  ]
})
export class SharedModule {
  static forRoot(): ModuleWithProviders {
    return {
      ngModule: SharedModule,
      providers: [{
        provide: HTTP_INTERCEPTORS,
        useClass: AuthInterceptor,
        multi: true
      }]
    }
  }
}

Die Option multi ist hier auf true zu setzen, um Angular zu informieren, dass es mehrere Interceptors geben kann. All diese bilden die oben erwähnte Kette. Die Reihenfolge der Registrierungen repräsentieren die Reihenfolge der Glieder innerhalb der Kette.

Direktiven

Bei Direktiven handelt es sich um die kleinen Geschwister der Komponenten. Sie fügen Verhalten zu bestimmten Elementen hinzu, ohne ein Template anzuzeigen. Die gewünschten Elemente adressieren sie über CSS-Selektoren. Das bedeutet, dass sie alle Elemente der Anwendung, die bestimmte Eigenschaften aufweisen, erweitern können. Genau deswegen eignen sie sich wunderbar für die Umsetzung von Querschnittsfunktionen.

Zur Demonstration soll eine für kritische Aktionen gedachte Alternative zum click-Ereignis bereitgestellt werden. Diese gibt zunächst nur eine Warnmeldung aus und stößt den hinterlegten Event Handler nur an, wenn diese bestätigt wurde (Abb. 3).

Abb. 3: Direktive für kritische Aktionen

Abb. 3: Direktive für kritische Aktionen

Eine Direktive definiert sich ähnlich wie eine Komponente: Es handelt sich dabei um eine Klasse, die Bindings aufweisen kann. Metadaten sind über den Dekorator Directive bereitzustellen. Dieser hat fast alle Eigenschaften, die man auch von Component kennt – lediglich templatebezogene Eigenschaften fehlen, zumal Direktiven eben keine Templates haben. Zu diesen Eigenschaften, die man vergeblich suchen wird, zählen template bzw. templateUrl, styles bzw. styleUrls und viewProviders.

Die hier betrachtete Direktive verwendet einen Selektor, der sämtliche Elemente mit dem Attribut flightClickWithWarning adressiert. Die Verwendung von Camel Case ist hier ebenso üblich wie der Einsatz eines projektspezifischen Präfix. Für letzteren fällt hier die Wahl abermals auf flight (Listing 5).

@Directive({
  selector: '[flightClickWithWarning]'
})
export class FlightClickWithWarningDirective implements OnInit {

  // Darzustellende Warnung
  @Input() warning: string = 'Are you sure?';

  // Event-Handler, der nach Besätigung der Warnung 
  // auszuführen ist
  @Output() flightClickWithWarning = new EventEmitter();

  constructor(
    private elementRef: ElementRef, 
    private renderer: RendererV2) {

      // elementRef: Verweis auf aktuelles Element
      // renderer: Services zum Verändern von Elementen

  }

  ngOnInit() {
    // Warnung: Direkter DOM-Zugriff!
    // this.elementRef.nativeElement.setAttribute('class', 'btn btn-danger');

    // Indirekter DOM-Zugriff über Renderer
    this.renderer.setAttribute(this.elementRef.nativeElement, 'class', 'btn btn-danger');
  }

  […]
}

Der Name flightClickWithWarning wird nicht nur für den Selektor herangezogen, sondern auch für das Event, das nach einer eventuellen Bestätigung aufzurufen ist. Eine solche Vorgehensweise ist üblich, zumal sie es erlaubt, im selben Atemzug sowohl die Direktive auf eine Komponente anzuwenden als auch eine erste Bindung festzulegen:

<button 
  (flightClickWithWarning)="remove()" 
  [warning]="…">Remove</button>

Die Direktive lässt sich die aktuelle ElementRef injizieren. Dabei handelt es sich um ein Objekt, das das aktuelle Element referenziert. Das ist jenes Element, auf das die Direktive angewandt wurde. Im Fall des letzten Beispiels handelt es sich dabei um das button-Element.

Zum Verändern des Buttons lässt sie sich auch den aktuelle Renderer injizieren. Dessen Nutzung demonstriert der Livecycle-Hook ngOnInit. Seine Aufgabe besteht darin, die Klassen btn und btn-danger zum Button hinzuzufügen. Diese lassen es in einem warnenden Rot erstrahlen. Wie der Kommentar zeigt, könnte diese Aufgabe eine Direktive ohne Renderer bewerkstelligen, zumal eine ElementRef über ihre Eigenschaft nativeElement direktiven Zugriff auf das zugrundeliegende DOM-Element gewährt. Diese Vorgehensweise klappt jedoch nur, wenn Angular auf klassische Weise im Hauptthread des Browsers ausgeführt wird. Kommt Angular zum Beispiel serverseitig, in einer nativen Anwendung oder in einem Web-Worker zur Ausführung, steht das DOM-Element nicht zur Verfügung.

Um dem gerecht zu werden, sieht Angular vor, dass jede Plattform ihren eigenen Renderer definiert. Dieser kümmert sich um das korrekte Modifizieren von Elementen, wie dem im betrachteten Fall demonstrierten Hinzufügen von Klassen. Neben dem Verändern der adressierten Elemente müssen Direktiven häufig auch deren Ereignisse behandeln. Im hier verwendeten Fallbeispiel hat die FlightClickWithWarningDirective beispielsweise die Aufgabe, auf das click-Ereignis zu reagieren. Hierfür kommen HostListener, die ein Ereignis mit einer Methode verknüpfen, zum Einsatz:

@HostListener('click', ['$event'])
  handleClick($event): void {
    if (confirm(this.warning)) {
      this.flightClickWithWarning.emit();
    }
  }

Der hier betrachtete HostListener bringt bei jedem click-Ereignis des adressierten Elements die Methode handleClick zur Ausführung. Diese gibt einen Warndialog aus und stößt – sofern dieser bestätigt wird – das Ereignis flightClickWithWarning an. Wie auch Komponenten sind Direktiven über ein Modul zu deklarieren und auf Wunsch auch über exports anderen Modulen zur Verfügung zu stellen.

Fazit

Idealerweise werden Querschnittsfunktionen automatisch aktiv, ohne dass der Programmcode sie immer und immer wieder anstoßen muss. Guards ermöglichen das im Zuge des Routings. Sie stellen Logiken bereit, die beim Routenübergang greifen und diese können den Routenvorgang auch unterbinden. Damit lassen sich zum Beispiel Autorisierungslogiken implementieren. HTTP-Interzeptoren werden hingegen bei jedem HTTP-Aufruf tätig und können ausgehende Anfragen sowie eingehende Antworten manipulieren. Anwendungsgebiete sind das Übermitteln von Authentifizierungsinformationen, Caching oder Fehlerbehandlungen. Die ein wenig im Schatten der Komponenten stehenden Direktiven erlauben, zusätzliches Verhalten zu allen Elementen der Anwendung hinzuzufügen, die bestimmte Eigenschaften aufweisen.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu:
X
- Gib Deinen Standort ein -
- or -