First steps in Kotlin as a javascript replacement
Kotlin has been compiling to JavaScript for some time. This makes it possible to use another language for development in the browser. This can be an interesting alternative, especially for projects that already use Kotlin in the backend. The article presents the first steps to a runnable KotlinJS project.
This is my journey to my first web application in Kotlin.
First, I set the framework in which the project should operate:
- The development environment should be IntelliJ to avoid buggy or incomplete language support from the beginning.
- The Kotlin version should be stable, i.e. not a beta version. The current version at the time of this article is 1.3.0.
- A JavaScript library should also be used to test the interaction between Kotlin and JavaScript.
- It must be possible to write and run tests.
- The application must be buildable and testable outside of IntelliJ.
Initialize
First I generate a project with IntelliJ of type Kotlin/JS. When the project is available, the disillusionment follows first - only little is created. So it is worth to read the documentation first and find a more beginner-friendly way. After reading the official documentation and some examples, I decide to use Gradle. The decision for WebGL comes down to ThreeJS and a Kotlin wrapper, three.kt.
Setting up a Gradle project is very simple: you install Gradle (e.g. with Chocolatey), create the project directory and call :
gradle init
This creates the first structure. This must now be filled with life. The example in three.kt is very helpful here.
build.gradle
1 /*
2 * This file was generated by the Gradle 'init' task.
3 *
4 * This is a general purpose Gradle build.
5 * Learn how to create Gradle builds at <https://guides.gradle.org/creating-new-gradle-builds/>
6 */
7 group 'net.npg'
8 version '0.1'
9
10 buildscript {
11 ext.kotlin_version = '1.2.71'
12 repositories {
13 mavenCentral()
14 }
15 dependencies {
16 classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
17 }
18 }
19
20 apply plugin: 'kotlin2js'
21
22 repositories {
23 mavenCentral()
24 }
25
26 dependencies {
27 compile "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlin_version"
28 compile "info.laht.threekt:wrapper:0.88-ALPHA-6"
29 }
A first try on the command line did not bring any error:
c:\development\ballrunner> gradle compileKotlin2Js
BUILD SUCCESSFUL in 1s
Afterwards, the Gradle project can be easily imported into IntelliJ. Of course, it is recommended to version the projects immediately in Git to have a backup of the history. If you create projects in Git, it is also recommended, create a “.gitignore” file. This avoids inadvertently putting non-shareable files into Git, such as local build files or files generated by the IDE.
Since the project is still empty, the build also runs through in IntelliJ. Now we need some code. For this, I’ll take a simple example from three.kt and delete everything that is not needed. After that the following important files remain:
* 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.
The first compile operation with these files creates the following structure in the build directory:
* build
* classes
* kotlin
* main
* ballrunner.js # This is the javascript file generated from main.kt.
* lib
* kotlin.js # A file with all functions Kotlin provides in the Javascript environment.
* wrapper.js # Javascript code generated from the three.kt wrapper.
But how do you run it now? For WebGL we need a browser. From IntelliJ you can open html-pages directly in the browser. To do this, right-click on ballrunner.html
and select “Open in Browser”.
But why can’t you see anything? The Chrome developer tool immediately shows that the compiled javascript files are missing. In fact, calling gradle assemble
creates a jar file, which is not very helpful in the web environment. Good that this is described in the tutorials of Kotlinlang.org right at the beginning. So extend build.gradle
with the task assembleWeb
, start it and it runs in the browser!
build.gradle
1 task assembleWeb(type: Sync) {
2 configurations.compile.each { File file ->
3 from(zipTree(file.absolutePath), {
4 includeEmptyDirs = false
5 include { fileTreeElement ->
6 def path = fileTreeElement.path
7 path.endsWith(".js") && (path.startsWith("META-INF/resources/") ||
8 !path.startsWith("META-INF/"))
9 }
10 })
11 }
12 from compileKotlin2Js.destinationDir
13 into "${projectDir}/web"
14
15 dependsOn classes
16 }
17
18 assemble.dependsOn assembleWeb
Testing
The next step is testing. The tests can also be written in Kotlin, but the execution environment is still in JavaScript. It should be noted here that debugging is also done under JavaScript. The generated code is readable, but not ideal. With the help of source maps this can be avoided.
An alternative would be to set up a multiplatform environment. Then one could test large parts of the code e.g. directly in Java or Kotlin environments, which is however only conditionally meaningful, since one wants to know whether the application runs in the target platform.
How do you test a KotlinJS application now?
If you come from the Java world, the first thing that comes to mind here is JUnit or TestNG. Unfortunately it is not that simple, because Java libraries cannot be merged. These must be executable under the Javascript VM. That is also the largest weak point of KotlinJS - one cannot cannot simply access the large pool of Java libraries. There is currently no easy solution to this. To use a Java library, you could either convert it to Kotlin (and then to JavaScript) or use a framework like TeaVM.
But Kotlin already provides a simple test library: kotlin-test-js - so you can do without a Java test framework. A simple test case looks very familiar:
A test case in kotlin
1 import kotlin.test.Test
2 import kotlin.test.assertEquals
3 import kotlin.test.assertNotNull
4
5 class TestFieldView {
6
7 @Test
8 fun testInit() {
9 val baseField = EndlessField(5)
10 val fieldView = FieldView(5, 5, baseField)
11 assertNotNull(fieldView)
12 assertEquals(5, fieldView.xsize)
13 }
14 }
But how do you execute this test case?
For this you need a testrunner framework. Here are several available and there are directly from Jetbrains examples how to use them. use them. I decided to use Qunit for now. Directly in IntelliJ the tests can only be run in the Ultimate Edition, but I wanted to be able to run the tests externally via Gradle. There is a bit more to do for this.
To test with Qunit, in addition to the test framework, you also need npm to easily install the JavaScript packages and node.js to run the test environment. And already you are in the middle of the JavaScript world, so it can’t be avoided completely. However, Gradle and its multitude of plugins do the work for you. The first thing to do is to add the test library:
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).
While the configuration in Gradle was very fast with the help of the examples, a weak point of KotlinJS shows up here: You have to do a lot by hand, to get it to work. But there is a workaround: kotlin-frontend. With this you can configure with a few lines a build file for frontend development. Unfortunately I had some difficulties with the threekt library, which could not be solved easily with the manual approach. easy to solve even with the manual approach. The library lacks the appropriate module import (require) on three.js so that the runtime environment can automatically include this JavaScript library. For this the library must be modified, which is beyond the scope of this article. The official wrappers have the appropriate imports and can thus be used directly.
The preliminary Gradle configuration for the tests still looks quite clear:
build.gradle
1 node {
2 download = true
3 }
4
5 task populateNodeModules(type: Copy, dependsOn: compileKotlin2Js) {
6 from compileKotlin2Js.destinationDir
7
8 configurations.testCompile.each {
9 from zipTree(it.absolutePath).matching { include '*.js' }
10 }
11 into "${buildDir}/node_modules"
12 }
13
14 task installQunit(type: NpmTask) {
15 args = ['install', 'qunitjs']
16 }
17
18 task runQunit(type: NodeTask, dependsOn: [compileKotlin2Js, compileTestKotlin2Js, populateNodeModules, installQunit]) {
19 script = file('node_modules/qunitjs/bin/qunit')
20 args = [projectDir.toPath().relativize(file(compileTestKotlin2Js.outputFile).toPath())]
21 }
22
23 test.dependsOn runQunit
Now when you want to run the test with Gradle, the following actions are performed:
- Node.js is installed locally if necessary.
- Qunit is installed using npm.
- The main and test Kotlin sources are compiled to JavaScript.
- The generated files are then copied to node_modules.
- Qunit is executed on the generated test JavaScript files.
With this you can already write simple tests. It becomes interesting however, if one uses 3rd party libraries such as ThreeJS. These are not found at first, if the wrapper does not identify them as dependecy:
not ok 1 build/classes/kotlin/test/ballrunner_test.js > Failed to load the test file with error:
ReferenceError: THREE is not defined
The three.js library was previously included via the HTML page directly from the Internet, so this cannot work. Therefore three.js is installed parallel to Qunit via the npm task. But this is not enough - furthermore a small test wrapper in Javascript is needed, which first installs three.js and then the test JavaScript file generated by KotlinJS. This is then called by Qunit. To make sure that all this works without any dislocations in the file paths, all files are copied into one directory. There they run through the first 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
The full example is available on Github.
Conclusion
Getting a simple application to work is very easy with Kotlin/JS. As you can see in testing, deeper knowledge is soon required. The lack of Java library support is also a major drawback that cannot be easily turned off. However, the following reasons also speak for Kotlin/JS:
- It is much more pleasant to develop in Kotlin than in JavaScript - for this, one only has to recall the many stumbling blocks of JavaScript.
- Client/server projects could have great potential, since both the client and the server can be developed in the same programming language. This allows, for example, code to be reused, client code to be executed directly in the JVM and tested there, etc.