A idéia de transferir a frente para qualquer estrutura js apareceu simultaneamente com a capacidade de escrever React no Kotlin. E eu decidi tentar. O principal problema: poucos materiais e exemplos (tentarei corrigir essa situação). Mas eu tenho digitação completa, refatoração destemida, todos os recursos do Kotlin e, mais importante, o código geral para o back-end na JVM e a frente no Javascript.
Neste artigo, escreveremos uma página no Javasript + React em paralelo com o seu homólogo no Kotlin + React. Para tornar a comparação honesta, adicionei a digitação no Javasript.

Adicionar digitação ao Javascript não foi tão fácil. Se para o Kotlin eu precisava do gradle, npm e webpack, para o Javascript eu precisava do npm, webpack, flow e babel com predefinições react, flow, es2015 e stage-2. Ao mesmo tempo, o fluxo aqui está de alguma forma paralelo, e você precisa executá-lo separadamente e ser amigo do IDE separadamente. Se você agrupar o conjunto e algo semelhante, para a escrita direta do código, por um lado, o Kotlin + React permanece e, por outro, Javascript + React + babel + Flow + ES5 | ES6 | ES7.
Para o nosso exemplo, criaremos uma página com uma lista de carros e a capacidade de filtrar por marca e cor. Nós filtramos a marca e a cor que arrastamos pelas costas uma vez durante a primeira inicialização. Os filtros selecionados são salvos na consulta. Exibimos carros em um prato. Meu projeto não é sobre carros, mas a estrutura geral é geralmente semelhante à que trabalho regularmente.
O resultado é o seguinte (não serei designer):

Não vou descrever a configuração de toda essa máquina shaitan aqui, este é um tópico para um artigo separado (por enquanto, você pode fumar as fontes deste).
Carregando dados pela parte traseira
Primeiro, você precisa carregar as marcas e as cores disponíveis na parte de trás.
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 ) }
|
Parece muito semelhante. Mas existem diferenças:
- Os valores padrão podem ser gravados no mesmo local em que o tipo é declarado. Isso facilita a manutenção da integridade do código.
- O lateinit permite que você não defina um valor padrão para o que será carregado posteriormente. Durante a compilação, essa variável é considerada NotNull, mas cada vez que é verificada, é preenchida e um erro legível por humanos é gerado. Isso será especialmente verdade com um objeto mais complexo que uma matriz. Sei que o mesmo poderia ser alcançado com o fluxo, mas é tão complicado que não tentei.
- O kotlin-react pronto para uso fornece a função setState, mas não combina com corotinas porque não está em linha. Eu tive que copiar e colocar em linha.
- Na verdade, corotinas . Este é um substituto para assíncrono / espera e muito mais. Por exemplo, o rendimento é obtido através deles. Curiosamente, apenas a palavra suspender é adicionada à sintaxe, tudo o resto é apenas código. Portanto, mais liberdade de uso. E um controle um pouco mais rígido no nível de compilação. Portanto, você não pode substituir componentDidMount por um modificador de
suspend
, o que é lógico: componentDidMount é um método síncrono. Mas você pode inserir o bloco assíncrono de launch { }
em qualquer lugar do código. É possível aceitar explicitamente uma função assíncrona em um parâmetro ou campo de uma classe (logo abaixo é um exemplo do meu projeto). - Em Javascript, menos controle é anulável. Portanto, no estado resultante, você pode alterar a nulidade da marca, cor e campos carregados e tudo será coletado. Na versão Kotlin, haverá erros de compilação justificados.
Trekking paralelo com corutin suspend fun parallel(vararg tasks: suspend () -> Unit) { tasks.map { async { it.invoke() }
Agora carregue as máquinas pela parte traseira usando os filtros da 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)
Quero prestar atenção ao
Car::class.serializer().list
. O fato é que o jetBrains criou uma biblioteca para serialização / desserialização que funciona da mesma maneira na JVM e JS. Primeiramente, menos problemas e código, caso o back-end esteja na JVM. Em segundo lugar, a validade do json recebido é verificada durante a desserialização e não em algum momento durante a chamada; portanto, ao alterar a versão do back-end e ao integrar em princípio, os problemas serão mais rápidos.
Nós desenhamos um boné com filtros
Escrevemos um componente sem estado para exibir duas listas suspensas. No caso do Kotlin, será apenas uma função; no caso de js, é um componente separado que será gerado pelo carregador de reação durante a montagem.
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
|
A primeira coisa que chama sua atenção - HomeHeaderProps na parte JS, somos forçados a declarar os parâmetros recebidos separadamente. Inconvenientemente.
A sintaxe do menu suspenso mudou um pouco. Eu uso
primereact aqui , é claro, tive que escrever um invólucro kotlin. Por um lado, esse é um trabalho supérfluo (graças a Deus há
ts2kt ), mas, por outro, é uma oportunidade de tornar a API mais conveniente em alguns lugares.
Bem, um pouco de açúcar sintático na formação de itens para dropdown.
})))}
na versão js parece interessante, mas não importa. Mas endireitar a sequência de palavras é muito mais agradável: “transformaremos as cores em itens e adicionaremos todos por padrão", em vez de "adicionarmos todos às cores convertidas em itens". Parece um pequeno bônus, mas quando você tem vários golpes seguidos ...
Salvamos filtros na consulta
Agora, ao escolher filtros por marca e cor, você precisa alterar o estado, carregar os carros pela parte de trás e alterar o 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() } }
|
E aqui novamente, o problema com os valores padrão dos parâmetros. Por alguma razão, o fluxo não me permitiu ter uma tipificação, um destruidor e um valor padrão retirado do estado ao mesmo tempo. Talvez apenas um bug. Mas, se o fizesse, seria necessário declarar o tipo fora da classe, ou seja, geralmente uma tela mais alta ou mais baixa.
Desenhe uma mesa
A última coisa que precisamos fazer é escrever um componente sem estado para renderizar a tabela com as 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}" } } }
|
Aqui você pode ver como eu endireitei os primefaces da API e como estilizar no kotlin-react. Este é o json comum, como na versão js. No meu projeto, criei um invólucro com a mesma aparência, mas com digitação forte, o máximo possível com os estilos html.
Conclusão
Se envolver em novas tecnologias é arriscado. Existem poucos guias, não há nada no estouro de pilha, algumas coisas básicas estão faltando. Mas no caso de Kotlin, meus custos foram compensados.
Enquanto preparava este artigo, aprendi várias coisas novas sobre o Javascript moderno: modelos de fluxo, babel, async / waitit, jsx. Gostaria de saber com que rapidez esse conhecimento se torna obsoleto. E tudo isso não é necessário se você usar o Kotlin. Ao mesmo tempo, você precisa saber um pouco sobre o React, porque a maioria dos problemas é facilmente resolvida com a ajuda do idioma.
E o que você acha de substituir todo esse zoológico por um idioma, além de um grande conjunto de pães?
Para os interessados,
códigos-fonte .
PS: planeja escrever artigos sobre configurações, integração com a JVM e sobre o dsl, formando o react-dom e o html regular.
Artigos já escritos sobre Kotlin:
O final de Kotlin, parte 1O final de Kotlin, parte 2Retrogosto do Kotlin, parte 3. Coroutines - compartilhe o tempo do processador