Ein bisschen Nachsicht bitte! Anwendungsstabilität mit MicroProfile Fault Tolerance

MicroProfile (https://microprofile.io/) enthält u. a. den Baustein MP Fault Tolerance, mit dem auf sehr einfache Weise die bekannten Stability Patterns in JEE/MicroProfile-Anwendungen eingebaut werden können.

Bei verteilten Anwendungen (Microservices) – aber nicht nur dort – können Fehler durch nicht erreichbare oder nicht funktionierende Anwendungsteile entstehen. Soll dann nicht wie bei einem Dominoeffekt die Gesamtanwendung betroffen sein, muss der aufrufende Teil resilient sein, d. h. durch Wiederholungen oder Ersatzaufrufe zu einem verträglichen Ergebnis kommen.

MP Fault Tolerance enthält Interzeptoren mit den im Folgenden beschriebenen Bindings, mit denen im Fehlerfall gewünschte Ausweichaktionen initiiert werden können.

Nochmals probieren

@Retry führt beim Auftauchen einer Exception zum wiederholten Aufruf der Methode:

@Retry(maxRetries = 4)
public int doSomethingWithRetry() {

Mit diversen Parametern von @Retry kann bestimmt werden, welche Exceptions die Wiederholungen auslösen sollen, wie lange oder wie oft es versucht werden soll und welche Wartezeit zwischen den Wiederholungen eingehalten werden soll.

Programmierte Ungeduld

Mit @Timeout kann eine Obergrenze für die Ausführungszeit einer Methode angegeben werden:

@Timeout(1000)
public int doSomethingWithTimeout() {

Rückstellende Sicherung

Circuit Breaker ist ein Pattern, das bei zu häufigen Fehlern (=Exceptions) „den Schalter öffnet“, d. h. die betroffene Methode für eine gewisse Zeit nicht mehr aufruft und statt dessen eine dementsprechende Exception wirft. Nach Ablauf der Wartezeit wird der Schalter probeweise wieder geschlossen.

MP Fault Tolerance bietet dazu @CircuitBreaker an:

@CircuitBreaker(failureRatio = 0.25, requestVolumeThreshold = 10)
public int doSomethingWithCircuitBreaker() {

Mit Parametern kann bestimmt werden, was „zu häufig“ sein soll und wann der Schalter nach Öffnung wieder geschlossen wird.

Ersatzhandlung

Sind Fehler (=Exceptions) für den Aufrufer nicht OK, kann mit @Fallback eine Ersatzaktion definiert werden:

@Fallback(fallbackMethod = "return42")
public int doSomethingWithFallback() {
  ...
}

private int return42() {
  return 42;

Plattformen

MicroProfile Fault Tolerance wird u. a. von folgenden JEE-Servern unterstützt:

  • OpenLiberty (mit aktiviertem Feature mpFaultTolerance-2.0).
  • Payara.

WildFly wird MP Fault Tolerance voraussichtlich ab der Version 19 unterstützen

Die genannten Annotationen befinden sich bspw. in der Maven-Dependency org.eclipse.microprofile.fault-tolerance:microprofile-fault-tolerance-api (derzeit in Version 2.0).

Demo

In https://github.com/GEDOPLAN/microprofile-demo/tree/master/microprofile-fault-tolerance finden Sie ein Beispielprojekt mit einem Service, der die oben gezeigten Annotationen enthält und über Rest Endpoints aufgerufen werden kann. Die Anwendung kann als WAR-File auf einen der genannten Server deployt werden. Alternativ können über vorkonfigurierte Maven-Profile Docker-Images zum direkten Ausprobieren erstellt werden.

Bis bald – vielleicht auch in einem unserer Trainings in Berlin, Frankfurt, Bielefeld, Köln oder bei Ihnen!

Schauen Sie sich doch mal unseren neuen Kurs zum Thema an: JEE und MicroProfile mit Quarkus

Angular Formulare – Formatter und Parser, Teil 2

Ein großer Anteil in Businessanwendungen wird sicherlich den Formularen zufallen. Von der einfachen Eingabe eines Textes bis hin zu aufwendigen GUI-Komponenten haben sie doch alle eines gemeinsam: am Ende müssen die erfassten Daten im gewünschten Format in der gewünschten Qualität im Datenmodel landen. Den einzig richtigen Weg gibt es nicht, schauen wir uns also einige der möglichen Varianten an

Als einfaches Beispiel dient uns wie zuvor das Beispiel der Kundennummer Format: 6stellig numerisch mit dem Präfix: „C-„. Im ersten Teil haben wir für einen Validator gesorgt der dieses Format überprüft. In diesem Teil gehen wir einen Schritt weiter. Wir wollen dem Benutzer ein etwas toleranteres Eingabeverhalten bieten: Die Kundennummer darf auch nur aus 6 numerischen Werten bestehen. Angular sollte in diesem Fall natürlich dafür sorgen das unser Format ( C-XXXXXX ) trotzdem eingehalten wird.

Pragmatisch

Eine sehr einfache pragmatische Lösung ist es mittels Event-Listener die eingegebenen Werte anzupassen, falls benötigt. Dazu ist nicht viel nötig, außer eine entsprechende Methode innerhalb der Komponente-klasse

Template

 <input type="text" formControlName="customernumber1" 
         (blur)="formatNumber('customernumber1')" />

Komponente

  formatNumber(formControlName) {
    const control = this.form.controls[formControlName];
    if (control.value && control.valid && !/^C-.*?/.test(control.value)) {
      control.patchValue('C-' + control.value);
    }
  }

Erfüllt seinen Zweck und mag für einige Anwendungsfälle ausreichen. Diese Lösung ist aber kaum wiederverwendbar, kapselt es die Logik zur Formatierung doch in einer konkreten Komponente. Selbst, wenn wir dies in einen separaten Service auslagern bleibt der fade Beigeschmack das es hier eine elegantere Lösung gibt

Formatter als Direktive

Warum also die Logik nicht aus der Komponente herauslösen und das Ganze in eine separate Direktive verpacken

@Directive({
  selector: '[gedCustomernumberFormatter]'
})
export class CustomernumberFormatterDirective {

  constructor(private el: ElementRef, 
              private renderer: Renderer2,
              private control: NgControl) { }

  @HostListener('blur', ['$event.target.value'])
  onBlur(val: string) {
    if (this.control.valid && val && !/^C-.*/.test(val)) {
      this.renderer.setProperty(this.el.nativeElement, 'value', 'C-' + val);
    }
  }
}

Eine klassische Direktive. Wir registrieren uns per HostListener-Binding an das Blur-Event des Eingabefeldes und formatieren den übergebenen Wert. Kleines Gimmick: wir injizieren uns das zugrundeliegende FormControl, um die Formatierung der Eingabe nur dann vorzunehmen wenn die Validierung auch erfolgreich war

Verwendung

<input type="text" formControlName="customernumber2" gedCustomernumberFormatter />

Formatter als ValueAccessor

Mit den Lösungen oben haben wir für die meisten der Situationen das passende Mittel zu Hand. Eine kleine Anpassung unserer Anforderung führt aber zur Erforderniss einer Alternative: der Anwender soll unsere Präfix („C-„), das später Teil des Datensatzes sein muss, nicht sehen . Allgemein ausgedrückt: der eingegeben / sichtbare Wert soll sich vom Wert der im Datenmodel vorliegt unterscheiden. Von einer pragmatischen Lösung wie oben innerhalb der konkreten Komponente, gibt es auch hier einen eleganten Weg: einen eigenen ControlValueAccessor der das Bindeglied zwischen Komponenten-Klasse und UI darstellt

@Directive({
  selector: '[gedCustomernumberFormatterAccessor]',
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => CustomernumberFormatterAccessorDirective),
      multi: true
    }
  ]
})
export class CustomernumberFormatterAccessorDirective implements ControlValueAccessor {

  constructor(private el: ElementRef, private renderer: Renderer2) { }

  updateValue: any;
  touched: any;

  writeValue(obj: any): void {
    obj = obj.replace('C-', '');
    this.renderer.setProperty(this.el.nativeElement, 'value', obj);
  }

  registerOnChange(fn: any): void {
    this.updateValue = fn;
  }

  @HostListener('blur', ['$event'])
  onInput(event: any) {
    this.touched();
    let val = event.target.value;

    if (!/^C-/.test(val)) {
      val = 'C-' + val;
    } else {
      this.renderer.setProperty(this.el.nativeElement, 'value', val.replace('C-', ''));
    }

    this.updateValue(val);
  }

  registerOnTouched(fn: any): void {
    this.touched = fn;
  }
}


Die spannenden Stellen Zusammengefasst:

  • providers-Array, wir registrieren unsere Komponente als NG_VALUE_ACCESSOR, ähnlich wie bei den Validatoren um Angular mitzuteilen, das es sich bei dieser Komponente um ein Control handelt
  • Interface ControlValueAccesor, mit den Methoden
    • writeValue, Model > View, hier entfernen wir das Präfix, falls hier initiale Daten von der FormControl übergeben werden
    • registerOnChange, Change-Methode registrieren die wir später bei Änderungen triggern werden (wird von Angular aufgerufen, wenn die Komponente der FormGroup hinzugefügt wird)
  • HostListener, wenn der Focus das Textfeld verlässt sorgen wir dafür, dass:
    • das „C-“ Präfix zum übertragenden Wert hinzugefügt wird, falls nicht vorhanden
    • das „C-“ Präfix im Textfeld entfernt wird, falls der Benutzer es eingeben hat (optional)
    • der geänderte Wert (Benutzereingabe + Präfix) an die vorher registrierte Change-Methode übergeben wird

Verwendung

<input type="text" formControlName="customernumber3" gedCustomernumberFormatterAccessor />

Cool. Das erlaubt es uns (gekapselt und wiederverwendbar) Benutzereingaben bei der weiteren Verarbeitung zu formatieren, ohne das der Benutzer dies wissen / sehen muss.

Gibt es das auch bei GitHub? Klar. https://github.com/GEDOPLAN/ng-input

Angular Formulare – Komponenten und Validatoren, Teil 1

Ein großer Anteil in Businessanwendungen wird sicherlich den Formularen zufallen. Von der einfachen Eingabe eines Textes bis hin zu aufwendigen GUI-Komponenten haben sie doch alle eines gemeinsam: am Ende müssen die erfassten Daten im gewünschten Format in der gewünschten Qualität im Datenmodel landen. Den einzig richtigen Weg gibt es nicht, schauen wir uns also einige der möglichen Varianten an

Als einfaches Beispiel soll uns eine Kundennummer dienen die im Datenmodel ein festes Format aufzuweisen hat: 6stellig numerisch mit dem Präfix: „C-„. Die Grundlage für alles weitere stellt folgendes reaktives Formular dar.

Template

<form [formGroup]="form" (ngSubmit)="submit()">
    <input type="text" formControlName="customernumber1" />
</form>

Component

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

  constructor(private fb: FormBuilder) { }

  form = this.fb.group({
    customernumber1: ['C-123456']
  });

  submit() {
    console.log(this.form.value);
  }
}

app.component.ts

Validierung

Eine einfache Validierung in Angular kann technisch gesehen eine einfache Funktion sein, die als Parameter einen Wert vom Typ AbstractControl entgegennimmt und einen booleschen Wert zurückliefert (alternativ ein Observable). Das stößt allerdings schnell an seine Grenzen, wenn wir Angular Services nutzen wollen die bekanntlich per Injektion zur Verfügung gestellt werden. Also implementieren wir das Ganze am besten gleich als Service:

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

  private static readonly PATTERN = /^(C-)*\d{6}$/;

  validate(ctrl: AbstractControl) {
    const value = ctrl.value;
    const valid = CustomernumberValidatorService.PATTERN.test(value);
    if (value && !valid) {
      return {
        pattern: 'Ungültiges Format'
      };
    }
  }
}

customernumber-validator.service.ts

Per inject holen wir uns nun diesen Validator in unsere Komponente und verwenden sie bei der Erstellung unserer FormGroup. Optional können wir noch angeben, wann die Validierung / Übertragung ins Model erfolgen soll (Default: ‚change‘)

@Component({...})
export class AppComponent {

  constructor(private customernumberValidator: CustomernumberValidatorService) {}


  form = this.fb.group({
    customernumber1: [
        'C-123456', 
         { 
             validators: [this.customernumberValidator.validate],                 
             updateOn: 'blur' 
         }
        ]
  });

app.component.ts

Für die Template-Driven-Nutzer wird zusätzlich eine entsprechende Wrapper-Direktive benötigt. Hier ist insbesondere die Provider-Deklaration spannend, über die wir Angular mitteilen, das diese Direktive ein Validator ist und zusammen mit den anderen Validatoren ablaufen soll. Das verwendete Interface ist hingegen unspektakulär. Die Implementierung für unser Beispiel:

@Directive({
  selector: '[gedCustomernumberValidator]',
  providers: [
    { provide: NG_VALIDATORS, useExisting: forwardRef(() => CustomernumberValidatorDirective), multi: true }
  ]
})
export class CustomernumberValidatorDirective implements Validator {

  constructor(private customernumverValidatorService: CustomernumberValidatorService) { }

  validate(control: AbstractControl): ValidationErrors {
    return this.customernumverValidatorService.validate(control);
  }

  registerOnValidatorChange?(fn: () => void): void {
    // nothing to do
  }
}

customernumber-formatter.directive.ts (nur für Template-Driven)

Entsprechende Fehlermeldungen lassen sich dann über die Attribute „valid“ und „errors“ des FormControls verarbeiten. Zugriff darauf erhält man entweder durch eine entsprechende Template-Variable (Template-Driven) oder den Zugriff über die FormGroup (Model-Driven)

 
<input type="text" [(ngModel)]="templateModelValue" #templateModel="ngModel" />
{{templateModel.control}}

<input type="text" formControlName="customernumber1" />
{{form.controls['customernumber1']}}
  

Hilfreich ist hier sicherlich die Ausgabe der Meldungen zu vereinheitlichen und z.B. eine eigene Komponente zu entwickeln die dafür sorgt das alle Meldungen am Textfeld ausgeben werden.

@Component({
  selector: 'ged-validation-error-marker',
  template: '<span *ngIf="visible"> {{ message }} </span>',
  styleUrls: ['./validation-error-marker.component.scss']
})
export class ValidationErrorMarkerComponent{
  @Input()
  control: FormControl;

  get visible() {
    return this.control && !this.control.valid;
  }

  get message() {
    const errors = this.control.errors;
    return Object.keys(errors).map(errorKey => `${errorKey} : ${errors[errorKey]}`).join(', ');
  }
}

Verwendung

<input type="text" formControlName="customernumber2"/>
<ged-validation-error-marker 
    [control]="form.controls['customernumber2']">
</ged-validation-error-marker>

Bei großen Formularen kommt es bekanntlich auf jede Zeile an und zu Recht mag man bei der gerade gezeigten Variante bemängeln, dass der Name des FormControl hier doppelt angegeben werden muss. Zudem muss in Sachen Styling hier berücksichtigt werden das die Eingabekomponente und die Ausgabe der Fehlermeldungen zwei HTML-Komponenten sind. Überredet… eine Lösung für die angesprochenen Punkte, ohne die Flexibilität zu verlieren, könnte folgende Komponente sein:

@Component({
  selector: 'ged-input',
  templateUrl: './input.component.html',
  styleUrls: ['./input.component.scss']
})
export class InputComponent{
  @ContentChild(NgControl, { static: false })
  control: NgControl;
}
<div class="input">
  <ng-content></ng-content>
  <ged-validation-error-marker [control]="control"></ged-validation-error-marker>
</div>

Die Verwendung ist dann zum einen flexibel, weil wir beliebige Eingabekomponenten verwenden können, übersichtlich, da wir keinen eigenen Layout-Container um unsere Eingaben legen müssen und weniger Fehleranfällig, da der Name des FormControlls nur noch einmal angegeben werden muss und per ContetChild-Injection innerhalb der Komponente zur Verfügung gestellt wird und dazu noch beliebig erweiterbar, z.B. um die Ausgabe eines Labels:

Verwendung

    <ged-input>
      <input type="text" formControlName="customernumber4"/>
    </ged-input>

Zeit für einen Kaffee, also machen wir einen 2-Teiler daraus. Bis dahin:

Auf GitHub? Klar. https://github.com/GEDOPLAN/ng-input

Immer noch gesund? Änderungen von MicroProfile Health 2.0 vs. 1.0

In meinem Post https://javaeeblog.wordpress.com/2019/01/27/alles-gesund-health-checking-mit-microprofile-health habe ich einen Überblick über MicroProfile Health in der Version 1.0 gegeben. Mittlerweile ist die Version 2.0 erschienen (als Teil von MicroProfile 3.0), woduch einige – teilweise inkompatible – Änderungen einhergehen:

  • Die Prüfungen werden nun in Tests der Verfügbarkeit (Liveness) und Bereitschaft (Readiness) unterteilt, um so besser die Anforderungen von Container-Infrastrukturen zu unterstützen (vgl. entsprechende Probes in Kubernetes). Statt des bisherigen Qualifiers @Health werden nun @Live bzw. @Ready zur Definition von HealthCheck-Beans verwendet:
    @ApplicationScoped
    @Liveness
    public class LivenessCheck implements HealthCheck {
    
      @Override
      public HealthCheckResponse call() {
        return HealthCheckResponse
            .named("Service1")
            .up()
            .build();
    

    @Health steht noch zur Verfügung, ist aber deprecated.

  • Die neuen Endpunkte /health/live und /health/ready veröffentlichen den Zustand von Liveness bzw Readiness:
    GET /health/live
    
    200 OK
    {
      "status": "UP",
      "checks": [{
        "name": "Service1",
        "status": "UP"
      }]
    }
    

    Der bisherige Endpunkt /health existiert weiterhin.

  • Im JSON-Format wurden die Attribute outcome und state durch status ersetzt.

Ausblenden von Default-Checks in MicroProfile Health 2.1

Die Implementierung von Healthchecks als CDI-Beans ermöglicht es, dass auch Plattform-Implementierungen/Bibliotheken Prüfergebnisse bereitstellen (bspw. ein DB Connection Pool). Setzt man die neue MicroProfile Configuration Property mp.health.disable-default-procedures auf true, werden solche Default-Checks nicht aktiviert, d. h. im Response der o. a. Endpoints befinden sich nur von der Anwendung selbst bereitgestellte Werte.

Demo

Ich habe das Beispielprojekt mit anderen MicroProfile-Demos kombiniert in https://github.com/GEDOPLAN/microprofile-demo eingecheckt. Im Verzeichnis microprofile-health finden Sie ein Demoprojekt mit zwei Health Checks, deren Ergebnis über eine Webseite beeinflusst werden kann. Die Anwendung kann als WAR-File auf einen Server mit Unterstützung von MicroProfile 3.0 deployt werden. Alternativ können über vorkonfigurierte Maven-Profile Docker-Images zum direkten Ausprobieren erstellt werden.

Bis bald – vielleicht in einem unserer Trainings in Berlin, Bielefeld, Köln oder bei Ihnen!
https://gedoplan-it-training.de/

Desktop-App mit Angular + Electron, Teil 2

In einem älteren Beitrag haben wir uns das grundlegende Zusammenspiel von Electron und Angular angesehen, haben aber noch kaum Vorteile aus diesem Team gezogen. Es ist Zeit die Fesseln der Browser-Sandbox ab zu streifen.

Wir erinnern uns: Electron basiert auf der Idee dass das laufende Programm später aus zwei Prozessen besteht: dem Render-Prozess in dem unsere Angular Anwendung gans genauso ausgeführt wird wie im Browser und dem main-Prozess der nichts anderes ist als eine lokal laufende Node-Anwendung.

Diese lokal laufende Node-Anwendung ist ( um das Beispiel aus unserem letzten Beitrag auf zu greifen) die Datei electron.js, in der wir bisher lediglich die initialisierung des Fensters und einige grundlegende Lister konfiguriert haben. Da diese Datei nicht innerhalb der Browser-Sandbox ausgeführt wird, sondern als separater Node-Prozess, haben wir alle Freiheiten die eine Node-Anwendug mit sich bringt, so zum Beispiel das Lesen und Schreiben von Dateien:

const { app, BrowserWindow } = require("electron");
const url = require("url");
const path = require("path");
const fs = require("fs");
const storageFile = require("os").homedir() + "/electron-demo.json";

// ... electron listener...

function readFileContent() {
  if (fs.existsSync(storageFile)) {
    const content = fs.readFileSync(storageFile, "utf8");
    return JSON.parse(content);
  } else {
    return [];
  }
}

function writeFileContet(ele) {
  let elements = readFileContent();
  elements.push(arg);
  fs.writeFileSync(storageFile, JSON.stringify(elements));
}

Die Verknüpfung zwischen Angular- und Node-Anwendung stellt der Event-Emmiter ipcMain dar, der auf Basis von selbst definierbaren Event-Namen („appendStorageFileContent“) Parameter entgegen nehmen (arg) und reagieren kann ( .reply ):

const { ipcMain } = require("electron");
...
ipcMain.on("getStorageFileContent", (event, arg) => {  // Schritt 2
  const elements = readFileContent();
  event.reply("getStorageFileContent", elements);
});

ipcMain.on("appendStorageFileContent", (event, arg) => { // Schritt 2
  writeFileContent(arg);
  event.reply("appendStorageFileContent", true);
});

Auf Angular-Seite heißt das Gegenstück ipcRenderer und man erhält dieses Objekt, etwas Angular unüblich, über die Verwendung von require. der ipcRenderer hat nun diverse Methoden um mit dem Node-Prozess zu kommunizieren. Diese Kommunikation besteht in aller Regel aus zwei Teilen: wir registrieren uns auf ein Event um die Rückgaben vom main-Prozess zu erhalten. Dafür stehen unterschiedlliche Methoden zur Verfügung, unter anderem die „.once“-Methode, die es uns erlaubt einmalig auf ein Event zu warten. Neben dem Namen übergeben wir als zweiten Parameter eine Callback Methode die aufgerufen wird wenn der Node-Prozess ein Ergebiss liefert. Nun folgt lediglich noch der trigger für den main-Prozess, wir schicken also eine Nachricht mittels Angabe des Event-Namens an den main-Prozess.

(In unserem Beispiel verpacken wir das Ganze noch in ein Observable, die typische Angular-Schnittstelle für unsere Services)

@Injectable({
  providedIn: 'root'
})
export class DemoDesktopService extends DemoService {

  private ipc: any;

  constructor() {
    super();
    this.ipc = (window as any).require('electron').ipcRenderer;
  }

  public readAll(): Observable<any[]> {
    return new Observable(observer => {
      this.ipc.once('getStorageFileContent', (event, arg) => { // Schritt 3
        observer.next(arg);
        observer.complete();
      });

      this.ipc.send('getStorageFileContent'); // Schritt 1
    });
  }

  public write(obj: any): Observable<any> {
    return new Observable(observer => {
      this.ipc.once('appendStorageFileContent', (event, arg) => {// Schritt 3
        observer.next(arg);
        observer.complete();
      });

      this.ipc.send('appendStorageFileContent', obj); // Schritt 1
    });
  }
}

GitHub? Hier!

Desktop-App mit Angular + Electron, Teil 1

Webanwendungen sind dieser Tage nicht mehr wegzudenken aus dem Geschäftsleben. So manche Anwendung die früher ihr Dasein als Swing- oder JavaFX-Applikation gefristet hat, wurden mittels Webtechnologien neu eingekleidet. Trotzdem sind native oder hybride Ansätze immer dann interessant wenn die Anwendung mehr möchte als die Sandbox des Browser es zu lässt. Ein Ansatz der sich großer beliebtheit erfreut: wir schreiben eine Web-Anwendung und „packen sie ein“. Was Ionic für mobile-Apps ist, stellt electron für die Desktop-Welt da. Zeit für einen kurzen Überblick.

Wir starten wie wir es von Angular gewohnt sind. Mittels Angular-CLI erzeugen wir ein neues Projekt, entwickeln Komponenten und Services und verwenden den Development-Server um das Ganze an den Start zu bringen und im Browser an zu zeigen / zu debuggen / zu testen.

Für die Verwendung von electron installieren wir nun zwei weitere Abhänigkeiten:

npm install --save-dev electron
npm install --save-dev electron-packager

Die Grundidee hinter electron, die wir uns bewusst machen sollten ist einfach ( und genial ): wir haben es später mit zwei Prozessen zu tun: zum einen der Renderer-Instanz die quasi unseren Browser darstellt und innerhalb dem unsere Angular-Anwendung läuft . Diese hat die „normalen“ Einschränkungen wie jeder Browser ( kein Zugriff aufs Betriebssystem ). Der Zweite Prozess mit dem die Angular Anwendung per IPC (Interprozesskommunikation) kommunizieren kann ist eine Node-Anwendung, welche alle „Freiheiten“ auf dem System hat.

Also los. Wir legen zwei neue Dateien an (in einem beliegen Ordner, meine Wahl: im Root des Verzeichnisses im Ordner ‚electron‘). Eine package.json für unsere electron „Projekt“. Abgesehen von den Abhänigkeiten handelt es sich dabei bei mir nahezu um eine Kopie der package.json meiner Angular Anwendung und enthält lediglich einige Basis Infos + die Angabe meiner „main“-JS:

{
  "name": "ng-gedoplan-electron",
  "version": "1.0.0",
  "main": "electron.js",
  "description": "Start-File für Electron-Anwendung",
  "author": {
    "name": "Dominik Mathmann, GEDOPLAN"
  }
}

Als zweiten Schritt legen wir diese main-JS an, electron.js:

const { app, BrowserWindow, ipcMain } = require("electron");
const url = require("url");
const path = require("path");

let mainWindow;

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 600,
    height: 600,
    webPreferences: {
      nodeIntegration: true
    }
  });

  mainWindow.loadURL(
    url.format({
      pathname: path.join(__dirname, `index.html`),
      protocol: "file:",
      slashes: true
    })
  );
  // Open the DevTools.
  // mainWindow.webContents.openDevTools();
  mainWindow.setMenu(null);

  mainWindow.on("closed", function() {
    mainWindow = null;
  });
}

app.on("ready", createWindow);

app.on("window-all-closed", function() {
  if (process.platform !== "darwin") app.quit();
});

app.on("activate", function() {
  if (mainWindow === null) createWindow();
});

Hier steuern wir unsere electron App: unser „node-backend“. Das ist in unserem Beispiel sehr überschaubar und enthält leiglich einige Standard-Deklarationen. Da wir bisher keinerlei Interaktion mit dem Betriebssystem haben, kümmert sich das Script also lediglich mittels ein paar Listener um die Erzeugung eines Fenster und reagiert auf entsprechende Fenster-Events.

Das war es schon fast. Auf einen Fehler stoßen wir derzeit mit Angular 8, und zwar das die Scripte aus Security-Gründen nicht geladen werden können wenn das neue Modul-System von JS verwendet wird. Aktueller Workaround: Ziel-JS Version in der Angular-Anwendung tsconfig.json umstellen: „target“: „es5“, Nun müssen wir nur noch dafür sorgen das unsere Angular-App und electron zusammen finden. Das ist einfach: nach dem kompilieren von Angular braucht electron lediglich noch die beiden erzeugten Dateien um zu starten, die wir ihm mittels script in den Ziel-Ordner kopieren ( in meinem Beispiel mittels OS-unabhängigen copyfiles-npm)

"start-electron": 
   "ng build --base-href ./ 
    && copyfiles -u 1 electron/* dist/ng-gedoplan-electron 
    && electron dist/ng-gedoplan-electron"

(anstatt electron) können wir auch stattdessen den Packager verwenden:

 electron-packager dist/ng-gedoplan-electron --platform=win32 --out=dist

um in diesem Beispiel eine Windows-Anwendung zu generieren.

Das war es schon. Zugegeben aktuell haben wir kaum einen Vorteil unsere Anwendung in eine Desktop-App zu verpacken, den spannenden Teil: Zugriff auf das Betriebssystem schauen wir uns erst im nächsten Beitrag an :-). Trotzdem schon mal: hier. GitHub.

Angular Testing CodeCoverage

Wer Unit-Tests für seine Anwendungen schreibt ( also jeder von uns 😉 ) wird kaum in der Lage sein ohne Tooling zu Überblicken welche Teile der Software getestet wurden und welche nicht. Angular macht es uns da zum Glück sehr leicht. Dank einer simplen Option in der angular.json Konfiguration:

  ...
"test": {
          "builder": "@angular-devkit/build-angular:karma",
          "options": {
            ...
            "codeCoverage": true
          }
        },
...

Dank dieser wird bei jedem Test-Durchlauf ein neuer ein Bericht generiert der als HTML-Seite abgerufen werden kann:

[project-root]/coverage/index.html

Neben der groben Übersicht welche Pfade der Anwendung mit welcher Test-Abdeckung versehen sind lässt sich hier bequem bis auf Datei-Ebene navigieren um so die Stellen zu erkennen die möglicherweise noch nicht ausreichen mit Unit-Tests abgedeckt sind: