Funktionale Frontend-Entwicklung: ClojureScript in Desktopanwendungen
Wie bereits im Artikel Web-Apps mit Reacl programmieren beschrieben, verwenden wir bei der Active-Group für die Entwicklung von Web-Frontends ClojureScript mit unserem Framework Reacl. Im Projekt EQUALS entwickeln wir derzeit für die EQUALS-Anwendung Teile der grafischen Benutzeroberfläche neu.
Unser Ziel dabei ist, die neue entwickelten Teile der Benutzeroberfläche nicht nur in der Desktopanwendung, sondern später auch in der Webversion von EQUALS zu verwenden. Die Lösung liegt für uns darin, dass wir die Benutzeroberfläche als Webanwendung umsetzen. Neben der Wiederverwendbarkeit (das Design ist für die Desktop- und Webanwendung weitgehend identisch und basiert auf HTML5 & CSS), können wir dabei insbesondere unser Framework reacl verwenden und die Benutzeroberfläche mithilfe von funktionaler Programmierung umsetzen.
Technische Voraussetzungen
Seit Version 7u6 integriert die Java-Runtime (JRE) das Framework
JavaFX,
welches unter anderem einen eingebetteten Browser (als WebView
bezeichnet) enthält. Dieser kann in eine Java/Scala-Benutzeroberfläche
integriert werden, und erlaubt somit die Einbindung beliebiger
Webseiten in eine Desktopanwendung. Es wird dabei, wie auch in Apples
Safari, die Rendering-Engine WebKit
verwendet. Seit der Version 8 der Java-Runtime ist die Darstellung von
Webseiten , die sich ja häufig von Browser zu Browser unterscheidet,
in JavaFX sehr ähnlich zu Apples Browser Safari und auch Google
Chrome.
Das Framework bietet noch mehr als einen eingebetteten Browser, wir betrachten hier aber nur die Einbindung von Webanwendungen. Wer nicht unbedingt JavaFX verwenden muss/möchte, kann auch gern auf Alternativen zurückgreifen, so bietet z.B. das .NET-Framework mit der WebBrowser-Klasse ähnliche Funktionalität.
Integration einer Webanwendung in eine Scala-Desktopanwendung
Es ist einfach möglich, in einer mit Swing gestalteten
Benutzeroberfläche JavaFX zur Gestaltung der Benutzeroberfläche zu verwenden: Das JFXPanel
ist eine JComponent
und kann daher überall eingesetzt werden, wo man auch andere Swing-Komponenten wie JTextField
oÄ. verwenden kann.
Das typisches Aussehen einer Scala-Anwendung, die eine Webanwendung integriert, sieht wie folgt aus:
Der obige Code erzeugt eine Main-Klasse, die einen Frame erstellt. In
diesem Frame wird ein JFXPanel
platziert. In diesem Panel hat man
die Möglichkeit, beliebige JFX-Funktionalitäten einzubinden. Wir
wollen eine Webanwendung anzeigen, was beispielhaft in initView
geschieht: Es wird ein neuer WebView
erzeugt, und dann die Seite www.funktionale-programmierung.de
geladen. Damit die Seite im JFXPanel
angezeigt wird, muss diese noch mit einer Scene
dem Panel hinzugefügt werden. (Eine Scene ist vom Grundsatz her nichts anderes als ein JPanel. Es heißt in JavaFX nur anders und kann mehr.)
Somit hat man eine einfaches Programm, welches Webanwendung wie ein Browser lädt und anzeigt. Jetzt fehlt noch die Möglichkeit, mit dieser Webanwendung zu interagieren. Normalerweise geschieht dies über einen Server, der Anfragen entgegennimmt und dann passende Antworten zurückliefert. So einen Server gibt es hier nicht, es stellen sich also folgende Fragen:
- Woher weiß Scala, wann in der Webanwendung ein Knopf gedrückt wurde, oder welche Eingaben ein Benutzer in ein Textfeld gemacht hat?
- Woher weiß die Webanwendung, wenn der Benutzer im restlichen Teil der Scala-Anwendung (also in der „normalen“ Benutzeroberfläche) eine Aktion getätigt hat, die sich auf die Webanwendung auswirken soll?
Zur Lösung und Erklärung dieser Fragestellungen verwenden wir im Folgenden eine mit reacl entwickelte Mini-Webanwendung, die nur einen Button enthält.
Kommunikation von der Desktopanwendung zur Webanwendung
Die folgende Reacl-Anwendung wird im Namespace javafx.simple
erstellt und enthält nur eine Reacl-Klasse javafx-simple-app
, die
mit dom/button
einen Button erzeugt, sowie eine Funktion simple-app
, die diese Reacl-Klasse aufruft.
Erstmal macht dieser Button noch nichts, wir füllen Ihn später mit Funktionalität.
Außerdem gehört zu jeder Reacl-Anwendung noch eine kleine HTML-Datei,
hier javafxtest.html
, die die benötigtigen JavaScript-Bibliotheken einbindet (mehr Details
gibts dazu im früheren Artikel Erste Schritte mit ClojureScript).
Diese Reacl-Anwendung soll dann innerhalb des Scala-Programmes
angezeigt werden. Dabei sind zwei Dinge zu beachten: Zum einen ist die Webanwendung
nicht mehr im Internet verfügbar, sondern eine lokale Anwendung,
die Teil des Scala-Programms sein soll, und zum anderen reicht es
nicht, die zum ClojureScript-Programm zugehörige HTML-Seite zu
laden - die Anwendung mit dem Namen simple-app
muss auch noch
gestartet werden. Der Code der dies erledigt sieht wie folgt aus:
Die Klasse WebView
stellt dem Entwickler eine WebEngine
zur Verfügung. Dabei handelt es sich um ein Objekt, mit dessen Hilfe man
mit der eingebundenen Webanwendung interagieren kann: Die Methode executeScript
als Teil der WebEngine
kann JavaScript-Code der
Webanwendung benutzen. Das machen wir uns zunütze, um unsere
ClojureScript-Anwendung zu starten.
Nachdem die HTML-Seite im Pfad
resources/javafxtest.html
fertig geladen wurde (deshalb das Konstrukt
mit getLoadWorker.stateProperty.addListener
) ruft Scala mit
webEngine.executeScript
JavaScript-Code der Webanwendung auf:
webEngine.executeScript("goog.require(\"" + namespace + "\");")
lädt das reacl-Programm im angegebenen Namespace und sorgt dafür, dass mögliche Abhängigkeiten aufgelöst werdenval jsjfx = webEngine.executeScript(namespace).asInstanceOf[JSObject]
holt das reacl-Programm im angegebenen Namespace und speicherte es injsjfx
jsjfx.call("simple_app")
ruft die in ClojureScript definierte Funktionsimple-app
auf, die dann die Anwendung startet. Aber Moment mal: Wieso ruft der Codesimple_app
auf, obwohl die Anwendung in ClojureScriptsimple-app
heißt? Der Grund liegt darin, dass in ClojureScript Namen erlaubt sind, die in JavaScript nicht möglich sind. Dadurch finden beim Compilieren Umbenennungen statt:-
wird zu_
oder ein!
zu_BANG_
Es gibt hierfür noch viele weitere Regeln, die z.B. hier gut zusammengefasst sind.
Und das war schon, zumindest für die Richtung von Scala nach Webanwendung. Die Kommunikation von Scala zur eingebundenen Webanwendung ist mit dem
Aufruf von executeScript
also sehr einfach zu bewerkstelligen. Der
Rückgabetyp der aufgerufenen JavaScript-Methoden wird übrigens von der
WebEngine automatisch in bestimmte Java-Datentypen zurückkonvertiert. Hierfür gibt es bestimmte Regeln,
die man am Besten einfach nachliest.
Kommunikation vom Webanwendung zu Desktopanwendung
Es ist tatsächlich sehr einfach, Code der Desktopanwendung aus der Webanwendung aufzurufen (also sogenannte „Upcalls“ durchzuführen). Wir benötigen nur das Wissen, dass man solche Scala-Klassen oder Funktionen an eine ClojureScript-Anwendung übergeben, und von dort aufrufen kann. Jetzt ist es sehr einfach, wir müssen dazu lediglich nur eine Zeile unserer bereits bekannten Scala-Codes modifzieren:
call
erlaubt die Angabe eines Arrays mit Argumenten, die der aufgerufenen JavaScript-Funktion übergeben werden.
Und genau das geschieht jetzt: Es wird die JavaScript-Funktion
simple_app
mit einem Array, welches unsere parameterlose Funktion
{() => System.exit(0)}
zum Beenden der Scala-Anwendung enthält.
Der Button der ClojureScript/React-Anwendung war vorhin noch ohne Funktion - jetzt können wir das ändern:
simple-app
nimmt jetzt noch einen Parameter entgegen, der die
Argumente des Scala-Aufrufes enthält. Das erste Argument enthält
unsere exit
-Funktion, welche dann auch an die
React-Klasse javafx-simple-app
weitergereicht wird. Das bedeutet,
wir können den Scala-Code jetzt in ClojureScript aufrufen! Somit kann auch der Button mit einer
Beenden-Funktionalität versehen werden: Man muss einfach nur beim
Klicken exit
aufrufen.
An dieser Stelle muss allerdings noch auf ein Problem hingewiesen
werden: Der Aufruf von exit
ist synchron. Bei einer Webanwendung mit
Client und Server wäre so ein Aufruf allerdings asynchron. Es ist nun sinnvoll
ClojureScript-Anwendung erstellen, die hier den Scala-Code ansychron
aufrufen, um dann den Wechsel von einer
ClojureScript-Webanwendung innerhalb von JavaFX zu einer „echten“ Webanwendung deutlich zu erleichtern.
Dafür ist allerdings etwas mehr Arbeit notwendig, die Thema eines
künftigen Artikels sein wird.
Parameterübergabe und Tykonvertierung
Etwas knifflig ist noch die Frage, wie man am Besten mit Methoden
umgeht, die Parameter enthalten. Grundsätzlich ist es kein Problem,
denn man kann auch eine Funktion mit Parametern bei einem
call
-Aufruf übergeben und diese dann in ClojureScript mit Argumenten aufrufen.
Das funktioniert auch problemlos für Zahlen, Strings oder Booleans. Bei uns war es jedoch der Fall, dass wir häufig komplexere JavaScript-Objekte als Argumente verwenden wollen. In diesem Fall bietet sich an, mit JSON zu arbeiten: Unsere Datenstrukturen in JavaScript werden nach JSON serialisiert und der Ergebnisstring dann als Parameter übergeben.
Auf Scala-Seite wird dann der String mithilfe von JSON deserialisiert und passend verarbeitet. Somit hat man nicht nur eine klare Schnittstelle - man übergibt immer JSON-Strings - sondern hat auch noch Kontrolle darüber, wie Methodenparameter vearbeitet werden, da die bereits erwähnten automatischen Typkonvertierungsregeln von JavaFX nicht mehr zur Anwendung kommen. Es gibt einige JSON-Bibliotheken für Scala, wir haben beispielsweise gute Erfahrungen mit der Play JSON Bibliothek gemacht.
Zusammenfassung
JavaFX bietet eine komfortable Möglichkeit, Webanwendungen in eine Desktopanwendung zu integrieren. Die Vorteile davon sind vor allem in den größeren Freiheiten bei der grafischen Gestaltung der Anwendung, sowie in der Wiederverwendbarkeit einer so entwickelten Anwendung für „echte“ Webanwendungen. Gerade in Kombination mit ClojureScript/Reacl können wir hier funktionale Programmierung sowohl für die Benutzeroberfläche, als auch für den zugrundeliegenden Scala-Code einsetzen.