Wie funktioniert ein Observable in Angular?

Ehrlich gesagt glaube ich, dass jeder, der einen Artikel mit solch einer Überschrift liest, bereits weiss, was ein Observer ist. Und das Thema Observer ist im Grunde eine absolut einfache Sache. In diesem Artikel erkläre ich kurz, was Observer sind und zeige es dann an einem Codebeispiel.

Was sind Obersables?

Stimmt, die heissen ja Observables, gar nicht Observer, verdammt. Ja ne, einen dooferen Namen hätten sie denen wohl nicht geben können. Sogar im Titel dieses Abschnitts habe ich es unabsichtlich falsch geschrieben.

Das Prinzip ist ganz einfach und auch unter Publish Subscribe und anderen Namen bekannt – Oder vorallem von Instagram und anderen Medienplattformen. Wir haben zum Beispiel eine ganz coole Band wie Sum 41, richtige Rockstars, die übrigens gerade jetzt ein neues Album herausbringen. Und dann haben wir Fans dieser Rockstars. Und die Fans wollen natürlich wissen, was die Rockstars so treiben, was sie zum Frühstück essen, wo das nächste Konzert ist etc. Und die Rockstars schicken natürlich gerne alle Informationen an die Fans, sobald diese bekannt sind.

PS: Ich wollte eigentlich die Band Falconer nehmen, die ein echter Geheimtipp von mir ist und die ich momentan im Repeat-Forever Modus rauf und runter höre, aber die sind so unbekannt, dass ich lieber Sum 41 genommen habe. Die kennen viele wohl noch aus ihrer Jugendzeit 🙂

Ja und die Rockstars ist also unser Observable und die Fans sind die Observer (auf Deutsch: Beobachter). Und jeder Fan klickt natürlich auf Instagram oder Youtube auf „Follow“, um immer über alles informiert zu sein – das wäre der Subscribe-Mechanismus.

But why?

Ja das frage ich mich auch jeden Abend vor dem Einschlafen: Warum braucht es eigentlich Observables? Also bei mir war es einfach so, dass in meinem aktuellen Projekt Observables verwendet werden und ich darum gezwungenermassen lernen muss, was dahinter steckt.

Na gut, schauen wir mal, was auf der offiziellen Seite steht… blabla… blabla… ah ja, hier steht es: Es geht darum, dass Komponenten miteinander kommunizieren wollen, und dass es asynchron passiert, also nicht blockierend.

Gut, irgendwie finde ich das auch komisch, zu erklären, warum es Observables braucht. Frag mal einen Sum 41 Fan, warum er die neuesten News von der Band haben will… Na logisch, er will halt immer auf dem aktuellen Stand sein. Und dass es nicht blockiert, wie wir Informatiker zu sagen pflegen, ist auch logisch: Kein Fan möchte permanent auf der Sum 41-Homepage sein und aktualisieren drücken, sondern die Nachrichten bequem erhalten, sobald es Neuigkeiten gibt.

Aber wem erkläre ich das eigentlich? Wir alle werden ja mittlerweile todgespamt von allen möglichen Apps, die uns Push-Notifications zusenden.

Aber gut, genug geschwatzt, bevor mir auch noch die letzte Handvoll Leser weglaufen, bringe ich lieber mein Code-Beispiel.

Lass uns ein Observable in Angular bauen

Das Beispiel ist todlangweilig (Super Einstieg, um Spannung aufzubauen, Anm. des Reviewers):

  • Wir haben eine Liste von Studenten in der Rockband (Observable)
  • und die Fans (Observer) erhalten entweder die Informatikstudenten oder die Chemiestudenten als Liste serviert.

Zuerst brauchen wir ein Beispielprojekt. Sie wissen schon, wie man ein Projekt erstellt, oder? In Java fängt auch nicht jedes Tutorial damit an, wie man ein Projekt in Eclipse erstellt. Aber vielleicht ist das ein Ritual bei Angular-Tutorials, also falls Sie es nicht wissen würden: Sie müssen mittels npm angular installieren und dann folgenden Befehl ausführen:

ng new echt-cooles-projekt

Okay haben wir diese Hürde genommen. Als nächstes erstellen wir eine einfache Model-Klasse von einem Studenten. Im Ordner „src -> app“ können Sie die folgende Datei student.model.ts erstellen:

export class Student {
   id: number;
   name: string;
   fach: string;
 }

Nun kommt der eigentliche Observer. Diesen packen wir gleich neben das Student-Model in eine Datei student.service.ts und die sieht folgendermassen aus:

import { Injectable } from '@angular/core';
import { Student } from './student.model';
import { Observable, Observer } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class StudentService {

  outsideObserver;

  informatikStudenten: Student[] = [{
    id: 1,
    name: 'Fritz',
    fach: 'Informatik'
  },
  {
    id: 2,
    name: 'Hans',
    fach: 'Informatik'
  }];

  chemieStudenten: Student[] = [{
    id: 3,
    name: 'Peter',
    fach: 'Chemie'
  }];

  constructor() {

    // 5000 steht für 5000 Millisekunden bzw. Sekunden
    setTimeout(() => {
      console.log('wechsle jetzt zu Chemiestudenten mit next');
      this.outsideObserver.next(this.chemieStudenten);
    }, 7000);

    // Das reine Löschen der Daten von aussen aktualisiert die Daten im Client nicht
    // Erst nach einem erneuten next() Aufruf
    setTimeout(() => {
      console.log('lösche jetzt die Informatikstudenten ohne next aufzurufen');
      this.informatikStudenten = [];
      console.log(this.informatikStudenten);
    }, 10000);
  }

  public getStudents(): any {
    const studentsObservable = new Observable(observer => {
      this.outsideObserver = observer;

      observer.next(this.informatikStudenten);

      setTimeout(() => {
        console.log('Jetzt schicke ich nochmals die Informatikstudenten mit next');
        observer.next(this.informatikStudenten);
      }, 12000);

      setTimeout(() => {
        console.log('sende nochmals Chemiestudenten mit next');
        observer.next(this.chemieStudenten);
      }, 16000);

      setTimeout(() => {
        console.log('Verarbeitung complete');
        // Nach einem Complete funktioniert kein error() und kein next() mehr
        observer.complete();
      }, 18000);

      // Nach einem Error funktioniert kein next() mehr
      setTimeout(() => {
        observer.error('Verdammt dieser Fehler bringt nach dem Complete gar nichts!!!');
      }, 20000);

    });

    return studentsObservable;
  }
}

Häh? Ich versteh nichts! Ich werde lieber Kebabverkäufer!

Pah, das kann gar nicht sein, ich habe das Beispiel extra einfach und verständlich gewählt!

Also gut, ich erkläre, was ich gemacht habe:

a) Die Klasse beinhaltet zwei Listen von Studenten: Informatikstudenten und Chemiestudenten. Diese wollen wir später auf der Homepage sehen.

b) Die Rockband selber findet sich in der Methode getStudents();. Dort sieht es auch komplizierter aus, als es tatsächlich ist. Ich habe einige setTimeout()’s eingebaut, welche nach und nach ausgeführt werden, damit wir das Verhalten des Observables untersuchen können. Die könnte man auch weglassen.

Das Wichtigste ist aber einfach das erstellen des Observables mit new.

Die ganze Geschichte mit dem outsideObserver können Sie eigentlich ignorieren. Das habe ich nur eingebaut, um zu zeigen, dass man das Observables auch speichern und später an einer anderen Stelle wiederverwenden kann – Was ich nämlich in keinem anderen Tutorial im Internet gefunden habe.

Die drei Methoden des Observables

Und was man dann noch wissen muss: Ein Observables – so mühsam ich dieses Wort auch finde – hat drei Methoden, die es verwenden kann:

  1. Mit next() werden alle Fans, die ein Abo haben, mit den neuesten Infos versorgt – Welche Infos das auch immer sein mögen, das kann man ja selber definieren.
  2. Mit error() kann man das Passwort des Fans auslesen. Neeeiiiinnn natürlich kann man damit eine Fehlermeldung schmeissen.
  3. Mit complete() kann man die Rockstars quasi in Rente schicken. Nach einem complete() geht next() oder error() nicht mehr, das Observable ist dann abgeschlossen. Ist übrigens optional, der Informationsfluss kann durchaus einfach offenbleiben.

Ja gut, jetzt haben wir sogar noch ein wenig Theorie gelernt.

Was ganz wichtig bei der ganzen Sache ist: Observer abonnieren zwar Informationen von den Observables, wenn ein Observable die Daten intern ändert, kriegt der Observer das aber noch nicht mit! Erst wenn der Observable die Methode next() aufruft, werden die Daten an alle Observer mit einem Abonnement verschickt.

Einen neuen Fan erstellen – Vielleicht sogar einen Groupie!

In unserem neuen Projekt können wir gleich die Datei app.component.ts für den Fan beziehungsweise den Observer verwenden. Dieser muss der Rockband mitteilen, dass sie jetzt alle News haben möchte. Das sieht dann so aus:

import { Component, OnInit } from '@angular/core';
import { Student } from './student.model';
import { StudentService } from './student.service';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {

  students: Student[] = [];
  studentsObserver;

  constructor(private studentService: StudentService) {}

  ngOnInit() {
    this.studentsObserver = this.studentService.getStudents();
  }

  getStudentData() {
    this.studentsObserver.subscribe((studentsData: Student[]) => {
      this.students = studentsData;
    });
  }

}

Hier sehen wir, wie das StudentService-Observable in den Konstruktor depency-injected wird (erklären Sie diesen Satz mal Ihrer Mutter…).

In der Methode getStudentData() wird dann effektiv das Abonnement abgeschlossen und auch gleich mitgegeben, wohinein die geschickten Daten gespeichert werden sollen.

Ich habe das ganze bewusst ein wenig auseinandergenommen um zu zeigen, dass der Code an verschiedenen Stellen in der Datei liegen kann.

Die Methode getStudentData() wird dabei von einem Button in der Datei app.component.html aufgerufen. Das sieht so aus:


<p>Öffne die Entwicklerkonsole mit F12 um die Meldungen zu sehen</p>
<p>Bitte rasch einmalig auf den Button drücken, um die Informatikstudenten zu sehen</p>

<button (click)="getStudentData()">Zeig die Studenten an</button>

<div class="col-md-3 col-xs-6" *ngFor="let student of students">
  <p>{{ student.id }}</p>
  <p>{{ student.name }}</p>
  <p>{{ student.fach }}</p>
  <hr>
</div>

Ja, als Konsequenz ist es halt so, dass Sie beim Start der Applikation den Knopf drücken müssen, damit das Abonnement abgeschlossen wird. Aber das Leben ist nun mal eine seriöse Angelegenheit. Da fällt mir gleich ein Zitat von einem „Falconer-Lied ein, das lautet:

There are too many stones and life ist just made of glass

Hmm im Lied klingt das irgendwie eindrucksvoller als einfach so geschrieben. Das Lied heisst übrigens „Busted to the floor„. Witziger Fact: Beim Erfinder dieses Titels handelt es sich um dieselbe Person, die auch den Observables den Namen gegeben hat!

Okay, wo waren wir?

Ah ja, also alles reinkopiert und dann Server gestartet mit

ng serve

dann http://localhost:4200/ aufgerufen und gleich den angezeigten Button geklickt.

Dann noch mit F12 die Entwicklerkonsole aufgemacht und schon können Sie schön nachvollziehen, wie die Studenten nacheinander vom Observable an den Observer geschickt werden.

Gut, ich gebe zu, ich habe heute Morgen wohl eine Clownsmannschaft gefrühstückt, aber es ist auch sauheiss hier und ausserdem Freitag, da muss man sich eine grosse Portion schlechten Humor bewahren. Wenn Ihnen das nicht passt schreiben Sie mir doch bitte einen Kommentar unten hin, das würde mich freuen!

In dem Sinne viel Glück mit der weiteren Angular-Programmierung!

How to create an icon in an Angular project using FontAwesome

So I just learned that if you want to insert an icon into a frontend website you don’t use images anymore but you use the icon like it was a font – For example by using the FontAwesome library.

Boostrap 4 does not support Glyphicons anymore, you can use Font Awesome instead. But if you just follow the FontAwesome instructions you will fail in an Angular project.

Instead check the hint on this page:

Font Awesome now has an official Angular component that’s available for all who want to easily use our icons in projects. If you are using Angular, you need the angular-fontawesome package or Web Fonts with CSS.

Okay this means in an Angular project you need to use the following packageAngular FontAwesome

The best page that I have found for me as I am using NPM was this page.

So basically you need to:

  1. Install FontAwesome in NPM
  2. Import the module AngularFontAwesomeModule in your app.module.ts
  3. Add the CSS in the angular-cli.json file

The details are all described in the link above.

Power Off Icon
  1. To finally add an icon you can add a power-off icon in the app.component.html file:
<fa-icon [icon]="faPowerOff"></fa-icon>
  1. AND you need to add the code in the related app.component.ts file:
    import { faPowerOff } from '@fortawesome/free-solid-svg-icons';
    ...
    export class AppComponent implements OnInit {
      faPowerOff = faPowerOff;
    ...

ONE IMPORTANT NOTE!

Please realize you need to install the correct version of AngularFontawesome in NPM for your Angular version. Check this page for the compatibility table.

I used Angular 5 so I had to install it like this:

npm install @fortawesome/angular-fontawesome@0.1.1

The entry will then be added to your package.json file.

Wie kann ich eine statische Webseite in einem AWS S3 Bucket mit einem Passwort schützen?

Amazon Web Service (AWS) erlaubt es einen auf einfache Art, eine Webseite in einem S3 Bucket hochzuladen und dafür eine URL zu generieren, so dass man in einem Browser Zugriff dafür hat.

Wie man das macht, lernt man zum Beispiel im Udemy.com Kurs Angular Essentials von Maximilian Schwarzmüller, welcher ich wärmstens empfehlen kann!

In diesem Kurs erstellt man eine Liste von Starwars Charakteren und lernt am Ende, wie man das Ganze in einen S3 Bucket hochlädt.

Was aber, wenn ich diese Seite nun mit einem Passwort schützen möchte?

Wenn ich eine Webseite auf einem Server habe ist es üblicherweise kein grosses Problem, darauf eine Authentifizierung dazwischenzuschalten (bei einem Apache oder Nginx kann das zum Beispiel via htpasswd geschehen).

Bei einer serverless Seite, die auf einem S3 Bucket liegt, ist das hinzufügen einer Authentifizierung ein wenig trickreicher. Es gibt aber eine Möglichkeit, die Seite Passwort geschützt zu machen, indem man vor die Webseite eine Lambda-Funktion stellt und mittels AWS CloudFront die Verlinkung vornimmt.

Hochladen einer Beispielseite in einen S3 Bucket

Um diesen Post durchführen zu können braucht man natürlich eine Webseite. Hat an keine zur Verfügung kann man einfach irgendeine Beispielseite als „index.html“ speichern, etwa folgende:

<html>
  <head>    
    <title>Testseite</title>  
  </head>
  <body>
  Meine Seite funktioniert!
  </body>
</html>

Und natürlich braucht man einen AWS Account dafür. Falls Sie noch keinen haben, können Sie sich problemlos einen kostenlosen Account erstellen, der ein Jahr gültig ist.

Sobald Sie in AWS eingeloggt sind, wählen Sie „S3“ als Service aus und erstellen mit „Create Bucket“ einen Kübel.

Nun kann die Datei „index.html“ mittels „Upload“ Button hochgeladen werden.

Damit man diese Seite online aufrufen kann, klicken Sie auf „Properties“ und dann „Static website hosting„. Als Index und Error Document können sie zweimal „index.html“ angeben.

Wenn Sie im Bucket auf „Permissions“ klicken, sehen Sie, dass der generelle Zugriff auf aussen geblockt ist („Block all public access„). Damit man die Webseite im Internet einsehen kann, muss man dies natürlich ändern und den Haken entfernen und dies in einem Dialog „confirmen„.

Jetzt müssen Sie noch die „index.html“ Datei klicken und dann unter „Overview“ auf „Make public„. Ganz unten sieht man nun die „Object URL„, unter der die Seite aufgerufen werden kann.

Alternativ klicken Sie nochmals auf „Properties“ und dann auf „Static website hosting“ und sie sehen oben die öffentliche URL.

Nun haben wir eine Testseite, für welche wir Authentifizierung einbauen können.

Verbreitung der Webseite mittels AWS CloudFront

CloudFront ist der AWS Verteilungsservice, mit welchem Sie Ihre Webseite nun auf Server auf der ganzen Welt verteilen können, damit Benutzer immer den schnellstmöglichen Zugriff darauf erhalten. Zusätzlich dazu erlaubt uns CloudFront in Zusammenarbeit mit Lambda auch, eine Authentifizierung aufzusetzen.

Wählen Sie zuerst in AWS den CloudFront Service aus. Da wahrscheinlich noch keine „Distribution“ verfügbar ist, klicken Sie auf „Create Distribution“ und dann im „Web“ Bereich auf „Get Started„.

Dort sind folgende Einstellungen wichtig:

  • Origin Domain Name: Dies muss der vorher erstellte Bucket sein. Kliken Sie einfach in das Feld und wählen Sie den Bucket aus.
  • Setzen Sie „Restrict Bucket Access“ auf „Yes
  • Unter „Origin Access Identity“ klicken Sie „Create a New Identity
  • Setzen Sie „Yes, Update Bucket Policy
  • Unten gibt es noch ein Feld „Default Root Object„: Geben Sie hier „index.html“ ein.
  • Klicken Sie auf „Create Distribution

Und weiter unten:

Wenn Sie nun im linken Menü auf „Distributions“ klicken sehen Sie die neu erstellte Distribution. Merken Sie sich den „Domain Name“ – Sobald sie fertig initialisiert wurde wird Ihre Webseite unter dieser URL aufgerufen werden können. Die Initialisierung dauert aber ein paar Minunten. In dieser Zeit können wir die IAM Rolle erstellen.

Erstellen der IAM Rolle

Damit die Lambda Funktion auf den S3 Bucket zugreifen kann, brauchen wir eine IAM Rolle, welche den Zugriff regelt. Gehen Sie also in den „IAM“ Service und klicken Sie links „Roles“ und dann „Create Role„.

Unter „Choose the service that will use this role“ wählen Sie „Lambda“ und klicken auf „Next: Permissions„.

Suchen Sie nach „AWSLambdaExecute“ und setzen Sie links davon einen Haken. Dann können Sie zweimal „Next“ klicken und dürfen Ihrer neuen Rolle noch einen Namen geben, etwa „lambda-website-execute„. Klicken Sie auf „Create Role“ um die Rolle zu erstellen.

Für die Authentifizierung benötigen wir Lambda@Edge, was wir der Lambda Policy angeben müssen. Klicken Sie Ihre neue Rolle und wählen dann „Trust relationships“ und dann „Edit trust relationship„.

Ersetzen Sie den gesamten Inhalt mit folgendem Text und klicken Sie auf „Update trust relationship„:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": [
            "lambda.amazonaws.com",
            "edgelambda.amazonaws.com"
        ]
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

Authentifizierung mit einer Lambda Funktion einbauen

Nun sind alle Teile bereit für den Einbau der eigentlichen Passwortabfrage mittels Lambda.

Zuerst aber noch etwas wichtiges: Damit wir später unsere CloudFront Distribution als Trigger hinzufügen können, müssen wir die „AWS Region“ auf „US East (N. Virginia)“ ändern (im schwarzen Balken ganz oben rechts. Das funktioniert übrigens erst im „Lambda“ Service, nicht im „IAM„.)

Lambda Funktionen laufen serverless, das bedeutet, wir müssen nur die Funktion erstellen, aber keinen Server, auf dem diese läuft. Die Funktion wird aufgerufen, wenn sie benötigt wird und in einer produktiven Umgebung zahlt man dann auch entsprechend der Anzahl Aufrufe.

Klicken Sie in „Lambda“ auf „Create a function„.

  • Lassen Sie auf der nächsten Seite „Author from scratch“ selektiert, um anzuzeigen, dass Sie eine leere Funktion erstellen möchten.
  • Als „Function name“ wählen Sie zum Beispiel „website-authentication
  • Als „Runtime“ ist die neueste Node.js Version auszuwählen.
  • Unter „Permissions“ klicken Sie auf das schwarze Dreieck, setzen dann „Use an existing role“ und wählen dann die erstellte IAM Rolle „lambda-website-execute“ aus, in dem Sie in das Feld klicken.
  • Anschliessend können Sie auf den orangen „Create function“ Knopf klicken.

Scrollen Sie nun runter in den Code Editor und erstezen den vorhandenen Code mit folgendem Snippet:

exports.handler = (event, context, callback) => {

  // Get the request and its headers
  const request = event.Records[0].cf.request;
  const headers = request.headers;

  // Specify the username and password to be used
  const user = 'user';
  const pw = 'password';

  // Build a Basic Authentication string
  const authString = 'Basic ' + new Buffer(user + ':' + pw).toString('base64');

  // Challenge for auth if auth credentials are absent or incorrect
  if (typeof headers.authorization == 'undefined' || headers.authorization[0].value != authString) {
    const response = {
      status: '401',
      statusDescription: 'Unauthorized',
      body: 'Unauthorized',
      headers: {
        'www-authenticate': [{key: 'WWW-Authenticate', value:'Basic'}]
      },
    };
    callback(null, response);
  }

  // User has authenticated
  callback(null, request);
};

Dieser Code nimmt den HTTP Request des Benutzers und überprüft, ob die Informationen im Header mit dem angegebenen Benutzer und Passwort übereinstimmen. Wenn nicht, gibt es keinen Zugriff auf die Seite.

Nun müssen wir noch angeben, dass die Lambda Funktion von CloudFront aufgerufen wird – dazu fügen wir CloudFront als Trigger hinzu. Zuerst müssen Sie aber die Funktion mittels „Save“ Knopf speichern und mittels „Actions -> Publish“ veröffentlichen.

Klicken Sie dann auf „Add trigger“ und wählen Sie CloudFront aus. Wie geschrieben werden Sie CloudFront nur sehen, wenn die Region „US East (N. Virginia)“ ist (siehe anfang dieses Kapitels).

Klicken Sie auf „Deploy to Lambda@Edge“ und setzen Sie die folgenden Einstellungen:

  • Distribution: Hier sollte der Code ihrer CloudFront Distribution stehen. Sie können jederzeit in einem neuen Tab CloudFront aufrufen und den Code überprüfen.
  • CloudFront event: Wählen Sie hier „Viewer request
  • Setzen Sie einen Haken bei „I acknowledge that on deploy…
  • Klicken Sie unten auf „Deploy„.

Aufruf der Seite in einem Browser

Et voilà – Wir haben es geschafft!

Nun können Sie die URL der CloudFront Distribution in einen Browser eingeben und sollten mit einem Login begrüsst werden. Die URL können Sie in CloudFront einsehen, falls Sie sie nicht mehr wissen.

Im Lambda haben wir als Benutzer und Passwort „user“ und „password“ genannt. Dies sollten Sie natürlich noch ändern und die Lambda Funktion nochmals mit „Publish“ veröffentlichen.

Den S3 Bucket nicht mehr public halten

Der S3 Bucket ist immer noch public und momentan kann immer noch jeder mit der URL darauf zugreifen. Gehen Sie also nochmals in den „S3“ Service, dann in den Bucket, dann auf „Permissons“ und stellen Sie „Block all public access“ wieder ein.

Der Bucket muss nicht öffentlich zugänglich sein, da man ja nun über CloudFront und über Lambda auf den Bucket mittlels Benutzer und Passwort zugreift.

Wenn Sie nun die Bucket URL (Achtung: Nicht die CloudFront URL!) aufrufen, sollte eine „403 Forbidden“ Fehlermeldung erscheinen.

Wunderbar!