Ještě před pár měsíci jsem byl názoru, že Node JS a celý jeho javascriptí ekosystém je svět sám pro sebe a že jakýkoliv pokus ho převzít ze strany jazyků, které vznikaly původně na JVMku, je jen marným počínáním. To by zřejmě byla pravda, pokud bych uvážil např. samostatný Kotlin JS vs. React, kde by React sám o sobě zvítězil na plné čáře. Avšak věci se pomalu začínají měnit od chvíle, kdy byly napsány Kotlin wrappery pro React, včetně „sesterského“ generátoru projektů založených na webpacku s názvem create-react-kotlin-app, které nám nyní umožní… hádejte co? 🙂

Úvod s trochou úžasu a motivace

… ano, umožní nám to používat React a jeho komponenty přímo v Kotlinu. Celé je to dle mého názoru ještě docela horké, nejspíše se to bude měnit a není to úplně production-ready, ale už nyní bych chtěl ukázat pár poznatků a důvodů, proč si myslím, že je to naprostý nářez 🙂 . Rozhodl jsem se to vyzkoušet na jednom malém soukromém projektu, který by defakto mohl být i klasická desktopová javafx aplikace, ale s dnešními možnostmi JS frontendu mi přišlo mnohem jednodušší, robustnější a díky CSS knihovnám (jako je Bootstrap) hlavně na oko pěknější, udělat to celé v JS jakožto frontendu a tuto statickou stránku naservírovat přes minimalistický Javovský webový framework Spark, který na rozdíl od Springu nastartuje zhruba do 1s.

Ještě než začneme, co je tedy na tom celém tak skvělého? Zaprvé, jako jeden z problému JS ekosystému vidím tuny různých knihoven a balíčků, které člověk potřebuje, aby překonal nedostatky ES6 (i když i to je mega pokrok vůči tomu, jak JS vypadal předtím), anebo aby měl alespoň nějakou statickou typovou kontrolu (Flow JS) a aby fungovala, pak by pro všechny tyto tuny knihoven musel mít soubory s informací o typech těch knihoven atd., což je téměř nereálné. Nebylo by lepší, kdyby většinu toho všeho pokryla standardní knihovna Kotlinu a kdyby FlowJS nahradil zcela a úplně mnohem sofistikovanější a již build-in překladač, který tohle už dávno umí a je v tom 100% striktní? Zadruhé, přes všechny snahy Facebooku vyvíjet Atom plugin Nuclide, který je snad po každé aktualizaci třeba přebuildovat, aby s danou verzí Atomu fungoval, je JS tooling v porovnání se staticky typovaným světem ještě stále v plenkách. Atom je pomalý, sem tam se rozbije a je třeba ho přeinstalovat, někdy novější verze Nuclide přestane fungovat s FlowJS atd. atd. atd. …. a upřímně, myslíte si, že bude mít někdy Nuclide tak namakanou a inteligentní autocompletion, refactoring, (téměř) automatickou správu importů a kdo ví, co všechno ještě, jako IntelliJ IDEA? Já si myslím, že nikdy … a navíc, od této chvíle už ho zřejmě asi ani nebude potřeba 🙂 .

Co budeme potřebovat

Pojďme na věc. Budu předpokládat, že jste s ničím podobným ještě zkušenost neměli a tak si to projdeme celé úplně komplet od začátku. Nejdříve si nainstalujte Node JS a yarn. Až to bude, nainstalujeme globálně create-react-kotlin-app:

yarn add global create-react-kotlin-app

Nyní bychom měli být připraveni vygenerovat základní strukturu našeho projektu. Přejděte tedy do nějakého adresáře, kde míváte cvičné projekty a spusťte:

create-react-kotlin-app myfirstkotlinreactapp

Tohle by vám mělo stáhnout vždy aktuální verzi projektové struktury. Až vše yarn dochroupe, svou první webovou aplikaci, kde se Kotlin snoubí s Reactem spustíte pomocí:

cd myfirstkotlinreactapp
yarn start

Tradá a běží! Hot-swap zde sice funguje taky, ale je podstatně pomalejší, než u klasického vývoje ES6, na který jste byli zvyklí. To je asi aktuálně jediná nevýhoda, na kterou by měli hoši ze Sv. Petrohradu ještě zapracovat a já věřím, že to bude jen otázka (ne příliš dlouhého) času.

Level 1: První krůčky

Nastartujte IDEu, otevřete v ní adresář myfirstkotlinreactapp a jdeme na to. Jelikož chci udělat jen jednoduchý průlet několika základními věcmi a principy užití, nebudu zde rozebírat strukturu projektu atd. Zkusme se rovnou učit příkladem a postupným přidáváním. Přesuňte se do třídy App.kt a upravte třídu App, aby vypadala takto:

class App : RComponent<RProps, RState>() {

    override fun RBuilder.render() {
        div("App-header") {
            h2 {
                +"Hi, this is my first React/Kotlin app!"
            }
        }
    }
}

… chvíli počkejte (předpokládám, že máte „yarn start“ stále spuštěný) … chroupy chroup … a vaše stránka by se měla v prohlížeči za několik sekund překreslit. Takže takový náš hello world by byl hotov 🙂 . Jak to celé funguje? Místo HTML tagů tady máme funkce, které berou jako argumenty další lambda funkce a díky Kotlinovským extension functions se jakožto this posílá instance třídy RBuilder, která má tyto funkce jako div, p, h2 atd. definované uvnitř sebe. Díky tomuto stylu je RBuilder vždy dostupný uvnitř každého dalšího „zanoření“ těchto lambda funkcí (snad to dává smysl 🙂 ).

Většina těchto builder metod, co generují HTML tagy, obsahují jako první argument názvy tříd. Takže např. div("App-header") {} vygeneruje <div class="App-header"></div>. Pro někoho nezvyk, ale já bych řekl, že pro takové „malé tagy“ je to šikovnější + se mi líbí, že není nutné tag uzavírat, což za mě řeší složené závorky {}. Dále vám možná přijde divné, proč je tam to + uvnitř h2 tagu. Když si to plusko prokliknu, zjistím, že je to jen přetížený operátor RBuilderu, který podobně jako třeba div() a jiné nepřidá tag, ale čistě jen samostatný String:

public final operator fun kotlin.String.unaryPlus(): kotlin.Unit

Level 2: Zkrášlujeme Bootstrapem

Bez Bootstrapu to prostě není ono a tak si ho nejdříve přidáme. Běžte do adresáře našeho projektu myfirstkotlinreactapp a dejte:

yarn add bootstrap@4.0.0-beta.2

aneb nasypem tam rovnou tu (aktuálně) nejnovější 🙂 . Nyní, aby to fungovalo musíme ještě v naši aplikaci nějak naincludovat CSS soubor Bootstrapu. To přidáme v souboru index.kt takto:

fun main(args: Array<String>) {
    require("bootstrap/dist/css/bootstrap.css")
    requireAll(require.context("src", true, js("/\\.css$/")))

    render(document.getElementById("root")) {
        app()
    }
}

Pak můžeme v naší třídě App přidat např. tohle:

    div("alert alert-success") {
        +"Bootstrap load was a success!"
    }

Pokud bychom chtěli success zvýraznit, zde mi to přijde oproti JSX už trochu horší:

    div("alert alert-success") {
        +"Bootstrap load was a "
        strong { +"success" }
        +"!"
    }

Level 3: Vlastní bezstavová React komponenta

Nadešel čas si trochu „zaReaktit“ a zkusíme si udělat svou první bezstavovou Reaktí komponentu přímo v Kotlinu za použití již dříve zmíněných wrapperů. V tomto případě to bude čistá extension funkce (extension to musí být proto, abychom stále mohli používat metody jako div, h2 atd …, které jsou v třídě RBuilder). Uvnitř package App si vytvořme nový package components a v něm si vytvoříme nový Kotlin soubor FancyNumbers.kt, jehož obsah bude vypadat takto:

package app.components

import react.RBuilder
import react.dom.*

fun RBuilder.fancyNumbers(number: Int) {
    div {
        strong { +"Check out the fancy number versions of $number:" }
        h1 { +"$number" }
        h2 { +"$number" }
        h3 { +"$number" }
        h4 { +"$number" }
    }
}

Nyní můžeme do třídy App tuto komponentu přidat, takže dostaneme:

class App : RComponent<RProps, RState>() {

    override fun RBuilder.render() {
        div("App-header") {
            h2 {
                +"Hi, this is my first React/Kotlin app!"
            }
        }

        div("alert alert-success") {
            +"Bootstrap load was a "
            strong { +"success" }
            +"!"
        }

        fancyNumbers(1)
    }
}

Level 4: Plnohodnotná stateful React komponenta

Nyní vytvoříme vlastni plnohodnotnou React komponentu (soubor FancyCounter.kt), kde nejdříve je třeba vytvořit 2 interfacy, které budou definovat, jaké jsou její props a state (včetně striktně definovaných typů). Dále si pro danou komponentu uděláme builder funkci, jednak jelikož musí být opět „uvnitř“ RBuilder a taky proto, že samotné vytvoření instance React komponenty se dělá pomocí funkce child(), což taktéž není úplně elegantní:

package app.components

import kotlinx.html.ButtonType
import kotlinx.html.js.onClickFunction
import org.w3c.dom.events.Event
import react.*
import react.dom.button
import react.dom.div
import react.dom.h2

interface FancyCounterProps: RProps {
    var startNumber: Int
}

interface FancyCounterState : RState {
    var currentNumber: Int
}

class FancyCounter(props: FancyCounterProps): RComponent<FancyCounterProps, FancyCounterState>(props) {

    override fun FancyCounterState.init(props: FancyCounterProps) {
        currentNumber = props.startNumber
    }

    fun handleIncrementClick(event: Event) = setState { currentNumber++ }

    override fun RBuilder.render() {
        div {
            h2 { +"This counter can increment fancy numbers starting from ${state.currentNumber}" }
            fancyNumbers(state.currentNumber)
            button(type = ButtonType.button, classes = "btn btn-success") {
                attrs.onClickFunction = ::handleIncrementClick
                +"Increment that!"
            }
        }
    }
}

fun RBuilder.fancyCounter(startNumber: Int) = child(FancyCounter::class) {
    attrs.startNumber = startNumber
}

Pak už jen stačí do render metody App třídy přidat např. řádek:

fancyCounter(5)

a už by nám to mělo i pěkně počítat 🙂 .

Level 5: Vlastní wrappery pro již existující externí React komponenty

Ne vždy se chce člověku vynalézat znovu kolo a tak si ukážeme další zajímavou věc, a sice jeden ze způsobů, jakým KotlinJS spolupracuje s tím, co již existuje na straně JavaScriptu. Např. defaultně v našem projektu nemáme k dispozici klasickou funkci alert(), což nám dá chybu při překladu. Ale jelikož víme, že JS takovou funkci má, pak jak tuto skutečnost sdělíme Kotlinu, aby o ní věděl? Dané kouzlo udělá klíčové slovo external. Zadefinujeme-li:

external fun alert(param: Any)

jsme nyní schopni tuto funkci volat kdekoliv v našem kódu a víme, že tato se při kompilaci převede na volání javascriptí alert(). Ale zpět k původní úloze. Zkusme si udělat např. wrapper na react-tooltip a tuto komponentu v naší aplikaci použít. Začněme instalací pomocí yarnu:

yarn add react-tooltip

Nyní vytvoříme soubor ReactTooltip.kt s obsahem:

package app.wrappers

import react.RClass
import react.RProps

@JsModule("react-tooltip")
external val reactTooltip: RClass<RProps>

Anotace @JsModule by údajně měla fungovat jako require() v NodeJS. Už se mi stalo, že samotné „react-tooltip“ u jiných komponent nemusí fungovat, kde bylo řešením dát tam celou cestu k hlavnímu .js souboru – v tomto případě @JsModule("react-tooltip/dist/index.js"). Zde si všimněme, že externí React komponenty se importují jako val, které je typu RClass, jejíž generický typ jsou RProps. Pokud bych chtěl specifikovat, jaké props tato komponenta má mít, můžu si opět udělat např. třídu, který z RProps podědí a pak reactTooltip definovat třeba jako: external val reactTooltip: RClass<ReactTooltipProps>

Nyní když si hlavní App třídu upravím takto, budu mít i fajn tooltipy:

package app

import app.components.fancyCounter
import app.components.fancyNumbers
import app.wrappers.reactTooltip
import react.RBuilder
import react.RComponent
import react.RProps
import react.RState
import react.dom.div
import react.dom.h2
import react.dom.strong

external fun alert(param: Any)

class App : RComponent<RProps, RState>() {

    override fun RBuilder.render() {
        alert("Welcome!")

        div("App-header") {
            h2 {
                +"Hi, this is my first React/Kotlin app!"
            }
        }

        div("alert alert-success") {
            attrs["data-tip"] = "A success tooltip!"
            attrs["data-type"] = "success"

            +"Bootstrap load was a "
            strong { +"success" }
            +"!"
        }

        fancyNumbers(1)

        fancyCounter(5)

        reactTooltip {}
    }
}

fun RBuilder.app() = child(App::class) {}

Závěr

Tak a máme hotovo 🙂 . Celkově to celé není až tak jednoduché, jak by se mohlo zdát a jsou ještě věci, se kterýma si poradit neumím + někdy to člověka i docela dobře potrápí, než přijde na to, v čem byla chyba, ale ty výhody, které to přináší včetně full-blown toolingu, který IDEA poskytuje, jsou něco, co změnilo můj názor, že Kotlin JS je slepá vývojová větev. Teď už jen zbývá, aby krom Googlu Kotlin oficiálně podpořil i samotný Facebook a bude to tam 🙂 .

Tento článek bych ukončil větou: To, že Kotlin ovládne Android – je téměř jisté, JVM svět – pravděpodobné, a svět frontendu v JavaScriptu – možné! 🙂