Die Idee, die Front auf ein beliebiges js-Framework zu übertragen, entstand gleichzeitig mit der Möglichkeit, React in Kotlin zu schreiben. Und ich beschloss, es zu versuchen. Das Hauptproblem: wenige Materialien und Beispiele (ich werde versuchen, diese Situation zu korrigieren). Aber ich habe vollständige Eingabe, furchtloses Refactoring, alle Funktionen von Kotlin und vor allem den allgemeinen Code für das Backend in der JVM und die Front in Javascript.
In diesem Artikel werden wir eine Seite über Javasript + React parallel zu seinem Gegenstück zu Kotlin + React schreiben. Um den Vergleich ehrlich zu machen, habe ich dem Javasript das Tippen hinzugefügt.

Das Hinzufügen von Eingabe zu Javascript war nicht so einfach. Wenn ich für Kotlin Gradle, Npm und Webpack brauchte, brauchte ich für Javascript Npm, Webpack, Flow und Babel mit Presets React, Flow, ES2015 und Stage-2. Gleichzeitig ist der Fluss hier irgendwie auf der Seite, und Sie müssen ihn separat ausführen und separat mit der IDE befreundet sein. Wenn Sie die Baugruppe und dergleichen in Klammern setzen, bleibt für das direkte Schreiben von Code einerseits Kotlin + React und andererseits Javascript + React + babel + Flow + ES5 | ES6 | ES7 erhalten.
In unserem Beispiel erstellen wir eine Seite mit einer Liste von Autos und der Möglichkeit, nach Marke und Farbe zu filtern. Wir filtern die Marke und Farbe, die wir beim ersten Start einmal von hinten ziehen. Die ausgewählten Filter werden in der Abfrage gespeichert. Wir zeigen Autos auf einem Teller. In meinem Projekt geht es nicht um Autos, aber die Gesamtstruktur ähnelt im Allgemeinen der, mit der ich regelmäßig arbeite.
Das Ergebnis sieht folgendermaßen aus (ich werde kein Designer sein):

Ich werde die Konfiguration dieser gesamten Shaitan-Maschine hier nicht beschreiben, dies ist ein Thema für einen separaten Artikel (im Moment können Sie die Quellen aus diesem rauchen).
Laden von Daten von hinten
Zuerst müssen Sie die Marken und verfügbaren Farben von hinten laden.
Javascript
| Kotlin
|
---|
class Home extends React.Component <ContextRouter, State>{ state = { loaded: false,
| class Home( props: RouteResultProps<*> ) : RComponent <RouteResultProps<*>, State> (props) { init { state = State( color = queryAsMap( props.location.search )["color"], brand = queryAsMap( props.location.search )["brand"] ) } override fun componentDidMount() { launch { updateState { //(3) brands = fetchJson( //(4) "/api/brands", StringSerializer.list ) colors = fetchJson( //(4) "/api/colors", StringSerializer.list ) } } } } class State( var color: String?, //(5) var brand: String? //(5) ) : RState { var loaded: Boolean = false //(1) lateinit var brands: List<String> //(2) lateinit var colors: List<String> //(2) } private val serializer: JSON = JSON() suspend fun <T> fetchJson( //(4) url: String, kSerializer: KSerializer<T> ): T { val json = window.fetch(url) .await().text().await() return serializer.parse( kSerializer, json ) }
|
Es sieht sehr ähnlich aus. Aber es gibt Unterschiede:
- Standardwerte können an derselben Stelle geschrieben werden, an der der Typ deklariert ist. Dies erleichtert die Aufrechterhaltung der Code-Integrität.
- Mit lateinit können Sie überhaupt keinen Standardwert für das festlegen, was später geladen wird. Während der Kompilierung wird eine solche Variable als NotNull betrachtet, aber jedes Mal, wenn sie überprüft wird, wird sie gefüllt und ein für Menschen lesbarer Fehler wird generiert. Dies gilt insbesondere für ein komplexeres Objekt als ein Array. Ich weiß, dass das gleiche mit Flow erreicht werden kann, aber es ist so umständlich, dass ich es nicht versucht habe.
- kotlin-react out of the box bietet die Funktion setState, lässt sich jedoch nicht mit Coroutinen kombinieren, da es nicht inline ist. Ich musste kopieren und inline setzen.
- Eigentlich Coroutinen . Dies ist ein Ersatz für Async / Warten und vieles mehr. Zum Beispiel wird durch sie Ausbeute erzielt. Interessanterweise wird der Syntax nur das Wort suspend hinzugefügt, alles andere ist nur Code. Daher mehr Nutzungsfreiheit. Und eine etwas strengere Kontrolle auf der Kompilierungsebene. Sie können componentDidMount daher nicht mit einem
suspend
Modifikator überschreiben, was logisch ist: componentDidMount ist eine synchrone Methode. Sie können den asynchronen Startblock launch { }
beliebigen Stelle im Code einfügen. Es ist möglich, eine asynchrone Funktion in einem Parameter oder Feld einer Klasse explizit zu akzeptieren (unten ist ein Beispiel aus meinem Projekt). - In Javascript ist weniger Kontrolle nullbar. Im resultierenden Zustand können Sie also die Nullbarkeit der Marke, der Farbe und der geladenen Felder ändern, und alles wird gesammelt. In der Kotlin-Version treten berechtigte Kompilierungsfehler auf.
Paralleles Trekking mit Corutin suspend fun parallel(vararg tasks: suspend () -> Unit) { tasks.map { async { it.invoke() }
Laden Sie nun die Maschinen von hinten mit den Filtern aus der Abfrage
JS:
async loadCars() { let url = `/api/cars?brand=${this.state.brand || ""}&color=${this.state.color || ""}`; this.setState({ cars: await (await fetch(url)).json(), loaded: true }); }
Kotlin:
private suspend fun loadCars() { val url = "/api/cars?brand=${state.brand.orEmpty()}&color=${state.color.orEmpty()}" updateState { cars = fetchJson(url, Car::class.serializer().list)
Ich möchte auf
Car::class.serializer().list
. Tatsache ist, dass jetBrains eine Bibliothek für die Serialisierung / Deserialisierung geschrieben hat, die für JVM und JS gleich funktioniert. Erstens weniger Probleme und Code für den Fall, dass sich das Backend auf der JVM befindet. Zweitens wird die Gültigkeit des eingehenden JSON während der Deserialisierung und nicht irgendwann während des Aufrufs überprüft. Wenn Sie also die Version des Back-End ändern und im Prinzip integrieren, werden die Probleme schneller.
Wir zeichnen eine Kappe mit Filtern
Wir schreiben eine zustandslose Komponente, um zwei Dropdown-Listen anzuzeigen. Im Fall von Kotlin ist es nur eine Funktion, im Fall von js ist es eine separate Komponente, die vom React Loader während der Montage generiert wird.
Javascript
| Kotlin
|
---|
type HomeHeaderProps = { brands: Array<string>, brand?: string, onBrandChange: (string) => void, colors: Array<string>, color?: string, onColorChange: (string) => void } const HomeHeader = ({ brands, brand, onBrandChange, colors, color, onColorChange }: HomeHeaderProps) => ( <div> Brand: <Dropdown value={brand} onChange={e => onBrandChange(e.value) } options={withDefault("all", brands.map(value => ({ label: value, value: value })))} /> Color: <Dropdown value={color} onChange={e => onColorChange(e.value) } options={withDefault("all", colors.map(value => ({ label: value, value: value })))} /> </div> ); function withDefault( label, options ) { options.unshift({ label: label, value: null }); return options; }
| private fun RBuilder.homeHeader( brands: List<String>, brand: String?, onBrandChange: (String?) -> Unit, colors: List<String>, color: String?, onColorChange: (String?) -> Unit ) { +"Brand:" dropdown( value = brand, onChange = onBrandChange, options = brands.map { SelectItem( label = it, value = it ) } withDefault "all" ) {} +"Color:" dropdown( value = color, onChange = onColorChange, options = colors.map { SelectItem( label = it, value = it ) } withDefault "all" ) {} } infix fun <T : Any> List<SelectItem<T>>.withDefault( label: String ) = listOf( SelectItem( label = label, value = null ) ) + this
|
Das erste, was auffällt - HomeHeaderProps im JS-Teil - wir sind gezwungen, die eingehenden Parameter separat zu deklarieren. Unbequem.
Die Syntax von Dropdown hat sich etwas geändert. Ich
benutze hier primereact , natürlich musste ich einen Kotlin-Wrapper schreiben. Einerseits ist dies überflüssige Arbeit (Gott sei Dank gibt es
ts2kt ), andererseits ist es eine Gelegenheit, die API an
einigen Stellen bequemer zu machen.
Nun, ein wenig syntaktischer Zucker bei der Bildung von Dropdown-Elementen.
})))}
in der js-Version sieht es interessant aus, aber es spielt keine Rolle. Das Begradigen der Wortfolge ist jedoch viel schöner: "Wir wandeln die Farben in Elemente um und fügen standardmäßig" alle "hinzu, anstatt" alle "zu den in Elemente konvertierten Farben hinzuzufügen". Es scheint ein kleiner Bonus zu sein, aber wenn Sie mehrere solcher Coups hintereinander haben ...
Wir speichern Filter in Abfragen
Wenn Sie nun Filter nach Marke und Farbe auswählen, müssen Sie den Status ändern, die Autos von hinten laden und die URL ändern.
Javascript
| Kotlin
|
---|
render() { if (!this.state.loaded) return null; return ( <HomeHeader brands={this.state.brands} brand={this.state.brand} onBrandChange={brand => this.navigateToChanged({brand})} colors={this.state.colors} color={this.state.color} onColorChange={color => this.navigateToChanged({color})} /> ); } navigateToChanged({ brand = this.state.brand, color = this.state.color }: Object) { //(*) this.props.history.push( `?brand=${brand || ""}` + `&color=${color || ""}`); this.setState({ brand, color }); this.loadCars() }
| override fun RBuilder.render() { if (!state.loaded) return homeHeader( brands = state.brands, brand = state.brand, onBrandChange = { navigateToChanged(brand = it) }, colors = state.colors, color = state.color, onColorChange = { navigateToChanged(color = it) } ) } private fun navigateToChanged( brand: String? = state.brand, color: String? = state.color ) { props.history.push( "?brand=${brand.orEmpty()}" + "&color=${color.orEmpty()}") updateState { this.brand = brand this.color = color } launch { loadCars() } }
|
Und auch hier das Problem mit den Standardwerten der Parameter. Aus irgendeinem Grund erlaubte mir Flow nicht, gleichzeitig eine Typisierung, einen Destruktor und einen Standardwert aus dem Status zu übernehmen. Vielleicht nur ein Fehler. In diesem Fall wäre es jedoch erforderlich, den Typ außerhalb der Klasse zu deklarieren, d. H. im Allgemeinen ein Bildschirm höher oder niedriger.
Zeichne einen Tisch
Als letztes müssen wir eine zustandslose Komponente schreiben, um die Tabelle mit den Maschinen zu rendern.
Javascript
| Kotlin
|
---|
const HomeContent = (props: { cars: Array<Car> }) => ( <DataTable value={props.cars}> <Column header="Brand" body={rowData => rowData["brand"] }/> <Column header="Color" body={rowData => <span style={{ color: rowData['color'] }}> {rowData['color']} </span> }/> <Column header="Year" body={rowData => rowData["year"]} /> </DataTable> );
| private fun RBuilder.homeContent( cars: List<Car> ) { datatable(cars) { column(header = "Brand") { +it.brand } column(header = "Color") { span { attrs.style = js { color = it.color } +it.color } } column(header = "Year") { +"${it.year}" } } }
|
Hier können Sie sehen, wie ich API-Primefaces begradigt habe und wie man Kotlin-React stylt. Dies ist reguläres json, wie in der js-Version. In meinem Projekt habe ich einen Wrapper erstellt, der gleich aussieht, aber stark getippt ist, so viel wie möglich mit HTML-Stilen.
Fazit
Sich auf neue Technologien einzulassen, ist riskant. Es gibt nur wenige Anleitungen, es gibt nichts über den Stapelüberlauf, einige grundlegende Dinge fehlen. Aber im Fall von Kotlin haben sich meine Kosten ausgezahlt.
Während ich diesen Artikel vorbereitete, lernte ich eine Reihe neuer Dinge über modernes Javascript: Flow, Babel, Async / Warten, JSX-Vorlagen. Ich frage mich, wie schnell dieses Wissen veraltet ist. Und all dies ist nicht notwendig, wenn Sie Kotlin verwenden. Gleichzeitig müssen Sie ein wenig über React wissen, da die meisten Probleme mit Hilfe der Sprache leicht gelöst werden können.
Und was denkst du darüber, diesen ganzen Zoo durch eine Sprache mit einem großen Satz Brötchen zu ersetzen?
Für Interessierte
Quellcodes .
PS: Pläne, Artikel über Konfigurationen, Integration mit JVM und über DSL zu schreiben, die sowohl reaktionsfähiges als auch reguläres HTML bilden.
Bereits geschriebene Artikel über Kotlin:
Der Nachgeschmack von Kotlin, Teil 1Der Nachgeschmack von Kotlin, Teil 2Nachgeschmack von Kotlin, Teil 3. Coroutinen - Prozessorzeit teilen