Fragen? Jetzt Anruf vereinbaren!

Erste Schritte in Kotlin als Javascriptersatz

Kotlin lässt sich schon länger nach JavaScript kompilieren. Damit ist es möglich, eine weitere Sprache für die Entwicklung im Browser zu verwenden. Dies kann, vor allem für Projekte die schon Kotlin im Backend einsetzen, eine interessante Alternative sein. Der Artikel stellt die ersten Schritte zu einem lauffähigen KotlinJS-Projekt dar.

Das ist meine Reise zu meiner ersten Webanwendung in Kotlin.

Zuerst habe ich den Rahmen festgelegt, in dem sich das Projekt bewegen soll:

Initialisieren

Als Erstes generiere ich ein Projekt mit IntelliJ vom Typ Kotlin/JS. Als das Projekt vorhanden ist, folgt erst einmal die Ernüchterung – es wird nur wenig angelegt. Es lohnt sich also, erst einmal die Dokumentation zu lesen und einen einsteigerfreundlicheren Weg zu finden. Nach dem Lesen der offiziellen Dokumentation und einiger Beispiele entscheide ich mich für Gradle. Die Entscheidung für WebGL fällt auf ThreeJS und einen Kotlin-Wrapper, three.kt.

Die Einrichtung eines Gradle-Projekts ist sehr einfach: Man installiert Gradle (z.B. mit Chocolatey), erzeugt das Projektverzeichnis und ruft auf:

gradle init  

Dadurch wird die erste Struktur angelegt. Diese muss jetzt noch mit Leben gefüllt werden. Dabei ist das Beispiel in three.kt sehr hilfreich.

/*
 * This file was generated by the Gradle 'init' task.
 *
 * This is a general purpose Gradle build.
 * Learn how to create Gradle builds at <https://guides.gradle.org/creating-new-gradle-builds/>
 */
group 'net.npg'
version '0.1'

buildscript {
    ext.kotlin_version = '1.2.71'
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

apply plugin: 'kotlin2js'

repositories {
    mavenCentral()
}

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlin_version"
    compile "info.laht.threekt:wrapper:0.88-ALPHA-6"
}

Ein erster Versuch auf der Kommandozeile brachte schon mal keinen Fehler:

c:\development\ballrunner> gradle compileKotlin2Js
BUILD SUCCESSFUL in 1s

Anschließend lässt sich das Gradle-Projekt ganz einfach in IntelliJ importieren. Empfehlenswert ist es natürlich, die Projekte sofort in Git zu versionieren, um eine Sicherung der Historie zu haben. Wenn man Projekte in Git anlegt, empfiehlt es sich auch, eine „.gitignore“-Datei anzulegen. Dies vermeidet, dass man unabsichtlich nicht gemeinsam nutzbare Dateien in Git einspielt, wie lokale Build-Dateien oder Dateien die von der IDE generiert wurden.

Da das Projekt noch leer ist, läuft auch der Build in IntelliJ durch. Jetzt brauchen wir etwas Code. Dazu nehme ich ein einfaches Beispiel aus three.kt und lösche alles, was nicht benötigt wird. Danach bleiben folgende wichtigen Dateien übrig:

* src
 * main
  * kotlin
	* main.kt  # Die Kotlin-Datei, die die Applikation beherbergen soll. 
* web
 * ballrunner.html # Die Html-Seite, die für dem Start der Applikation benötigt wird.

Der erste Compile-Vorgang mit diesen Dateien erzeugt folgende Struktur im Build-Verzeichnis:

* build
 * classes
  * kotlin
   * main
	* ballrunner.js  # Das ist die aus main.kt erzeugte Javascript-Datei.
   * lib
	* kotlin.js      # Eine Datei mit alle Funktionen, die Kotlin im Javascript-Umfeld zur Verfügung stellt.
    * wrapper.js     # Javascript-Code, der aus dem three.kt –Wrapper erzeugt wurde.

Aber wie führt man das jetzt aus? Für WebGL brauchen wir einen Browser. Aus IntelliJ kann man html-Seiten direkt im Browser öffnen. Dazu wählt man mit der rechten Maustaste auf ballrunner.html den Punkt „Open in Browser“ aus.

Open in Browser

Open in Browser

Aber wieso sieht man nichts? Das Chrome Entwicklertool zeigt sofort, dass die kompilierten Javascript-Dateien fehlen. Tatsächlich wird beim Aufruf von gradle assemble eine jar-Datei erzeugt, was im Webumfeld nicht gerade hilfreich ist. Gut, dass dies in den Tutorials von Kotlinlang.org gleich am Anfang beschrieben wird. Also build.gradle um den Task assembleWeb erweitern, starten und schon läuft es im Browser!

task assembleWeb(type: Sync) {
    configurations.compile.each { File file ->
        from(zipTree(file.absolutePath), {
            includeEmptyDirs = false
            include { fileTreeElement ->
                def path = fileTreeElement.path
                path.endsWith(".js") && (path.startsWith("META-INF/resources/") ||
                    !path.startsWith("META-INF/"))
            }
        })
    }
    from compileKotlin2Js.destinationDir
    into "${projectDir}/web"

    dependsOn classes
}

assemble.dependsOn assembleWeb

Testen

Der nächste Schritt ist das Testen. Die Tests können ebenfalls in Kotlin geschrieben werden, die Ausführungsumgebung ist aber weiterhin in JavaScript. Hier ist anzumerken, dass auch das Debugging unter JavaScript erfolgt. Der erzeugte Code ist zwar leserlich, aber nicht ideal. Mit Hilfe von Source-Maps lässt sich dies aber umgehen.

Eine Alternative wäre das Aufsetzen einer Multiplattform-Umgebung. Dann könnte man große Teile des Codes z.B. direkt in Java- oder Kotlin-Umgebungen testen, was aber nur bedingt sinnvoll ist, da man ja wissen will, ob die Applikation in der Zielplattform läuft.

Wie testet man jetzt eine KotlinJS Anwendung?

Wenn man aus der Java-Welt kommt, denkt man hier zuerst an JUnit oder TestNG. Leider ist es nicht ganz so einfach, denn Java-Bibliotheken lassen sich ja nicht einbinden. Diese müssen unter der Javascript-VM ausführbar sein. Das ist auch der größte Schwachpunkt von KotlinJS – man kann nicht einfach auf den großen Pool von Java-Bibliotheken zugreifen. Eine einfache Lösung dafür gibt es derzeit nicht. Um eine Java-Bibliothek zu verwenden, könnte man sie entweder nach Kotlin konvertieren (und dann nach JavaScript) oder ein Framework wie TeaVM verwenden.

Aber Kotlin bietet schon eine einfache Testbibliothek: kotlin-test-js – somit kann man auf ein Java-Testframework verzichten. Ein einfacher Testfall sieht sehr vertraut aus:

import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull

class TestFieldView {

    @Test
    fun testInit() {
        val baseField = EndlessField(5)
        val fieldView = FieldView(5, 5, baseField)
        assertNotNull(fieldView)
        assertEquals(5, fieldView.xsize)
    }
}

Aber wie führt man diesen Testfall aus?

Dazu wird ein Testrunner-Framework benötigt. Hier stehen mehrere zur Verfügung und es gibt direkt von Jetbrains Beispiele, wie man diese verwendet. Ich habe mich erst einmal für Qunit entschieden. Direkt in IntelliJ lassen sich die Tests nur in der Ultimate Edition ausführen, aber ich wollte die Tests auch extern über Gradle starten können. Dafür ist etwas mehr zu tun.

Um mit Qunit zu testen, benötigt man neben dem Testframework auch npm zur einfachen Installation der JavaScript Pakete und node.js zur Ausführung der Testumgebung. Und schon ist man mitten in der JavaScript-Welt, ganz vermeiden lässt es sich also nicht. Gradle und seine Vielzahl von Plugins nehmen aber einem die Arbeit ab. Als erstes muss die Testbibliothek hinzugefügt werden:

testCompile "org.jetbrains.kotlin:kotlin-test-js:$kotlin_version"

Als node.js-Plugin verwende ich com.moowork.node, man kann aber auch de.solugo.gradle.nodejs verwenden (siehe dazu diesen Artikel).

Während die Konfiguration in Gradle mit Hilfe der Beispiele sehr schnell ging, zeigt sich hier ein Schwachpunkt von KotlinJS: Man muss sehr viel von Hand machen, um es zum Laufen zubekommen. Aber es gibt eine Abhilfe: kotlin-frontend. Damit kann man mit wenigen Zeilen eine Build-Datei zur Frontendentwicklung konfigurieren. Leider hatte ich dort ein paar Schwierigkeiten mit der threekt-Bibliothek, die auch mit dem manuellen Ansatz nicht einfach zu lösen waren. Der Bibliothek fehlt der entsprechende Modulimport (require) auf three.js, so dass die Laufzeitumgebung diese JavaScript-Bibliothek automatisch einbinden kann. Dazu muss die Bibliothek verändert werden, was den Rahmen des Artikels sprengt. Die offiziellen Wrapper haben die entsprechenden Imports und können somit direkt verwendet werden.

Die vorläufige Gradle-Konfiguration für die Tests sieht noch recht übersichtlich aus:

node {
    download = true
}

task populateNodeModules(type: Copy, dependsOn: compileKotlin2Js) {
    from compileKotlin2Js.destinationDir

    configurations.testCompile.each {
        from zipTree(it.absolutePath).matching { include '*.js' }
    }
    into "${buildDir}/node_modules"
}

task installQunit(type: NpmTask) {
    args = ['install', 'qunitjs']
}

task runQunit(type: NodeTask, dependsOn: [compileKotlin2Js, compileTestKotlin2Js, populateNodeModules, installQunit]) {
    script = file('node_modules/qunitjs/bin/qunit')
    args = [projectDir.toPath().relativize(file(compileTestKotlin2Js.outputFile).toPath())]
}

test.dependsOn runQunit

Wenn man jetzt mit Gradle die Test ausführen will, werden folgende Aktionen ausgeführt:

Hiermit kann man schon einfache Tests schreiben. Interessant wird es aber, wenn man 3rd-Party Bibliotheken wie ThreeJS einsetzt. Diese werden erst einmal nicht gefunden, wenn der Wrapper diese nicht als dependecy ausweist:

not ok 1 build/classes/kotlin/test/ballrunner_test.js > Failed to load the test file with error:
ReferenceError: THREE is not defined

Die three.js Bibliothek wurde vorher über die HTML-Seite direkt aus dem Internet eingebunden, somit kann dies nicht funktionieren. Dazu wird jetzt three.js parallel zu Qunit über den npm-Task installiert. Dies reicht aber nicht – weiterhin wird ein kleiner Testwrapper in Javascript benötigt, der zuerst three.js und danach die von KotlinJS erzeugte Test-JavaScript-Datei einbindet. Diese wird dann von Qunit aufgerufen. Damit das alles ohne Verrenkungen bei den Dateipfaden funktioniert, werden alle Dateien in ein Verzeichnis kopiert. Dort durchlaufen sie die ersten Tests:

> Task :runQunit
TAP version 13
ok 1  > TestFieldView > testInit
   Hello World
ok 2  > Test > testInit
1..2
# pass 2
# skip 0
# todo 0
# fail 0

Das vollständige Beispiel ist auf Github verfügbar.

Fazit

Eine einfache Applikation zum Laufen zu bekommen, ist mit Kotlin/JS sehr einfach. Wie man beim Testen sieht, sind bald schnell tiefere Kenntnisse gefragt. Auch die fehlende Unterstützung von Java-Bibliotheken ist ein großer Nachteil, der sich nicht einfach abstellen lässt. Für Kotlin/JS sprechen aber auch folgende Gründe:

Welche Erfahrungen haben Sie mit der Entwicklung unter Kotlin? Teilen Sie Ihre Erkenntnisse mit mir, ich freue mich auf Ihre Nachricht.

Roland Spatzenegger

Software Architekt
Telefon: +49-89-5307 44 520
E-Mail: spatzenegger@4soft.de
4Soft GmbH
Mittererstraße 3
80336 München
Bildnachweis Titel: © kras99, Adobe Stock