L'idée de transférer le front vers n'importe quel framework js est apparue simultanément avec la possibilité d'écrire React dans Kotlin. Et j'ai décidé d'essayer. Le problème principal: peu de matériel et d'exemples (je vais essayer de corriger cette situation). Mais j'ai une saisie complète, une refactorisation intrépide, toutes les fonctionnalités de Kotlin, et surtout, le code général pour le backend sur la JVM et le front sur Javascript.
Dans cet article, nous allons écrire une page sur Javasript + React en parallèle avec son homologue sur Kotlin + React. Pour rendre la comparaison honnête, j'ai ajouté la saisie au Javasript.

Ajouter de la frappe à Javascript n'a pas été si simple. Si pour Kotlin j'avais besoin de gradle, npm et webpack, pour Javascript j'avais besoin de npm, webpack, flow et babel avec les presets react, flow, es2015 et stage-2. En même temps, le flux ici est en quelque sorte sur le côté, et vous devez l'exécuter séparément et séparément être ami avec l'IDE. Si vous mettez l'assemblage entre parenthèses et autres, alors pour l'écriture directe de code, d'une part, Kotlin + React reste, et d'autre part Javascript + React + babel + Flow + ES5 | ES6 | ES7.
Pour notre exemple, nous allons créer une page avec une liste de voitures et la possibilité de filtrer par marque et couleur. Nous filtrons la marque et la couleur que nous glissons de l'arrière une fois lors du premier démarrage. Les filtres sélectionnés sont enregistrés dans la requête. Nous exposons les voitures dans une assiette. Mon projet ne concerne pas les voitures, mais la structure globale est généralement similaire à celle avec laquelle je travaille régulièrement.
Le résultat ressemble à ceci (je ne serai pas designer):

Je ne décrirai pas la configuration de toute cette machine shaitan ici, c'est un sujet pour un article séparé (pour l'instant, vous pouvez fumer les sources de celui-ci).
Chargement des données par l'arrière
Vous devez d'abord charger les marques et les couleurs disponibles à l'arrière.
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 ) }
|
Cela ressemble beaucoup. Mais il y a des différences:
- Les valeurs par défaut peuvent être écrites au même endroit où le type est déclaré. Cela facilite le maintien de l'intégrité du code.
- lateinit vous permet de ne pas définir de valeur par défaut pour ce qui sera chargé plus tard. Lors de la compilation, une telle variable est considérée comme NotNull, mais chaque fois qu'elle est vérifiée, elle est remplie et une erreur lisible par l'homme est générée. Cela sera particulièrement vrai avec un objet plus complexe qu'un tableau. Je sais que la même chose pourrait être obtenue avec le débit, mais c'est tellement lourd que je n'ai pas essayé.
- kotlin-react out of the box donne la fonction setState, mais il ne se combine pas avec les coroutines car il n'est pas en ligne. J'ai dû copier et mettre en ligne.
- En fait, des coroutines . Il s'agit d'un remplacement pour async / wait et bien plus encore. Par exemple, le rendement se fait à travers eux. Fait intéressant, seul le mot suspend est ajouté à la syntaxe, tout le reste n'est que du code. Par conséquent, plus de liberté d'utilisation. Et un contrôle un peu plus serré au niveau de la compilation. Ainsi, vous ne pouvez pas remplacer componentDidMount avec un modificateur de
suspend
, ce qui est logique: componentDidMount est une méthode synchrone. Mais vous pouvez insérer le bloc asynchrone de launch { }
n'importe où dans le code. Il est possible d'accepter explicitement une fonction asynchrone dans un paramètre ou un champ d'une classe (juste en dessous est un exemple de mon projet). - En Javascript, moins de contrôle est nul. Ainsi, dans l'état résultant, vous pouvez modifier la nullité de la marque, la couleur et les champs chargés et tout sera collecté. Dans la version Kotlin, il y aura des erreurs de compilation justifiées.
Randonnée parallèle avec corutine suspend fun parallel(vararg tasks: suspend () -> Unit) { tasks.map { async { it.invoke() }
Maintenant, chargez les machines à l'arrière en utilisant les filtres de requête
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)
Je veux prêter attention à
Car::class.serializer().list
. Le fait est que jetBrains a écrit une bibliothèque de sérialisation / désérialisation qui fonctionne de la même manière sur JVM et JS. Tout d'abord, moins de problèmes et de code au cas où le backend se trouverait sur la JVM. Deuxièmement, la validité du json entrant est vérifiée pendant la désérialisation, et non pas pendant l'appel, donc lors du changement de version du back-end, et lors de l'intégration en principe, les problèmes seront plus rapides.
Nous dessinons un bouchon avec des filtres
Nous écrivons un composant sans état pour afficher deux listes déroulantes. Dans le cas de Kotlin, ce sera juste une fonction, dans le cas de js, c'est un composant séparé qui sera généré par React Loader lors de l'assemblage.
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
|
La première chose qui attire votre attention - HomeHeaderProps dans la partie JS, nous sommes obligés de déclarer les paramètres entrants séparément. Inopportunément.
La syntaxe de Dropdown a un peu changé. J'utilise
primereact ici , bien sûr, j'ai dû écrire un wrapper kotlin. D'une part, c'est un travail superflu (Dieu merci, il y a
ts2kt ), mais d'autre part, c'est l'occasion de rendre l'api plus pratique par endroits.
Eh bien, un peu de sucre syntaxique dans la formation des éléments pour la liste déroulante.
})))}
dans la version js, cela semble intéressant, mais cela n'a pas d'importance. Mais redresser la séquence de mots est beaucoup plus agréable: «nous transformerons les couleurs en éléments et ajouterons« tous »par défaut», au lieu de «ajouter« tous »aux couleurs converties en éléments». Cela semble être un petit bonus, mais lorsque vous avez plusieurs de ces coups d'État d'affilée ...
Nous enregistrons les filtres dans la requête
Maintenant, lorsque vous choisissez des filtres par marque et couleur, vous devez changer l'état, charger les voitures à l'arrière et changer l'URL.
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() } }
|
Et là encore, le problème avec les valeurs par défaut des paramètres. Pour une raison quelconque, le flux ne m'a pas permis d'avoir une typification, un destructeur et une valeur par défaut tirée de l'état en même temps. Peut-être juste un bug. Mais si c'était le cas, il faudrait déclarer le type en dehors de la classe, c'est-à-dire généralement un écran plus haut ou plus bas.
Dessinez une table
La dernière chose que nous devons faire est d'écrire un composant sans état pour rendre la table avec les machines.
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}" } } }
|
Ici, vous pouvez voir comment j'ai redressé les primefaces api et comment styler en kotlin-react. C'est du json normal, comme dans la version js. Dans mon projet, j'ai fait un wrapper qui a la même apparence, mais avec une frappe forte, autant que possible avec des styles html.
Conclusion
S'impliquer dans les nouvelles technologies est risqué. Il y a peu de guides, il n'y a rien sur le débordement de pile, certaines choses de base manquent. Mais dans le cas de Kotlin, mes frais ont payé.
Pendant que je préparais cet article, j'ai appris un tas de nouvelles choses sur le Javascript moderne: flux, babel, async / wait, modèles jsx. Je me demande à quelle vitesse cette connaissance devient obsolète? Et tout cela n'est pas nécessaire si vous utilisez Kotlin. Dans le même temps, vous devez en savoir un peu plus sur React, car la plupart des problèmes sont facilement résolus à l'aide de la langue.
Et que pensez-vous de remplacer tout ce zoo par une langue avec un grand ensemble de petits pains en plus?
Pour ceux intéressés, les
codes sources .
PS: prévoit d'écrire des articles sur les configs, l'intégration avec JVM et sur dsl formant à la fois react-dom et html normal.
Articles déjà écrits sur Kotlin:
L'arrière-goût de Kotlin, partie 1L'arrière-goût de Kotlin, partie 2Arrière-goût de Kotlin, partie 3. Coroutines - partager le temps processeur