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
});
}
}