Write once, run everywhere

Einfache Desktopanwendungen mit Electron und React erstellen
2 Kommentare

Eine Desktopapplikation für ein bestimmtes Betriebssystem zu schreiben, ist heutzutage relativ einfach. Für die gängigen Betriebssysteme sind zahlreiche Entwicklungsumgebungen und Bibliotheken verfügbar, mit denen mit wenig Aufwand Anwendungen mit grafischer Oberfläche erstellt werden können. Allerdings sind die so erstellten Programme nicht plattformunabhängig. Hier setzt Electron als vielversprechende Lösung an.

Plattformunabhängige Desktopanwendungen zu schaffen, ist nicht wirklich ein Kinderspiel. Eine mit Visual Studio erstellte Windows-Applikation lässt sich unter Linux etwa nicht verwenden. Seit langer Zeit werden daher Möglichkeiten gesucht, eine Anwendung einmalig zu implementieren (write once), und sie dann ohne wesentlichen Zusatzaufwand für verschiedene Betriebssysteme bereitstellen zu können (run everywhere). Electron ist ein weiterer Versuch, dieses Problem zu lösen. In diesem Artikel sehen wir uns anhand eines einfachen Beispiels an, wie eine Anwendung mit Electron und React implementiert und für verschiedene Betriebssysteme bereitgestellt werden kann.

Plattformunabhängige Programmierung

Seit Computer in Textform programmiert werden, besteht aus Sicht des Programmierers der Wunsch, ein Programm, das einmalig für einen bestimmten Computer geschrieben wurde, auch auf einem anderen Computer ausführen zu können. Der Grund liegt auf der Hand: Ist der Algorithmus für ein bestimmtes Problem korrekt formuliert, sollte es reichen, diese Arbeit einmalig durchzuführen, um das Programm in verschiedenen Umgebungen lauffähig zu machen. Zur Anfangszeit der Programmierung, als nur die maschinennahe Programmierung (Maschinensprache oder Assembler) verfügbar war, war das nicht gegeben, da diese Programmiertechniken es nicht erlaubten, eine Problemlösung bzw. einen Algorithmus unabhängig von der Ausführungsumgebung, der konkreten Maschine, zu formulieren. Die Lösung für dieses Problem war die Einführung der höheren Programmiersprachen. Diese erlaubten es, Algorithmen weitgehend unabhängig von der konkreten Ausführungsumgebung zu formulieren. Um die Programme ausführbar zu machen, wurden Übersetzerprogramme, Compiler und Interpreter eingeführt.

Eine typische Compilersprache ist etwa C. C wurde ursprünglich entwickelt, um Betriebssysteme schreiben zu können, die auf verschiedenen Computerarchitekturen lauffähig sein sollten. Ein C-Compiler übersetzt dabei den maschinenunabhängigen C-Code in für die konkrete Maschine ausführbaren Maschinencode. Soll das Programm für eine weitere Maschine verfügbar gemacht werden, muss es vorher mit dem Compiler für diese Maschine neu übersetzt werden. Die Übersetzung geschieht also, bevor das Programm ausgeführt wird.

JavaScript Days 2019

JavaScript Testing in der Praxis (Teil 1 + 2)

mit Dominik Ehrenberg (Crosscan) und Sebastian Springer (MaibornWolff)

Fortgeschrittene schwarze Magie in TypeScript

mit Peter Kröner (‚Webtechnologie-Erklärbär‘)


Interpreter arbeiten anders. Ein Interpreter ist ein Programm, das auf der Zielmaschine läuft und maschinenunabhängigen Code zur Laufzeit in Maschinencode übersetzt. Der Quellcode für den Interpreter kann dabei Hochsprachencode sein (wie etwa in BASIC oder Skriptsprachen wie Python und Perl), oder bereits vorkompilierter Code (wie im Fall der Java Virtual Machine, die Java-Byte-Code ausführt, der vorher mit einem Compiler erzeugt wurde). In einer Interpretersprache geschriebener Code funktioniert prinzipiell auf allen Maschinen, auf denen ein entsprechender Interpreter zur Verfügung steht (sofern die Sprache und die Interpreter standardisiert sind).

Wenn heutzutage von Plattformen gesprochen wird, so sind damit nicht nur konkrete Maschinenarchitekturen gemeint, sondern Klassen von Umgebungen, die neben Hardwareeigenschaften auch Software teilen. In den meisten Fällen wird eine Plattform dabei durch die verwendete Hardware und das dazugehörige Betriebssystem gekennzeichnet. Entsprechend bedeutet plattformunabhängige Programmierung in diesem Zusammenhang die Bereitstellung der Software für verschiedene Betriebssysteme. Für den Zugriff auf spezielle Funktionalität, wie etwa GUI-Elemente, wird dabei dann meist noch eine RTE (Runtime Environment) benötigt, die diese Funktionen für die jeweilige Plattform bereitstellt. Bei Code, der in einer Interpretersprache bereitgestellt wird, gehört zur RTE unter Umständen auch der Interpreter.

Electron-Grundlagen

Programmiert man mit HTML und JavaScript Anwendungen für den Browser, ist der Browser selbst die Ausführungsumgebung. Da es für die meisten Betriebssysteme Browser gibt, die weitgehend kompatibel sind, stellt die Bereitstellung von Anwendungen auf Basis dieser Technik keine Schwierigkeit dar. Die Anwendungen sind automatisch betriebssystemübergreifend lauffähig. Das ist einer der Gründe, warum Webtechnik für das Schreiben von Anwendungen attraktiv ist.

Allerdings hat die Ausführung im Browser auch Nachteile. Anwender von Desktopbetriebssystemen sind es gewohnt, spezialisierte Applikationen für bestimmte Zwecke verwenden zu können. Gegenüber der Benutzung von Browseranwendungen gibt es unter anderem folgende Unterschiede:

  • Die Anwendungen können unabhängig voneinander und vom Browser gestartet, beendet und positioniert werden.
  • Anwendungen können eigene Eigenschaften haben, wie etwa eine feste Fenstergröße.
  • Ein Wechsel zwischen den Anwendungen erfolgt über die Mittel des Betriebssystems – etwa über ALT + TAB oder die Taskleiste unter Windows.
  • Die Anwendungen laufen als eigenständige Betriebssystemprozesse und können im Fehlerfall auch isoliert beendet werden.

Die Arbeit mit eigenen Desktopanwendungen gestaltet sich dabei für die Anwender aufgrund der genannten Punkte deutlich handlicher. Außerdem kann der Browser dafür verwendet werden, für das er in in vielen Fällen noch hauptsächlich benutzt wird – zum Navigieren und Recherchieren im WWW – und wird nicht durch die Darstellung anderer Anwendungen blockiert.

Electron ermöglicht es nun, Anwendungen auf Basis von Webtechnik als Desktopanwendungen bereitzustellen. Die Technik ist mittlerweile sehr ausgereift, so basieren etwa der Editor Atom und Microsofts Visual Studio Code darauf. Als Grundlagen werden Node.js und Chromium verwendet.

Um eine Electron-Anwendung zu erzeugen, muss zuvor eine Webanwendung vorliegen, die dann mit einem speziellen Verarbeitungsschritt in eine Desktopanwendung überführt wird. Das ähnelt äußerlich der Verwendung eines Compilers, funktioniert aber etwas anders. JavaScript-Code, HTML-Code und ein Chromium-Browser werden dazu so zusammen gebündelt, dass eine ausführbare Datei entsteht, die wie alle anderen Desktopanwendungen gestartet werden kann. Beim Start wird dann zunächst die Laufzeitumgebung (die Browserkomponente) innerhalb eines nativen Betriebssystemfensters gestartet, und in dieser die Webanwendung ausgeführt. Das bedeutet:

  • Der Rahmen des Anwendungsfensters wird durch das Betriebssystem bzw. durch den verwendeten Windowmanager vorgegeben, ebenso die dazugehörigen Controls zum Schließen des Fensters etc.
  • Der Inhalt des Anwendungsfensters wird durch den in JavaScript, HTML und CSS implementierten Anwendungscode festgelegt. Er ist weitestgehend unabhängig vom verwendeten Betriebssystem.

Wir werden jetzt an einem Beispiel zeigen, wie die Überführung einer Webanwendung in eine Electron-Desktopanwendung konkret durchgeführt werden kann.

Fallbeispiel

Um das Beispiel einfach zu halten, setzen wir eine sehr einfache Anwendung, einen Teetimer, wie es ihn in vielen Haushalten als kleinen Kurzzeitmesser gibt. Er verfügt über

  • ein Digitaldisplay, das die verbleibende Zeit im Format mm:ss anzeigt, d. h. jeweils zwei Stellen für Minuten und Sekunden,
  • zwei Knöpfe zum Erhöhen der eingestellten Zeit, einen für die Minuten (Min), einen für die Sekunden (Sec),
  • einen Knopf, der den Countdown startet oder beendet (Start/Stop), sowie
  • einen Knopf (Reset) zum Zurücksetzen der eingestellten Zeit.
    Abb. 1: Mock-up der Anwendung

    Abb. 1: Mock-up der Anwendung

Wir verwenden für die Entwicklung unserer Anwendung React und nennen sie daher „ReacTeaTimer“. Abb. 1 zeigt ein Mock-up der zu entwickelnden Anmeldung.

Implementierung

Unsere Anwendung soll wie gesagt auf React basieren. Wir beginnen also damit, dass wir ein React-Projekt anlegen:

mkdir sandbox
cd sandbox
create-react-app react-t-timer-app

Als Entwicklungsmaschine verwenden wir einen Linux-Rechner unter Ubuntu 16.04. Wird unter einem anderen Betriebssystem gearbeitet, sind evtl. Anpassungen notwendig, so muss ggf. der Befehl sudo vor Kommandos weggelassen werden. Anschließend wechseln wir in das neu erstellte Verzeichnis und löschen den Inhalt des src-Verzeichnisses:

cd react-t-timer-app
rm -rf src/*

Das Skelett unseres Projekts ist damit vorbereitet. Jetzt müssen wir es mit Inhalt versehen. Listing 1 enthält den JavaScript-Quellcode unserer Anwendung.

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

class ReacTeaTimer extends React.Component {
  componentDidMount(){
    document.title = "ReacTeaTimer"
  }

  render() {
    return (
  <div className="outer_frame">
    <div className="display_frame">
      <div className="timer_display">
        {this.state.time}
      </div>
    </div>
    <div className="key_panel">
      <div className="min_sec">
        <button className="lean_button" onClick={() => this.incMins()}>
          Min
        </button>
        <button className="lean_button" onClick={() => this.incSecs()}>
          Sec
        </button>
      </div>
      <button className="wide_button" onClick={() => this.startStopTimer()}>
        Start/Stop
      </button>
      <button className="wide_button" onClick={() => this.reset()}>
        Reset
      </button>
    </div>
  </div>
    );
  }

  constructor(props) {
    super(props);
    this.state = {
      time: "05:00",
      lastTime: "05:00",
      timerRunning: 0,
      alarmRunning: 0
    };
    this.ac = new AudioContext()
  }

  updateModel(event) {
    this.setState({});
  }

  parseTime() {
    const [minStr, secsStr] = this.state.time.split(":");
    const mins = parseInt(minStr, 10);
    const secs = parseInt(secsStr, 10);
    return [mins, secs];
  }

  incMins() {
    let [mins, secs] = this.parseTime();
    mins += 1;
    const time = this.padZeros(mins) + ":" + this.padZeros(secs);
    this.setState({time: time, lastTime: time});
  }

  incSecs() {
    let [mins, secs] = this.parseTime();
    secs = (secs + 1) % 60;
    const time = this.padZeros(mins) + ":" + this.padZeros(secs);
    this.setState({time: time, lastTime: time});
  }

  reset() {
    this.setState({time: "00:00", lastTime: "00:00", timer: 0});
    if (this.state.timerRunning === 1) {
      this.setState({timerRunning: 0});
      clearInterval(this.state.timer);
    }
  }

  decTimer() {
    let [mins, secs] = this.parseTime();
    if (secs > 0) {
      secs -= 1;
    }
    else if (secs === 0) {
      if (mins > 0) {
        secs = 59;
        mins -= 1;
      }
      else {
        this.stopTimer();
  this.beepOneMinute();
      }
    }
    const time = this.padZeros(mins) + ":" + this.padZeros(secs);
    this.setState({time: time});
  }

  startTimer() {
      this.setState({timerRunning: 1});
      this.setState({timer: setInterval(this.decTimer.bind(this), 1000)});
  }

  stopTimer() {
      clearInterval(this.state.timer);
      this.setState({timerRunning: 0});
  }

  startStopTimer() {
    if (this.state.alarmRunning) {
      this.setState({alarmRunning: 0, time: this.state.lastTime});
    }
    else {
      if (this.state.timerRunning === 0) {
        this.startTimer();
      }
      else {
        this.stopTimer();
      }
    }
  }

  padZeros(number) {
    let result = String(number);
    result = result.padStart(2, "0");
    return result;
  }

  beepOneMinute() {
    function beep(){
      let v = this.ac.createOscillator();
      let u = this.ac.createGain();
      v.connect(u);
      v.frequency.value = 1760;
      v.type = "square";
      u.connect(this.ac.destination);
      u.gain.value = 0.5;
      v.start(this.ac.currentTime);
      v.stop(this.ac.currentTime + 0.2);
    }

    let interval = undefined;
    let startTime = new Date().getTime();
    this.setState({alarmRunning: 1});

    function beepBody() {
      beep.bind(this)();
      let currTime = new Date().getTime();

      if ((currTime - startTime > 60000) || (this.state.alarmRunning === 0)) {
        clearInterval(interval);
      }
    }
    interval = setInterval(beepBody.bind(this), 400);
  }
}

ReactDOM.render(
  <ReacTeaTimer/>,
  document.getElementById('root')
);

Auf die import-Anweisungen für die React-Bibliotheken und das Stylesheet folgt die Deklaration der Klasse ReacTeaTimer, die die React-Komponente für die Darstellung des Timers implementiert. Die Methode componentDidMount() bewirkt, dass bei der Initialisierung der Komponente der Titel als „ReacTeaTimer“ gesetzt wird. Die render()-Methode gibt, wie in React üblich, die anzuzeigende Seite in React-Syntax zurück. Sie besteht aus:

  • einem Rahmen für die gesamte Anwendung (outer_frame),
  • einem darin enthaltenen Element für den Rahmen um das Display (display_frame),
  • einem wiederum in display_frame enthaltenen Element für das eigentliche Display (timer_display),
  • parallel zu display_frame einem Element für die Knöpfe (key_panel),
  • darin in einem speziellen Element min_sec button-Elemente für die Eingabe von Minute („Min“) und Sekunde „Sec“), sowie
  • parallel zu dem min_sec-Knoten zwei Buttons „Start/Stop“ und „Reset“.

Für die einzelnen Buttons sind dabei jeweils über das onClick-Attribut eigene Callback-Funktionen definiert. Im Übrigen definiert die render()-Methode hier im Wesentlichen nur die Struktur der Seite. Das Layout wird durch das weiter unten beschriebene Stylesheet umgesetzt. Darüber hinaus gilt Folgendes:

  • Die Methode constructor() setzt die aktuell anzuzeigende Zeit time und die zuletzt eingestellte Zeit lastTime jeweils auf fünf Minuten und null Sekunden.
  • parseTime() ist eine Hilfsmethode, die aus der internen Zeitdarstellung im Format „mm:ss“ als String die Anzahl der verbleibenden Minuten und Sekunden als Zahlen ermittelt, und diese als Paar (Minute, Sekunde) zurückgibt.
  • incMins() ist die Callback-Funktion für den Minutenbutton, und erhöht die Minuten jeweils um 1.
  • Analog ist incSecs() die Callback-Funktion zur Erhöhung der Sekunden, mit dem Unterschied, dass bei einem Übertrag (wenn 60 Sekunden erreicht werden) außerdem die Minuten um 1 erhöht werden.
  • Die Methode reset() stellt den Initialzustand wieder her und deaktiviert gleichzeitig die Intervallfunktion zum Herunterzählen.
  • decTimer() dient dem Herunterzählen des Timers um jeweils eine Sekunde, dabei werden jeweils die Sekunden und bei einem Übertrag ggf. auch die Minute um 1 verringert. Ist der Timer abgelaufen, wird er angehalten, und die Methode beepOneMinute() aufgerufen, um ein akustisches Signal wiederzugeben.
  • startTimer() setzt den Timer in Gang. Dazu wird alle tausend Millisekunden über einen Intervalltimer timer die Methode decTimer() aufgerufen.
  • stopTimer() hält den Timer wieder an, indem der Intervalltimer wieder gelöscht wird.
  • Die Hilfsmethode padZeros() verwandelt einen numerischen Wert für die Anzahl von Minuten oder Sekunden in einen mit führenden Nullen aufgefüllten String der Länge 2.

Die Methode beepOneMinute() enthält zwei innere Funktionen. beep() erzeugt einfach einen Ton, der 0,2 Sekunden lang ist. beepBody() prüft bei jedem Aufruf, ob seit dem ersten Aufruf bereits eine Minute (60 000 Millisekunden) vergangen sind. Ist das der Fall, wird der Intervalltimer für die Ausgabe des Signals über beep() gelöscht. Am Ende von beepOneMinute() wird ein Intervalltimer definiert, der beepBody() alle 400 Millisekunden startet. Zu guter Letzt bewirkt der Aufruf von ReactDOM.render(), dass die ReacTeaTimer-Komponente unterhalb des root-Knotens in das DOM eingehängt wird. Wir speichern die Datei als index.js im Ordner src.

Listing 2 enthält das dazugehörige Stylesheet. Wir gehen hier nicht im Einzelnen darauf ein. Zu erwähnen ist aber, dass hier mit festen Größen gearbeitet wird, so ist etwa die Höhe des body-Elements fix auf 260 Pixel gesetzt. Grund ist, dass unsere spätere Electron-Anwendung der Einfachheit halber ebenfalls eine feste Fenstergröße erhalten soll.

html,body {
  height: 260px;
  margin: 0px;
  padding: 0px;
  overflow: hidden;
}

.outer_frame {
  width: 200px;
  height: 280px;
  background-color: lightgray;
  display: grid;
  grid-template-rows: auto auto;
  justify-items: center;
}

.display_frame {
  width: 160px;
  height: 50px;
  color: black;
  border: 2px solid;
  text-align: center;
  margin: auto;
  display: grid;
  grid-template-rows: auto;
}

.timer_display {
  font-size: 40px;
  background-color: white;
}

.key_panel {
  width: 160px;
  height: 160px;
  display: grid;
  grid-gap: 10px;
  grid-template-rows: auto auto auto;
}

.min_sec {
  display: grid;
  grid-template-columns: auto auto;
  grid-gap: 10px;
}

.lean_button {
  font-size: 20px;
}

.wide_button {
  font-size: 20px;
}

Diese Datei speichern wir im Ordner src unter dem Namen index.css. Damit ist die Implementierung der eigentlichen Anwendung abgeschlossen. Wir können sie jetzt im Browser testen: npm start. Daraufhin öffnet sich im Browser ein Reiter mit der Adresse http://localhost:3000/, der die Anwendung anzeigt. Abbildung 2 zeigt einen Screenshot der laufenden Anwendung in Firefox.

Abb. 2: Die Anwendung im Browser

Abb. 2: Die Anwendung im Browser

Electronifizieren

Nachdem die Browseranwendung implementiert ist, ist der nächste Schritt, sie in eine Electron-Anwendung umzuwandeln. Als Erstes erzeugen wir aus der Serveranwendung einen statisch optimierten Build. Dazu passen wir zunächst den Beginn der Datei package.json an:

{
  "name": "react-t-timer-app",
  "version": "0.1.0",
  ...

und ergänzen ein Attribut homepage, so dass die Datei jetzt so anfängt:

{
  "name": "react-t-timer-app",
  "homepage": ".",
  "version": "0.1.0",
    ...

Das bewirkt, dass Pfade in Verweisen als relativ zur im Rahmen des Builds generierten Datei index.html erzeugt werden. Ohne diese Konfiguration würden Pfade relativ zum Serverwurzelverzeichnis generiert, was wir in diesem Fall nicht wollen, weil wir einen statischen Build erzeugen. Das geschieht mit npm run build.

Im Ergebnis befindet sich im Unterverzeichnis build ein optimierter Build der Anwendung. Um unsere Anwendung zu verteilen, müssten wir jetzt lediglich dieses Verzeichnis samt Inhalt weitergeben. Auf einem anderen Rechner müsste dieses Verzeichnis dann irgendwohin kopiert werden, und über das Öffnen der enthaltenen index.html mit einem JavaScript-fähigen Browser könnte die Anwendung dort ohne Änderung ausgeführt werden. Wir haben damit also schon mal eine plattformunabhängige Browseranwendung implementiert.

Was wir aber erreichen wollen, ist eine Anwendung, die sich in der Zielplattform als eigenes Fenster unabhängig vom Browser darstellt. Dazu müssen wir zunächst die Electron-Infrastruktur installieren. Das geschieht mit npm install –save-dev electron im vorher von create-react-app angelegten Verzeichnis react-t-timer-app.

Anschließend kopieren wir den Inhalt der Datei main.js von https://github.com/electron/electron-quick-start/blob/master/main.js in eine gleichnamige Datei unterhalb von src (Listing 3). Dann passen wir die Funktion createWindow() an, indem wir Breite (width) und Höhe (height) an den Inhalt unserer CSS-Datei anpassen, und die Fenstergröße fix setzen (resizable: false). Der Wert true für useContentSize bewirkt, dass die Fenstergröße so berechnet wird, dass die enthaltene Webseite in den inneren Bereich des Fensters (ohne den Rahmen) passt. Die tatsächliche Fenstergröße ist dann etwas größer, weil noch die Maße für den Fensterrahmen hinzukommen. Außerdem setzen wir das Attribut show auf false, was bewirkt, dass das Fenster erst einmal nicht angezeigt wird. Damit vermeiden wir, dass die Anzeige flackert, weil der Fensterrahmen vor dem kompletten Laden des Inhalts dargestellt wird.

function createWindow () {
  // Create the browser window.
  mainWindow = new BrowserWindow({
    width: 200,
    height: 260,
    resizable: false,
    useContentSize: true,
    webPreferences: {
      nodeIntegration: true
    },
    show: false
  })

Damit das Fenster nach dem Laden des Inhalts sichtbar wird, fügen wir einen Event Handler ein, der bewirkt, dass nach dem Empfangen des Events ready-to-show das Fenster gezeigt wird.

mainWindow.once('ready-to-show', () => {
  mainWindow.show()
})

Außerdem ändern wir die Zeile mainWindow.loadFile(‚index.html‘) um in mainWindow.loadFile(‚build/index.html‘).

Jetzt müssen wir noch die Datei package.json anpassen. Wir fügen unterhalb von dependencies ein Attribut main ein, das den Einstiegspunkt für die Electron-Anwendung definiert, und ergänzen mit Electron ein Skript zum Starten von Electron:

"main": "src/main.js",
"scripts": {
  "start": "react-scripts start",
  "build": "react-scripts build",
  "test": "react-scripts test",
  "eject": "react-scripts eject",
  "electron": "electron ."
},

Damit sind die Vorbereitungen abgeschlossen, und wir können die Anwendung mit npm run electron starten.

Abbildung 3 zeigt das Anwendungsfenster (auf einem Ubuntu-System mit

Abb. 3: Über npm gestartete Electron-Anwendung

Abb. 3: Über npm gestartete Electron-Anwendung

Gnome). Der Rahmen des Fensters hat eine feste, unveränderliche Größe, für Gnome typische Menüs und die ebenfalls für Gnome üblichen Controls zum Schließen und Minimieren der Anwendung. Als Fenstertitel wird der Dokumenttitel „ReacTeaTimer“ angezeigt. Die Anwendung kann über das Fenster-Control, die Tastenkombination ALT + F5 oder über Betätigung von CTRL + C in der npm-Kommandozeile wieder geschlossen werden.

 

Es fehlen allerdings noch Darstellungen: in der Taskbar (hier wird nur ein generisches Icon und der Name der Anwendung dargestellt), sowie in der Fensterübersicht, die man bei Betätigung von ALT + TAB zum Wechseln der Anwendung sieht. Dort erscheint nur ein Verbotsschild (Abb. 4).

Abb. 4: Verbotsschild in der Vorschau beim Fensterwechsel

Abb. 4: Verbotsschild in der Vorschau beim Fensterwechsel

Aus diesem Grund legen wir unterhalb des Ordners react-t-timer-app folgende Struktur an

<assets/
  icons/
    mac/
    win/
    png/

In den Unterordnern mac, win und png sind die Icondateien für die verschiedenen Betriebssysteme abzulegen:

  • mac: (für Apple) im Format .icns
  • win: (für Windows) im Format .ico
  • png: (für Linux) im Format .png

Entsprechend speichern wir im Ordner png ein Bild für die Anzeige (teatimer.png). Damit die Anzeige der Datei unter Linux funktioniert, müssen wir noch eine Anpassung in main.js vornehmen (Listing 4).

...
var path = require('path')
function createWindow () {
  // Create the browser window.
  mainWindow = new BrowserWindow({
    width: 200,
    ...
    show: false
    icon: path.join(__dirname, '../assets/icons/png/teatimer.png')
  })

Wir importieren also zusätzlich die Bibliothek path und setzen das Attribut icon in dem Dictionary, das der Funktion createWindow() übergeben wird, explizit auf den relativen Pfad zu dieser Datei.

Bereitstellung der Anwendung

Um jetzt ein natives Paket zu erzeugen, müssen wir ein weiteres Werkzeug installieren. In diesem Fall verwenden wir electron-packager. Dazu rufen wir aus dem Verzeichnis react-t-timer-app sudo npm install electron-packager –save-dev -g auf. Nach der Installation können wir nun über die Kommandozeile ein Executable erzeugen:

electron-packager . ReacTeaTimer --overwrite --platform=linux --arch=x64 --prune=true --out=release-builds
Abb. 5: Ergebnis ReacTeaTimer

Abb. 5: Ergebnis ReacTeaTimer

Wir können jetzt in das Verzeichnis release-builds/ReacTeaTimer-linux-x64/ wechseln und das Executable mit ./ReacTeaTimer starten. Das Ergebnis ist in Abbildung 5 zu sehen. Die Anwendung wird ohne die Menüleiste aus dem npm-Lauf dargestellt.

Wie Abbildung 6 zeigt, wird jetzt auch beim Fensterwechsel das richtige Vorschaubild angezeigt.

Abb. 6: Vorschau beim Fensterwechsel

Abb. 6: Vorschau beim Fensterwechsel

Für Windows kann ein Build mit diesem Kommando angestoßen werden:

electron-packager . ReacTeaTimer --overwrite --asar --platform=win32 
--arch=ia32 --icon=assets/icons/win/teatimer.ico 
--prune=true --out=release-builds

Kopiert man das Verzeichnis release-builds/ReacTeaTimer-win32-ia32 auf eine Windows-Maschine, kann das Programm durch Doppelklick auf ReacTeaTimer.exe gestartet werden. Abbildung 7 zeigt die laufende Windows-Anwendung. Das Fenster hat jetzt einen für Windows typischen Rahmen mit den entsprechenden Controls. Der Inhalt ist dagegen identisch mit der Darstellung unter Linux.

Abb. 7: Windows Executable in Ausführung

Abb. 7: Windows Executable in Ausführung

Platzbedarf

Die fertigen Builds sind auch für heutige Verhältnisse sehr groß. Die Gesamtgröße der Verzeichnisse beträgt unter Linux (64 bit) 448 MB und unter Windows (32 bit) 351 MB. Grund für diesen großen Platzbedarf ist unter anderem, dass jedes Mal ein kompletter Chromium-Browser in das Paket mit aufgenommen wird. Trotzdem können wir hier relativ einfach einiges einsparen, indem wir in packages.json im Bereich dependencies die folgenden Zeilen löschen:

 "react": "^16.8.4",
  "react-dom": "^16.8.4",
  "react-scripts": "2.1.8"

und stattdessen unter devDependencies einfügen:

"devDependencies": {
  "react": "^16.8.4",
  "react-dom": "^16.8.4",
  "react-scripts": "2.1.8",    
  "electron": "^4.1.0"
}

Damit verringert sich der Platzbedarf zumindest auf 221 MB (Linux, 64 bit), gezippt 73 MB, bzw. 141 MB (Windows, 32 bit) gezippt 57 MB. Das ist zwar immer noch viel, aber zumindest wird unsere Anwendung dadurch halbwegs transportabel.

Zusammenfassung

Hier folgt nochmal eine kurze Übersicht, welche Schritte wir durchgeführt haben, um eine Electron-Anwendung auf React-Basis zu implementieren (es handelt sich nur um einen möglichen Weg, hier gilt: Viele Wege führen nach Rom!):

  1. Neues React-Projekt mit create-react-app anlegen:
    create-react-app <Name der Applikation>
  2. Im src-Ordner des React-Projekts Code für eine Browseranwendung schreiben
  3. Im React-Project Electron installieren:
    npm install --save-dev electron
  4. Die Anwendung mit npm testen:npm run build && npm run electron
  5. main.js in das Projekt kopieren und Funktion createWindow() anpassen
  6. Die Anwendung mit npm bauen:
    npm run build
  7. Vor dem Build: Icons für die verschiedenen Zielsysteme bereitstellen
  8. Ggf. Electron-Packager installieren:npm install electron-packager –save-dev -g
  9. Ggf. packages.json umstrukturieren und Abhängigkeiten von dependencies nach devDependencies verschieben
  10. Kommandozeile für das betreffende Zielsystem aufrufen, etwa für Linux:electron-packager . ReacTeaTimer --overwrite --platform=linux --arch=x64 --prune=true --out=release-builds

Fazit

Mit Electron ist es mit nicht allzu viel Aufwand möglich, auf React-Basis implementierte Browseranwendungen in eigenständige Anwendungen für verschiedene Desktopbetriebssysteme zu überführen. Im Ergebnis kann aus ein und demselben Quellcode Software für verschiedene Plattformen generiert werden. Kleinere Anpassungen müssen für die verschiedenen Betriebssysteme dennoch gemacht werden, etwa bei der Bereitstellung von Icons in verschiedenen Formaten. Das ist aber ein bekanntes und weitgehend akzeptiertes Problem bei der Cross-Platform-Entwicklung und der Aufwand dafür in den meisten Fällen relativ überschaubar. Die entstehenden Anwendungen behalten innerhalb des Fensters Aussehen und Verhalten. Die einbettenden Fenster dagegen werden in Aussehen und Verhalten durch das jeweilige Betriebssystem bestimmt. Die entstehenden Anwendungen sind bisher sehr groß, die Technik ist also eher nicht geeignet, kleine Hilfsapplikationen bereitzustellen.

Das Thema hat viele Facetten, und vieles konnte in diesem Artikel nur angerissen werden. Interessante, weiterführende Themen sind etwa die folgenden:

  • electron-forge: Dieses Werkzeug arbeitet ähnlich wie electron-packager, erzeugt aber auch gleich Installer für die gebaute Software.
  • Feintuning: Durch Verwendung eines entsprechendes Boilerplates wie electron-react kann der Platzbedarf weiter reduziert werden.
  • Verwendung veränderbarer Fenstergrößen/Responsiveness: Anpassung der Anwendung, sodass sie in der Größe verändert werden kann.

Interessant ist auch, ob es in Zukunft die Möglichkeit geben wird, dass sich mehrere Electron-Applikationen eine Runtime-Umgebung teilen (d. h. die Chromium-Komponenten und weitere Bibliotheken). Das würde den Platzbedarf und die Ladezeiten sehr verringern.

Auf jeden Fall wünsche ich interessierten Lesern viel Spaß bei eigenen Projekten mit Electron – die Hürden sind, wie wir gesehen haben, nicht sehr hoch, und eigenen Experimenten stehen daher kaum Hindernisse im Weg.

PHP Magazin

Entwickler MagazinDieser Artikel ist im PHP Magazin erschienen. Das PHP Magazin deckt ein breites Spektrum an Themen ab, die für die erfolgreiche Webentwicklung unerlässlich sind.

Natürlich können Sie das PHP Magazin über den entwickler.kiosk auch digital im Browser oder auf Ihren Android- und iOS-Devices lesen. In unserem Shop ist das Entwickler Magazin ferner im Abonnement oder als Einzelheft erhältlich.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Hinterlasse einen Kommentar

2 Kommentare auf "Einfache Desktopanwendungen mit Electron und React erstellen"

avatar
400
  Subscribe  
Benachrichtige mich zu:
Jibbex
Gast

Hallo,

wenn man das Projekt wie beschrieben mit create-react-app initialisiert, dann ist es meines Wissens nach nicht möglich native Nodejs Module im Render Threat zu nutzen. Damit ist Electron ein einfaches Browser-Fenster.

Das mag für die Beispiel Anwendung genügen aber der eigentliche Sinn Electron zu nutzen ist dahin. Besser wäre es dafür electron-forge zu nutzen.

MfG

Jibbex
Gast

Ein kurzer Nachtrag.

Sie haben am Ende electron-forge ja auch erwähnt. Asche über mein Haupt. Ich denke eine kurze Erwähnung des Problems wäre vielleicht auch gut gewesen. Falls ich das auch übersehen habe, dann tut es mir leid und Sie können mich gerne getrost ignorieren.

Das Thema ist in der Tat sehr komplex und der Artikel bietet ansonsten einen sehr schönen Einstieg wie ich finde.

X
- Gib Deinen Standort ein -
- or -