La idea de transferir el frente a cualquier framework js apareció simultáneamente con la capacidad de escribir React en Kotlin. Y decidí intentarlo. El problema principal: pocos materiales y ejemplos (intentaré corregir esta situación). Pero tengo una escritura completa, una refactorización intrépida, todas las características de Kotlin y, lo más importante, el código general para el backend en JVM y el frontal en Javascript.
En este artículo, escribiremos una página sobre Javasript + React en paralelo con su contraparte en Kotlin + React. Para hacer la comparación honesta, agregué escribir a Javasript.

Agregar escribir a Javascript no fue tan fácil. Si para Kotlin necesitaba gradle, npm y webpack, para Javascript necesitaba npm, webpack, flow y babel con los presets react, flow, es2015 y stage-2. Al mismo tiempo, el flujo aquí está de alguna manera al margen, y debe ejecutarlo por separado y ser amigo del IDE por separado. Si pone entre paréntesis el ensamblaje y similares, para la escritura directa de código, por un lado, Kotlin + React permanece, y por otro Javascript + React + babel + Flow + ES5 | ES6 | ES7.
Para nuestro ejemplo, crearemos una página con una lista de automóviles y la capacidad de filtrar por marca y color. Filtramos la marca y el color que arrastramos desde atrás una vez durante el primer arranque. Los filtros seleccionados se guardan en la consulta. Exhibimos autos en un plato. Mi proyecto no se trata de automóviles, pero la estructura general es generalmente similar a la que trabajo regularmente.
El resultado se ve así (no seré diseñador):

No describiré la configuración de toda esta máquina shaitan aquí, este es un tema para un artículo separado (por ahora, puede fumar las fuentes de este).
Cargando datos desde atrás
Primero debe cargar las marcas y los colores disponibles desde la parte posterior.
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 ) }
|
Se ve muy similar. Pero hay diferencias:
- Los valores predeterminados se pueden escribir en el mismo lugar donde se declara el tipo. Esto facilita el mantenimiento de la integridad del código.
- lateinit le permite no establecer un valor predeterminado para lo que se cargará más adelante. Durante la compilación, dicha variable se considera NotNull, pero cada vez que se verifica, se llena y se genera un error legible por humanos. Esto será especialmente cierto con un objeto más complejo que una matriz. Sé que lo mismo podría lograrse con el flujo, pero es tan engorroso que no lo intenté.
- kotlin-react fuera de la caja proporciona la función setState, pero no se combina con las rutinas porque no está en línea. Tuve que copiar y poner en línea.
- En realidad, corutinas . Este es un reemplazo para async / wait y mucho más. Por ejemplo, el rendimiento se hace a través de ellos. Curiosamente, solo la palabra suspender se agrega a la sintaxis, todo lo demás es solo código. Por lo tanto, más libertad de uso. Y un control un poco más estricto a nivel de compilación. Por lo tanto, no puede anular componentDidMount con un modificador de
suspend
, lo cual es lógico: componentDidMount es un método síncrono. Pero puede insertar el bloque asíncrono launch { }
en cualquier parte del código. Es posible aceptar explícitamente una función asincrónica en un parámetro o campo de una clase (justo debajo es un ejemplo de mi proyecto). - En Javascript, menos control es anulable. Por lo tanto, en el estado resultante, puede cambiar la nulabilidad de la marca, el color y los campos cargados y todo se recopilará. En la versión de Kotlin habrá errores de compilación justificados.
Trekking paralelo con corutina suspend fun parallel(vararg tasks: suspend () -> Unit) { tasks.map { async { it.invoke() }
Ahora cargue las máquinas desde atrás utilizando los filtros de consulta
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)
Quiero prestar atención a
Car::class.serializer().list
. El hecho es que jetBrains ha escrito una biblioteca para serialización / deserialización que funciona igual en JVM y JS. En primer lugar, menos problemas y código en caso de que el backend esté en la JVM. En segundo lugar, la validez del json entrante se verifica durante la deserialización, y no en algún momento durante la llamada, por lo que al cambiar la versión del back-end y al integrar en principio, los problemas serán más rápidos.
Dibujamos una gorra con filtros
Escribimos un componente sin estado para mostrar dos listas desplegables. En el caso de Kotlin, será solo una función, en el caso de js, es un componente separado que será generado por el cargador de reacción durante el ensamblaje.
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
|
Lo primero que llama la atención: HomeHeaderProps en la parte JS, nos vemos obligados a declarar los parámetros entrantes por separado. Inconveniente
La sintaxis de Dropdown ha cambiado un poco. Utilizo
primereact aquí , por supuesto, tuve que escribir un envoltorio de kotlin. Por un lado, este es un trabajo superfluo (gracias a Dios que hay
ts2kt ), pero por el otro, es una oportunidad para hacer que la API sea más conveniente en algunos lugares.
Bueno, un poco de azúcar sintáctica en la formación de elementos para desplegable.
})))}
en la versión js parece interesante, pero no importa. Pero enderezar la secuencia de palabras es mucho más agradable: "transformaremos los colores en elementos y agregaremos 'todos' por defecto", en lugar de "agregar 'todos' a los colores convertidos en elementos". Parece una pequeña bonificación, pero cuando tienes varios golpes de este tipo seguidos ...
Guardamos filtros en consulta
Ahora, al elegir filtros por marca y color, debe cambiar el estado, cargar los automóviles desde la parte posterior y cambiar la 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() } }
|
Y aquí nuevamente, el problema con los valores predeterminados de los parámetros. Por alguna razón, flow no me permitió tener una tipificación, un destructor y un valor predeterminado tomado del estado al mismo tiempo. Tal vez solo un error. Pero, si lo hiciera, sería necesario declarar el tipo fuera de la clase, es decir generalmente una pantalla más alta o más baja.
Dibujar una mesa
Lo último que debemos hacer es escribir un componente sin estado para representar la tabla con las máquinas.
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}" } } }
|
Aquí puedes ver cómo enderecé las superficies principales de la API y cómo peinar en kotlin-react. Esto es json regular, como en la versión js. En mi proyecto, hice un contenedor que se ve igual, pero con un tipeo fuerte, tanto como sea posible con estilos html.
Conclusión
Involucrarse en nuevas tecnologías es arriesgado. Hay pocas guías, no hay nada en el desbordamiento de pila, faltan algunas cosas básicas. Pero en el caso de Kotlin, mis costos pagaron.
Mientras preparaba este artículo, aprendí muchas cosas nuevas sobre Javascript moderno: plantillas flow, babel, async / await, jsx. Me pregunto qué tan rápido este conocimiento se vuelve obsoleto. Y todo esto no es necesario si usa Kotlin. Al mismo tiempo, necesita saber un poco sobre React, porque la mayoría de los problemas se resuelven fácilmente con la ayuda del lenguaje.
¿Y qué piensas acerca de reemplazar todo este zoológico con un idioma con un gran conjunto de bollos además?
Para aquellos interesados,
códigos fuente .
PD: Planea escribir artículos sobre configuraciones, integración con JVM y sobre dsl formando react-dom y html regular.
Artículos ya escritos sobre Kotlin:
El regusto de Kotlin, parte 1El regusto de Kotlin, parte 2Postgusto de Kotlin, parte 3. Corutinas: comparta el tiempo del procesador