Für eines meiner nächsten Projekte denke ich darüber nach eine Desktop-App zu bauen. Vorzugsweise sollte sie auf JavaScript basieren, denn so kann ich auch meine TypeScript-Skills schärfen. Electron scheint ein guter Startpunkt zu sein.


Voraussetzungen

  • Node.js
  • Microsoft Visual Sudio Code

Milestone 1 - Electron App

Starten wir zuerst Microsoft Visual Sudio Code, und öffnen einen passenden (leeren) Ordner in dem das Projekt liegen wird.

Erstellen wir nun eine leere JavaScript-Datei main.js.

Öffnen wir dann ein Terminal und initiieren ein neues Node.js Projekt mit npm init.

Im Terminal tauchen viele Fragen auf, die wir brav beantworten.

Node.js erstellt die Projektdatei package.json. Weil zuvor die main.js existierte, erkennt Node.js sie automatisch als Einstiegspunkt.

Als Nächstes installieren wir Electron und TypeScript.

npm install --save-dev electron typescript

Weil Node.js dies erkennt, wird automatisch die package.json-Datei im Abschnitt devDependencies angepasst.

Um ein Electron Fenster zu laden, muss man Node.js das ein passendes script-Kommando beibringen. Im Abschnitt scripts der package.json-Datei muss ein neuer Eintrag plaziert werden, z.B. start.

"scripts": {
  "start": "electron ."
}

Tipp: Der vorherge Eintrag test kann zunächst ersetzt bzw. gestrichen werden.

Die package.json könnte nun wie folgt aussehen:

{
  "name": "helloworld1",
  "version": "1.0.0",
  "description": "Hello World",
  "main": "main.js",
  "dependencies": {},
  "devDependencies": {
    "electron": "^7.1.5",
    "typescript": "^3.7.3"
  },
  "scripts": {
    "start": "electron ."
  },
  "author": "Damian Thater",
  "license": "ISC"
}

Mit npm start wird Node.js nun angewiesen das dahinter liegende Programm electron zu starten. Dabei wird das Startverzeichnis . verwendet.

Tipp: Wer in Zukunft nicht auf dem Terminal rumhacken will, kann sich im Visual Studio Code auch die Option NMP SCRIPTS aktivieren. VSC erkennt den scripts Abschnitt in der package.json Datei automatisch und bietet eine klickbare Alternative im Fenster an. Schön für alle Maus-Jockeys.

Nun passiert aber nicht viel. Unter MacOS wird lediglich eine neue App gestartet, die zwar das Icon von React vorhält, es wird aber kein Fenster angezeigt. Das liegt daran, dass der Einstiegspunkt main.js leer ist. Das ändern wir nun.

Die main.js beinhaltet das Setup des Fensters, also müssen wir die passenden Anweisungen angeben.

// Import notwendiger Klassen und der dem app Singleton
const { app, BrowserWindow } = require('electron')

// Referenz auf das Fenster
let win

// Wenn die App geladen wurde, Fenster erstellen
app.on('ready', createWindow)

function createWindow() {
    // Erstelle Fenster
    win = new BrowserWindow({
        width: 800,
        height: 600,
        webPreferences: {
            nodeIntegration: true
        }
    })
    // Vernichte die Fenster-Referenz, 
    // wenn das Fenster geschlossen.
    win.on('closed', () => {
        win = null
    })
    // Lade Start-Datei
    win.loadFile('index.html')
}

Tipp: Da MacOS anders als Windows nur die Fenster, nicht aber die Applikation schließt, muss dieses Verhalten unter MacOS simuliert werden.

// Unter MacOS gilt: Sind alle Fenster geschlossen, 
// verbleibt die App weiterhin bestehen!
app.on('window-all-closed', () => {
    if (process.platform !== 'darwin') {
        app.quit()
    }
})

// MacOS zeigt das Icon der App an. 
// Klickt man es an und es ist kein Fenster offen,
// muss eins neu erzeugt werden.
app.on('activate', () => {
    if (!win) {
        createWindow()
    }
})

Starten wir das Programm nun mit npm start, so sehen wir ein leeres Fenster.

Was fehlt ist die index.html Datei die per win.loadFile('index.html') angefordert, aber nicht bereitgestellt wird. Legen wir sie also einfach mit folgendem Inhalt an.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
    </head>
    <body>
        Hallo Welt
    </body>
</html>

Der Projektordner sollte nun folgenden Inhalt haben:

node_modules
index.html
main.js
package.json
package-lock.json

Startet man die App, erscheint “Hello Welt” im Fenster.

Electron Demo App

Milestone 2 - TypeScript Support

So weit, so gut. Wir haben eine laufende Applikation, aber noch keinen TypeScript Support. Zeit dies ändern. Da der TypeScript Compiler in den node_modules installiert wurde, können wir dies nutzen, um das Projekt entsprechend einzustellen.

./node_modules/.bin/tsc --init

Es wird eine neue Datei tcconfig.json erstellt - randvoll mit auskommentierten Parametern.

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true
  }
}

Damit nun TypeScript einen Job machen kann und ts-Dateien zu js-Dateien transpiliert, muss noch einwenig Vorbereitung passieren:

Zunächst einmal verlegen wir die main.js in einen neuen Unterornder src. Dort benennen wir die Datei zu main.ts um. Daraufhin beschwert sich TypeScript, dass Teile der main.ts ungültig sind.

So ist let win nicht ausreichend formuliert. TypeScript erwartet hier etwas ausführlichere Anweisungen:

let win: Electron.BrowserWindow | null

Nun beschwert sich TypeScript, dass Electron nicht gefunden wird. Das liegt daran, dass der Import der Module per require nicht ausreichend ist. Also auch hier eine kleine Korrektur:

import { app, BrowserWindow } from "electron"

Wagen wir mal einen Start der App.

Hoppla.

Cannot find module './helloworld1/main.js'. 
Please verify that the package.json 
has a valid "main" entry.

Ohja, bevor wir die App mit main.js nutzen können, muss diese erst gebaut werden. Erweitern wir die package.json im Abschnitt scripts um einige sinvolle Shortcuts.

"scripts": {
  "build": "tsc ."
  "start": "electron .",,
  "buildAndStart": "tsc && electron ."
}

Während build nur die ts-Dateien transpiliert, ruft buildAndStart den Transpiler auf und startet im Anschluss die Electron App.

Beginnen wir mit npm build.

War TypeScript erfolgreich, existiert im Ordner src die Datei main.js. Nun haben wir aber zwei Dateien im src Ordner. Das räumen wir als Nächstes auf. Die main.js soll statt im src-Ordner in einem dist-Ordner landen.

Damit das gelingt und Electron die Dateien nun findet, müssen einige Dateien angepasst werden. Die tsconfig.json erhält folgende Ergänzungen: outDir, include und exclude

{
  "compilerOptions": {
    [...]
    "outDir": "./dist"
  },
  "include": [
    "./src"
  ],
  "exclude": [
    "./node_modules"
  ]
}

Führen wir nun npm build aus, wird ein Ordner dist erzeugt und darin die main.js abgelegt. Die ursprüngliche main.js im src-Ordner muss gelöscht werden.

Kümmern wir uns nun um Electron und den App Start.

Hier muss lediglich main im package.json umgestellt werden:

{
  [...]
  "main": "./dist/main.js"
}

So, nun können wir die App mit npm start starten, und siehe da: es funktioniert.

Tipp: Damit wir eventuelle JavaScript Fehler in der Electron App sehen, ist es sinnvoll die DevTools beim Start zu aktivieren. Dazu fügen wir nach der Anweisung win.loadFile folgende hinzu:

win.webContents.openDevTools()

Nach dem Start und den Blick in die JavaScript-Console, sehen wir eine Security-Warnung.

Hinweis: Folgen wir den Hinweisen auf https://electronjs.org/docs/tutorial/security#csp-meta-tag wird empfohlen die Content-Security-Policy in der html-Datei anzupassen.

<head>
  ...
  <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
</head>

Fertig.

Customizing

Um eigenen Code einzubringen, kann man wie folgt vorgehen.

Wir erzeugen eine neue Datei app.ts im Ordner src. Darin legen wir Code hinein, der lediglich den Text im Dokument ändert.

export const NAME = "Hello Electron"

document.addEventListener("DOMContentLoaded", () => {
    document.body.innerHTML = NAME
})

Um den Code auch ausführbar zu machen, muss die passende main.js als script in die HTML Seite geladen werden.

<head>
  <script src="./dist/app.js"></script>
</head>

Nach dem npm start, erscheint “Hello Electron” im Fenster.

Milestone 3 - React Framework

Da wir nicht ständig HTML-Elemente per “Vanilla JavaScript” erstellen wollen, wird uns React eine große Hilfe sein. Es verkürzt die Bauweise von Komponenten erheblich.

Na dann, auf geht’s!

Zunächst einmal müssen wir React mit einigen weiteren nützlichen Modulen installieren.

npm install --save-dev react react-dom @types/react @types/react-dom

Dabei legt Node.js ein ganzen Strauch von neuen devDependencies für uns in der package.json-Datei an.

Doch das reicht leider nicht für unser Vorhaben. Die tsconfig.json braucht auch eine Anpassung.

"compilerOptions": {
  ...
  "jsx": "react",
  "allowJs": true
}

Danach legen wir im src-Ordner eine neue Datei an mit dem Namen components.jsx. Dank der Endung jsx können wir das Komponenten-Model von React nutzen. Als Beispiel legen wir eine H1-Headline Komponente an.

import React from "react"

export class Headline extends React.Component{
   render(){
     return <h1>{this.props.text}{this.props.children}</h1>
   }
}

Als Nächstes passen wir unsere app.ts Datei an und nutzen die Headline-Komponente statt des einfachen Texts.

import React from "react";
import ReactDOM from "react-dom";
import { Headline } from "./components"

export const App = (
    <Headline text="Hallo React"></Headline>
)

document.addEventListener("DOMContentLoaded", () => {
    ReactDOM.render(App, document.getElementById("app"));
})

Leider versteht Node.js nur noch Bahnhof und erwartet keine spitzen Klammern im Code. Doch gernau das ist unser Wunsch. Das Problem ist schnell behoben, indem man die app.ts zu app.jsx umbenennt.

Wenn wir die App nun mit npm start starten, läuft sie zwar an, meldet aber in der JavaScript Console einen Fehler.

Uncaught Error: Cannot find module './components'

Damit unsere HTML-Seite die richtigen Sourcen findet, müssen wir den dist Order für Components verwenden.

import { Headline } from "./dist/components"

Und das war’s schon. Nun startet unsere Electron-App und meldet sich mit “Hello React”, wird unterstützt von TypeScript und gefüllt mit Hilfe des React Frameworks.

Electron Demo App mit React

Quellen und Weiterführende Links

comments powered by Disqus