Desktopanwendungen mit JavaScript entwickeln

Seite 3: Electron

Inhaltsverzeichnis

Das zweite populäre Framework zum Erstellen von Desktopanwendungen in JavaScript ist Electron. GitHub setzte das Projekt ursprünglich im Rahmen der Entwicklung des Code-Editors Atom ein, machte es 2013 aber als separates Framework der Öffentlichkeit zugänglich.

Vom Prinzip her ist Electron ähnlich zu NW.js. Kein Wunder: einer der urspünglichen Entwickler von NW.js, der zuvor bei Intel beschäftigt war, arbeitet mittlerweile für GitHub an Electron weiter. Dennoch gibt es einen wesentlichen Unterschied zwischen NW.js und Electron, der für Entwickler wichtig zu verstehen ist: Während sich in NW.js Node.js und WebKit einen einzelnen JavaScript-Kontext teilen, gibt es in Electron mehrere Kontexte: einen für den Hintergrundprozess, der die Anwendung steuert, und jeweils einen für jedes Anwendungsfenster (zu beidem gleich mehr).

Ein weiterer Unterschied ist die Definition des Startpunkts einer Anwendung: In NW.js ist das wie bereits beschrieben eine HTML-Datei, in Electron dagegen eine JavaScript-Datei. Darüber hinaus unterscheidet sich die Art und Weise, in der WebKit (bzw. Chromium) eingebunden ist (Details sind in der Dokumentation zu finden).

Im Gegensatz zu Electron ermöglicht NW.js zudem das Kompilieren des JavaScript-Codes in nativen Code, um zu verhindern, dass der Quelltext in lesbarer Form mit der Anwendung ausgeliefert wird. Allerdings läuft das kompilierte Ergebnis etwa 30 Prozent langsamer. Darüber hinaus verfügt NW.js über einen integrierten PDF-Viewer und eine Druckvorschau. Unter Electron dagegen muss man auf externe Bibliotheken wie pdf.js ausweichen. Ein detaillierter Vergleich beider Frameworks ist unter anderem auf TangibleJS zu finden.

Electron unterscheidet Hauptprozess und Renderer-Prozess. Der Hauptprozess (üblicherweise in einer Datei main.js enthalten) stellt den Einstiegspunkt für eine Electron-Anwendung dar und kontrolliert den Lebenszyklus einer Applikation. Aus dem Hauptprozess heraus lässt sich beispielsweise auf native Komponenten wie das Dateisystem zugreifen.

Dem gegenüber stehen die Renderer-Prozesse, die im Wesentlichen ein (Browser-)Fenster innerhalb einer Electron-Anwendung repräsentieren und die eine Kombination aus HTML, CSS und JavaScript enthalten. Folglich hat man innerhalb eines Renderer-Prozesses (beziehungsweise des entsprechenden JavaScript-Codes) Zugriff auf das DOM des entsprechenden Fensters. Außerdem kann man von dort die Node.js-API ansprechen.

Innerhalb einer Anwendung kann es daher durchaus mehrere Renderer-Prozesse geben, jedoch nur einen Hauptprozess.

Die Installation von Electron führt wie bei NW.js über npm, sprich über den Befehl npm install -g electron (zumindest, wenn man die Kommandozeilentools von Electron nutzen möchte, ansonsten ist auch eine lokale Installation möglich). Wie erwähnt, bildet eine JavaScript-Datei den Einstiegspunkt in eine Electron-Anwendung. Da es sich bei Electron-Anwendungen wie bei NW.js-Anwendungen letztendlich um Node.js-Module handelt, werden Abhängigkeiten wie üblich über die Datei package.json in der jeweiligen Anwendung verwaltet.

Der einfachste Weg, sich diese (und weitere) Dateien generieren zu lassen, führt über das Git Repository electron-quick-start, das sich über den Befehl git clone https://github.com/electron/electron-quick-start klonen lässt und eine minimale Beispielanwendung enthält, bestehend aus Meta-Dateien wie genannter package.json-, .gitignore-, Lizenz- und Readme-Datei und folgenden weiteren:

  • eine JavaScript-Datei, in der sich der Code für den Hauptprozess befindet und in dem das Hauptfenster der Anwendung erzeugt wird (main.js),
  • eine HTML-Datei, die in das Hauptfenster geladen wird (index.html) und den Code für den Renderer-Prozess des Fensters bereitstellt,
  • eine JavaScript-Datei, die von der HTML-Datei eingebunden wird (renderer.js) und den Code enthält, den der Renderer-Prozess verarbeitet (zu Beginn ist die Datei bis auf einige Kommentare noch leer).

Über ein anschließendes npm install lassen sich die benötigten Abhängigkeiten installieren, mit npm start startet die Anwendung (alternativ eignet sich dafür auch der Befehl electron im entsprechenden Anwendungsverzeichnis).

Eine etwas angepasste Version des generierten Hauptprozess-Codes sieht wie folgt aus:

'use strict';
const electron = require('electron');
const app = electron.app;
const BrowserWindow = electron.BrowserWindow;
let mainWindow;

function createWindow () {
mainWindow = new BrowserWindow({width: 800, height: 600})
mainWindow.loadURL(`file://${__dirname}/index.html`)
mainWindow.on('closed', () => {
mainWindow = null;
})
}
app.on('ready', createWindow)
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
if (mainWindow === null) {
createWindow()
}
})

Um zwischen den einzelnen Prozessen, sprich dem Hauptprozess und den einzelnen Renderer-Prozessen zu kommunizieren, stehen die Objekte ipcMain und ipcRenderer zur Verfügung. Das Objekt ipcMain steuert dabei die Kommunikation auf Seiten des Hauptprozesses, während ipcRenderer den Austausch auf Seiten eines Renderer-Prozesses erledigt. Auf beiden Seiten lassen sich Events versenden (über die Methode send()) und auf sie lauschen (via on()).

Die folgenden beiden Listings zeigen dazu ein Beispiel, wobei der Ablauf folgender ist: im Renderer-Prozess wird innerhalb eines Event-Listeners, der für das click-Event an einer Schaltfläche registriert wurde, das Ereignis "example-request" mit dem Wert "Hello" an den Hauptprozess geschickt.

// Renderer-Prozess
'use strict';
const ipc = require('electron').ipcRenderer;
const button = document.getElementById('button')
button.addEventListener('click', () => {
ipc.send('example-request', 'Hello');
})
...

Innerhalb des Hauptprozesses beziehungsweise des dort für das Event example-request registrierten Event-Listeners wird ebenfalls ein Event versendet, und zwar example-response mit dem Wert "Hello World".

// Hauptprozess
'use strict';
const ipc = require('electron').ipcMain;
ipc.on('example-request', (event, arg) => {
event.sender.send('example-response', arg + ' World')
});

Der Renderer-Prozess, der auf das Event lauscht, nimmt den Wert wiederum entgegen und stellt ihn innerhalb der Oberfläche dar.

// Renderer-Prozess
...
ipc.on('example-response', (event, arg) => {
const message = `Antwort: ${arg}`
document.getElementById('response').innerHTML = message;
})

Ein weiteres Beispiel für die Anwendung der Interprozesskommunikation zeigen die folgenden Codeauszüge. In ihnen versendet das Programm auf Knopfdruck das Event open-file-dialog, woraufhin der Hauptprozess einen Dateidialog öffnet.

'use strict';
const ipc = require('electron').ipcRenderer;
const buttonSelectFile = document.getElementById('select-file');
buttonSelectFile.addEventListener('click', event => ipc.send('open-file-dialog'));
ipc.on('selected-files', (event, path) => {
/* ... */
});

Die ausgewählten Dateien beziehungsweise deren Dateinamen werden anschließend an den Renderer-Prozess zurückgeschickt und dort weiterverarbeitet:

'use strict';
const electron = require('electron');
const app = electron.app;
const BrowserWindow = electron.BrowserWindow;
const ipc = require('electron').ipcMain
const dialog = require('electron').dialog
let mainWindow;
...
ipc.on('open-file-dialog', event => {
dialog.showOpenDialog({
properties: ['openFile', 'openDirectory']
}, files => {
if (files) {
event.sender.send('selected-files', files);
}
})
})
...

Für das Implementieren von UI-Komponenten kann man wie bei NW.js auf bestehende CSS-Frameworks wie Bootstrap oder JavaScript-Frameworks wie AngularJS zurückgreifen. Darüber hinaus stellt die Bibliothek Photon einige typische UI-Komponenten für Desktopanwendungen wie Kopf- und Fußleisten, Toolbars, Tabs und einige mehr zur Verfügung, die speziell auf die Verwendung in Electron-Anwendungen ausgelegt sind. Interessierte können die Bibliothek als ZIP-Datei von der Homepage herunterladen, eine Installation über Bower oder npm wird dagegen nicht angeboten.

Mit Hilfe des Moduls electron-packager lassen sich für Electron-Anwendungen die entsprechenden Dateien für die unterschiedlichen Betriebssysteme generieren. Das Modul lässt sich sowohl programmatisch als auch über die Kommandozeile aufrufen. Für Ersteres reicht die lokale Installation mit npm install electron-packager --save-dev, für Letzteres ist das Modul über den Befehl npm install -g electron-packager global zu installieren.

Auf der Kommandozeile können Nutzer das Packaging über electron-packager starten, wobei als Parameter das Quellverzeichnis, der Name der Anwendung, die Zielplattformen ("darwin", "linux", "mas", "win32") sowie deren Architektur ("ia32", "x64", "armv7l") anzugeben sind. Der Befehl electron-packager electron example-electron --platform=darwin --arch=x64 beispielsweise paketiert die Anwendung für macOS in 64Bit.

Programmatisch lässt sich electron-packager wie folgt verwenden. Alternativ dazu steht das Grunt-Plug-in grunt-electron zur Verfügung.

'use strict';
const packager = require('electron-packager');
const options = {
dir: './src',
platform: 'darwin',
arch: 'x64'
}
packager(options, (error, appPaths) {
if(error) {
console.error(error);
}
});

Auch Electron-basierte Anwendungen lassen sich mit den Chrome Developer Tools debuggen. Programmatisch können Entwickler die Tools über die Methode toggleDevTools() aktivieren:

...
function createWindow () {
mainWindow = new BrowserWindow({width: 800, height: 600})
mainWindow.toggleDevTools();
...
}
...
app.on('ready', createWindow);
...

Zusätzliche Funktionen für das Debugging und Monitoring stellt das Tool Devtron (http://electron.atom.io/devtron/) bereit, das sich als Plug-in in den Chrome Developer Tools einnistet. Es lässt sich mit npm install --save-dev devtron installieren und anschließend via require('devtron').install() aktivieren.

...
function createWindow () {
mainWindow = new BrowserWindow({width: 800, height: 600})
require('devtron').install();
mainWindow.toggleDevTools();
...
}
...
app.on('ready', createWindow);
...

Im Wesentlichen bietet Devtron folgende Hilfestellungen:

  • Visualisierung von Abhängigkeiten: Die Bibliotheken, von denen die aktuelle Anwendung abhängig ist, lassen sich in Form eines Graphen darstellen.
  • Übersicht Event-Listener: Über den Reiter "Event Listeners" erhalten Entwickler eine Übersicht der in einer Electron-Anwendung registrierten Event-Listener.
  • Fehlerdiagnose: Über den Reiter "Lint" lässt sich die Anwendung auf Fehler überprüfen und Entwickler können potenzielle Schwachstellen aufdecken.
  • Interprozesskommunikation: Im Reiter "IPC" lässt sich die Kommunikation zwischen Hauptprozess und Renderer-Prozessen protokollieren beziehungsweise die versendeten Nachrichten und deren Inhalte einsehen.

Devtron und die Chrome Developer Tools helfen nur beim Debuggen von Renderer-Prozessen. Um den Code des Hauptprozesses zu debuggen, muss man dagegen auf externe Debugger zurückgreifen und die Anwendung unter Angabe des Parameters --debug beziehungsweise --debug-brk starten.

Bezüglich des Testens von Electron-Anwendungen ist besonders das Tool Spectron interessant. Es nutzt ChromeDriver und WebDriver I/O, ein Selenium-2.0-Binding für Node.js und lässt sich prinzipiell mit jeder Testing-Bibliothek (wie Mocha oder Jasmine) einsetzen. Spectron wird über den Befehl npm install --save-dev spectron installiert und steht anschließend über require('spectron') zum Einbinden zur Verfügung. Folgender Quellcodeausschnitt zeigt ein Beispiel zum Einsatz:

'use strict';
const Application = require('spectron').Application
const assert = require('assert');
let app = new Application({
path: '/Applications/Example.app/Contents/MacOS/Example'
});
app.start().then(() => {
return app.browserWindow.isVisible()
}).then(isVisible => {
assert.equal(isVisible, true)
}).then(() => {
return app.client.getTitle()
}).then(title => {
assert.equal(title, 'Example')
}).then(() => {
return app.stop()
}).catch(error => {
console.error('Test fehlgeschlagen', error.message)
});