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.
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.