Cross-plattform Apps mit Electron und React Teil 1

electron-react-header

Mit der Kombination aus Electron und React kann man sehr einfach Desktopanwendungen für Linux, Windows und Mac entwickeln. Die Kombination aus Electron und React ermöglicht einen angenehmen Entwicklungsworkflow, der den meisten anderen GUI-Frameworks weit voraus ist.

Ein Beispiel für eine Anwendung die mit React und Electron entwickelt wurde, ist Alva. Alva ist eine Anwendung um Prototypen für Web-Komponenten zu entwerfen.

Was ist Electron?

Electron ist ein Framework, um Desktop Anwendungen mithilfe von Web-Technologien zu entwickeln. Electron basiert auf dem Chromium-Browser und dem NodeJS-Framework.

Durch die Kombination von Chromium und NodeJS kann man gleichzeitig seine Web-Entwicklungs-Kenntnisse nutzen und trotzdem auf das Dateisystem bzw. die Hardware des Host-Rechners zugreifen.

Bekannte Beispiele für Anwendungen, die mit Electron geschrieben worden sind, sind die Text-Editor Atom und Visual Studio Code, die Desktop-Anwendung von Skype und der Slack Chat-Client. Electron ist vor allem bei Startups aus dem Silicon Valley sehr populär.

Durch die Verwendbarkeit von JavaScript-Bibliotheken kann man bei der Entwicklung von Electron-Apps auf einen riesigen Vorrat an nützlichen Frameworks zurückgreifen. Daher kann man auch React mit Electron verwenden.

Was ist React?

React ist eine JavaScript-Bibliothek zur Entwicklung von Benutzerschnittstellen. React wird von Facebook entwickelt und eignet sich besonders für die Entwicklung von Single-Page-Apps, wie sie häufig mit Electron entwickelt werden.

Das Besondere an der GUI-Entwicklung mit React ist, dass man seine Benutzerschnittstelle deklarativ entwickelt. Das heißt, man muss seine Web-Komponenten nicht selber verändern, wenn sich der zugrundeliegende Zustand der Anwendung verändert. Durch React passen sich die einzelnen Komponenten automatisch dem aktuellen Zustand der Anwendung an.

Dieses reaktive Verhalten ist auch der Namensgeber für React. Darüber hinaus bietet React noch einige interessanten Funktionen zum Zustands-Management, die bei Bedarf auch noch durch weitere Bibliotheken erweitert werden kann.

Warum Electron + React?

Die Kombination aus React und Electron bildet ein GUI-Framework, dass sehr gut dokumentiert ist, sehr Entwickler freundlich ist und auf ein riesiges Ökosystem zurückgreifen kann. Alles Eigenschaften, die die gängigen GUI-Frameworks, speziell unter Linux, so nicht bieten.

Erste Schritte

Im Folgenden gehe ich von grundlegenden JavaScript-Kenntnissen und Basiswissen in der Verwendung von Nodejs aus.

Als Erstes müssen wir Electron mit React aufsetzen. Das geht am leichtesten mit Electron Forge. Electron Forge ist ein Command-Line-Projekt. Mit dem sich Electron Projekte schnell aufsetzen lassen. Es stehen mehrere Templates zur Verfügung um schnell das passende Framework für Electron aufzusetzen. Neben React kann man beispielsweise auch TypeScript oder Angular mit Electron aufsetzen.

Um Electron Forge zu installieren und dann ein neues Electron-React Projekt zu starten, gibst du in einem Terminal folgenden Code ein:

npm install -g electron-forge
electron-forge init electron-react-example --template=react
cd electron-react-example
npm install
electron-forge start

Der Prozess dauert ein wenig, da die komplette Runtime von Electron heruntergeladen wird. Am Ende sollte dic ein Electron Fenster mit einer neuen React-App begrüßen.

Electron-React-Example

Eine weitere beliebte alternative Electron Apps mit React zu erstellen ist das Electron React Boilerplate. Ich habe mich für Electron Forge entschieden, weil die Optionen zur Paketierung der App aus meiner Sicht umfangreicher sind und besser funktionieren. Electron React Boilerplate bietet sehr umfangreiche Voreinstellungen für React, die mir mehr im Weg standen, als sie mir genutzt haben.

Warum kein create-react-app?

Ein Wort noch zu Create React App. Create React App ist das offizielle Tool von Facebook um neue React Apps zu erstellen. Allerdings ist Create React App ganz klar auf “echte” Web-Applikationen ausgelegt.

So verhindert Create React App die Benutzung von nativen Nodejs-Modulen. Damit degradiert es Electron zu einem einfachen Browser-Fenster. Sodass der eigentliche Sinn von Electron zerstört wird.

Am Ende steht folgende Verzeichnisstruktur:

/
├── LICENSE
├── node_modules
├── package.json
├── src
│ ├── app.jsx
│ ├── index.html
│ └── index.js
└── yarn.lock

Wir werden unsere Änderungen ausschließlich in src machen werden.

Eine Demo-App

Unsere Beispielanwendung wird ein Nachbau der gtk3-demo-application.

gtk3-demo-application

Schließlich wollen wir ja eine Alternative zur GUI-Entwicklung finden.

Du findest den Source-Code für dieses Tutorial auf Github: https://github.com/rockiger/electron-react-example

Eine erste Änderung

Um zu sehen, ob das Hot-Reloading von Electron Forge funktioniert, werden wir eine kleine Änderung an unserer App vornehmen, bevor wir richtig loslegen.

Dafür öffnen wir die Datei app.jsx und ändern den Platzhaltertext ab.

...
<h2>Demo Application</h2>
...

Sobald du die Datei geändert hast und abspeicherst, sollte sich auch der angezeigte Text in der Electron App ändern.

Um aus der Kombination von Electron und React ein richtiges GUI framework zu machen, benötigen wir noch eine weitere Bibliothek: BlueprintJS.

BlueprintJS ist ein UI-Toolkit für React. Es bietet eine große Anzahl an Komponenten, aus denen wir unsere Oberfläche bauen können. Um BlueprintJS zu installieren, geben wir folgenden Code in der Kommandozeile ein:

npm i @blueprintjs/core --save

Danach müssen wir noch die CSS-Datein einbinden. Dazu öffnen wir index.html und fügen folgenden Code ein vor dem </head>-Tag ein:

  <link href="../node_modules/normalize.css/normalize.css" rel="stylesheet" />
  <link href="../node_modules/@blueprintjs/core/lib/css/blueprint.css" rel="stylesheet" />
  <link href="../node_modules/@blueprintjs/icons/lib/css/blueprint-icons.css" rel="stylesheet" />

Wenn du nach der Änderung die Electron neu lädst (<Strg>+R), solltest du eine andere Schriftart sehen.

Um unsere Demoanwendung nachzubauen, werden wir wie folgt vorgehen: Zuerst werden wir die Toolbar nachbauen, dann werden wir ein Textfeld integrieren und abschließend die Statuszeile implementieren.

Der Einfachheit halber werden wir alles in einer Datei entwickeln. Bei einer echten Anwendung würden wir aus Gründen der Übersichtlichkeit die Komponenten in mehrere Dateien aufspalten.

Toolbar

Die Toolbar-Komponente heißt in BlueprintJS Navbar. Sie hat mindestens eine NavbarGroup als Tochter. Diese wiederum kann Buttons und Trenner als Töchter haben.

Um die Demo Applikation nachzubilden haben wir der Navbar 5 Töchter gegeben. Davon sind 4 Buttonn und eine ein Trenner. Den Button mit dem Pfeil nach unten haben wir in ein Popover gepackt, um das Menü zu repräsentieren.

Der Code für unsere App sieht wie folgt aus:

import React from 'react';
import {
  Button,
  Classes,
  Navbar,
  NavbarDivider,
  NavbarGroup,
  Popover,
  Menu,
  MenuItem,
} from '@blueprintjs/core';

export default class App extends React.Component {
  render() {
    return (
      <div id="Layout">
        <Navbar>
          <NavbarGroup>
            <Button className={Classes.MINIMAL} icon="folder-open" />
            <Popover content={<Menu><MenuItem text="File 1" /></Menu>}>
              <Button className={Classes.MINIMAL} icon="chevron-down" />
            </Popover>
            <Button className={Classes.MINIMAL} icon="small-cross" />
            <NavbarDivider />
            <Button className={Classes.MINIMAL} icon="properties" />
          </NavbarGroup>
        </Navbar>
      </div>);
  }
}

Wir importieren zuerst die benötigten Module und erweitern dann die App-Komponente. Jede React Komponenten-Klasse benötigt eine Render-Methode. Das Besondere der Render-Methode ist, dass man dort HTML und Javascript mischen kann. (Es gibt auch noch andere Komponenten. Die Besprechung würde hier aber zu weit fühern. Ich verweise daher auf die React-Dokumentation.)

Um das Verhalten der Toolbar nachzubauen, müssen wir nun den einzelnen Buttons eine onclick-Funktion geben.

import { remote } from 'electron';
const showMessageBox = remote.dialog.showMessageBox;
const app = remote.app;
...
            <Popover content={
              <Menu>
                <MenuItem
                  text="File 1"
                  onClick={() => showMessageBox({
                    type: 'info',
                    message: 'You activated action: "file1"',
                    buttons: ['Close'],
                  })}
                />
              </Menu>}
            >
              <Button className={Classes.MINIMAL} icon="chevron-down" />
            </Popover>
            <Button
              className={Classes.MINIMAL}
              icon="small-cross"
              onClick={() => app.quit()}
            />
            <NavbarDivider />
            <Button
              className={Classes.MINIMAL}
              icon="properties"
              onClick={() => showMessageBox({
                type: 'info',
                message: 'You activated action: "logo"',
                buttons: ['Close'],
              })}
            />
...

Da Electron zwischen Haupt- und Render-Prozess unterscheidet, können wir aus einem Render-Prozess ( Das ist immer das, was als Fenster dargestellt wird.) auf den Hauptprozess nur zugreifen, wenn wir das Remote-Modul importieren.

Dies haben wir in den ersten drei Zeilen gemacht: Wir haben das Remote-Modul importiert und dann die Methode ShowMessageBox und das App-Objekt in zwei Konstanten gespeichert.

Die eigentlichen Click-Handler sind sehr einfach gehalten. Aus diesem werden nur die importierten Electron Methoden aufgerufen. Das Ordnersymbol hat übrigens keinen Click-Handler bekommen, weil dies in der Demo App auch so ist. Allerdings könnte es eine gute Übung sein, mit einem Klick auf das Ordnersymbol einen Öffnen-Dialog zu zeigen.

Fertige Toolbar

Textfeld

Als nächstes werden wir das Textfeld einfügen. Hier bedienen wir uns einer HTML-Textarea.

Es muss dazu gesagt werden, dass dies kein vollständiger Text-Editor ist. Eine Textarea hat nur eine eingeschränkte API. Es sind viele Bibliotheken auf dem Markt, um leistungsfähige Editoren in eine Website zu integrieren.

Bekannte Bibliotheken, die gut mit React funktionieren sind draft.js und slate.js. Laut diesem Github-Issue soll eine Lösung auf Basis von draft.js demnächst in Blueprint landen. Allerdings ist dieses Ticket schon relativ alt und ich weiß nicht, wann diese Lösung eingeführt wird.

Um unser Layout wie in der Demo-Application zu halten, müssen wir das CSS für das umschließende Element und den Editor ein wenig abändern. Wir nutzen Flexbox, damit das Textfeld den gesamten freien Raum des Fensters einnimmt.

...
<div id="Layout" style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
...
</Navbar>
<textarea
  id="TextField"
  style={{
    height: '100%',
    flexGrow: 1,
    overflow: 'auto',
    border: 'none',
    resize: 'none',
    outline: 'none',
  }}
/>
...

Statusbar

Abschließend erstellen wir noch unsere Statusbar. Diese stellt uns neben dem eigentlichen Layout, noch vor eine zweite Aufgabe: Wir müssen nämlich die Cursorposition und die Anzahl der Zeichen in der Statusbar anzeigen.

Das Layout ist schnell erstellt. Wir erstellen ein HTML Element, das eine Höhe von 50 Pixel hat und einen leicht grauen Hintergrund. Hierzu verwenden wir vordefinierte Farben von Blueprint und schreiben sie in das Style-Attribut der Statusbar.

...
  Colors,
} from '@blueprintjs/core';
...
<div
  id="StatusBar"
  style={{
    backgroundColor: Colors.LIGHT_GRAY5,
    borderTop: `1px solid ${Colors.LIGHT_GRAY1}`,
    height: '50px',
    padding: 12,
  }}
/>
</div>);
  }
}

Aufwändiger wird es nun, die Statistik über das Textdokument zu erstellen. Hierfür müssen wir Zustand in React einführen. Das Tolle an React ist, das bei Veränderungen des Zustands die betroffenen Komponenten aktualisiert werden. Das erlaubt es ein sehr simples Zustandsmodell der Applikation zu entwerfen. Wer noch gar keine Erfahrung mit React hat, sollte an dieser Stelle mit dem offiziellen Tutorial auseinandersetzen und dann hierher zurückkommen.

Als erstes führen wir eine Zustandsvariable statistic ein und initialisieren diese. Darüber hinaus wollen wir deren Inhalt in der Statusbar darstellen.

Als Erstes legen wir einen Konstruktor für unsere Komponente an, in der wir die Zustandsvariable definieren. Wo wir gerade im Konstruktor sind, binden wir noch eine Methode an das this-Objekt der Komponenten. Die Methode werden wir gleich erstellen.

...
export default class App extends React.Component {
  constructor() {
    super();
    this.state = {
      statistic: {
        row: 0,
        col: 0,
        chars: 0,
      },
    };
    this.onInput = this.onInput.bind(this);
  }
...

Als nächstes ergänzen wir das Textfeld um ein onKeyDown-Attribut. Damit wir Nutzer-Eingaben abfangen können. Um die Änderung der Textarea nun von unserer Statusbar darstellen zu können, fügen wir folgenden Text innerhalb des Elements ein.

...
<textarea
  id="TextField"
  style={{
    height: '100%',
    flexGrow: 1,
    overflow: 'auto',
    border: 'none',
    resize: 'none',
    outline: 'none',
  }}
  onKeyDown={this.onInput}
/>
...

Dabei erlaubt uns React mit den geschweiften Klammern direkt auf die Zustandsvariable der Komponente zuzugreifen. Bei jeder Änderung der Zustandsvariablen wird die Komponente aktualisiert.

<div
  id="StatusBar"
  style={{
    backgroundColor: Colors.LIGHT_GRAY5,
    borderTop: `1px solid ${Colors.LIGHT_GRAY1}`,
    height: '50px',
    padding: 12,
  }}
>
  Cursor at row {this.state.statistic.row} column  
  {this.state.statistic.col} - 
  {this.state.statistic.chars} chars in document
</div>

Zu guter Letzt schreiben wir noch eine einen Event-Handler für Nutzer-Eingaben. Dieser passt den Zustand der Komponente der aktuellen Cursorposition des Textfeldes an. Achtung: Der Event-Handler berücksichtigt nicht die typische Vorgehensweise in React. Er dient nur zur Veranschaulichung des reaktiven Charakters von React. Ich möchte an dieser Stelle nicht noch weitere React-Konzepte wie Referenzen oder Props einführen.

...
  onInput() {
    const textField = document.querySelector('#TextField');
    const textBeforCaret = textField.value.slice(0, textField.selectionStart);
    const rows = textBeforCaret.split('\n');
    const row = rows.length;
    const col = rows.pop().length;

    this.setState({ statistic: {
      row,
      col,
      chars: textField.value.length,
    } });
  }

  render() {
...

Wenn er nun unsere Anwendung anschauen ist diese der Beispielanwendung von GTK sehr ähnlich. 

Fertige Electron-React-Demo-Application

Im nächsten Teil, werden wir die Optik der App anpassen, um das Feeling der App noch “nativer” erscheinen zu lassen. Später werden wir noch das Menü entsprechend abändern, dass es die gleiche Funktionalität umfasst wie in der gtk3-demo-application.

Created with Dictandu