Kotlin + React与Javasript + React

将前端转移到任何js框架的想法与在Kotlin中编写React的能力同时出现。 我决定尝试。 主要问题:材料和示例很少(我将尝试纠正这种情况)。 但是我有完整的打字,无畏的重构,Kotlin的所有功能,最重要的是,JVM后端和Javascript前端的通用代码。

在本文中,我们将在Javasript + React上与其在Kotlin + React上的对应页面并行编写一个页面。 为了使比较诚实,我在Javasript中添加了键入。



在Javascript中添加打字并非易事。 如果对于Kotlin,我需要gradle,npm和webpack,对于Javascript,我需要npm,webpack,flow和babel以及react,flow,es2015和stage-2预设。 同时,这里的流程是不固定的,您需要单独运行它,并与IDE成为朋友。 如果将汇编等内容括起来,则一方面直接写代码,一方面保留Kotlin + React,另一方面保留Javascript + React + babel + Flow + ES5 | ES6 | ES7。

对于我们的示例,我们将创建一个页面,其中包含汽车列表以及按品牌和颜色进行过滤的功能。 第一次启动时,我们会过滤掉从背面拖动的品牌和颜色。 所选过滤器将保存在查询中。 我们在一盘车上展示汽车。 我的项目与汽车无关,但总体结构通常与我经常使用的汽车相似。

结果看起来像这样(我不会成为设计师):



我不会在这里描述整个shaitan机器的配置,这是另一篇文章的主题(目前,您可以从此文章中获取信息)。

从背面加载数据


首先,您需要从背面加载品牌和可用颜色。
javascript
科特林
class Home extends React.Component <ContextRouter, State>{ state = { loaded: false, //(1) color: queryAsMap( this.props.location.search )["color"], brand: queryAsMap( this.props.location.search )["brand"], brands: [], //(2) colors: [] //(2) }; async componentDidMount() { this.setState({ //(3) brands: await ( //(4) await fetch('/api/brands') ).json(), colors: await ( //(4) await fetch('/api/colors') ).json() }); } } type State = { color?: string, //(5) brand?: string, //(5) loaded: boolean, //(1) brands: Array<string>, //(2) colors: Array<string> //(2) }; export default Home; 

 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 ) } 


看起来非常相似。 但是有区别:

  1. 可以在声明该类型的同一位置写入默认值。 这使得维护代码完整性更加容易。
  2. lateinit允许您完全不为以后加载的内容设置默认值。 在编译期间,此类变量被视为NotNull,但每次对其进行检查时,都会将其填充并生成人类可读的错误。 对于比数组更复杂的对象,情况尤其如此。 我知道使用flow可以达到相同的目的,但是它很麻烦,我没有尝试。
  3. 开箱即用的Kotlin-react提供了setState函数,但由于它不是内联的,因此不能与协程结合使用。 我不得不复制并插入行内。
  4. 实际上, 协程 。 这是异步/等待等等的替代品。 例如,通过它们产生产量 。 有趣的是,仅将单词suspend添加到语法中,其他所有内容都只是代码。 因此,使用更加自由。 并在编译级别进行了更严格的控制。 因此,您不能使用suspend修饰符来覆盖componentDidMount,这是合乎逻辑的:componentDidMount是一个同步方法。 但是您可以在代码中的任何位置插入launch { }异步块。 可以在类的参数或字段中显式接受异步函数(下面是我项目的示例)。
  5. 在Javascript中,较少的控制是可以为空的。 因此,在结果状态下,您可以更改品牌,颜色和加载字段的可为空性,所有内容都会被收集。 在Kotlin版本中,将存在合理的编译错误。

与corutin平行徒步旅行
 suspend fun parallel(vararg tasks: suspend () -> Unit) { tasks.map { async { it.invoke() } //  task,    . async {}  -  promise }.forEach { it.await() } // ,   } override fun componentDidMount() { launch { updateState { parallel({ halls = hallExchanger.all() }, { instructors = instructorExchanger.active() }, { groups = fetchGroups() }) } } } 


现在,使用查询过滤器从背面加载计算机
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 }); } 

科特林:

  private suspend fun loadCars() { val url = "/api/cars?brand=${state.brand.orEmpty()}&color=${state.color.orEmpty()}" updateState { cars = fetchJson(url, Car::class.serializer().list) //(*) loaded = true } } 

我要注意Car::class.serializer().list 。 事实是jetBrains已经编写了一个用于序列化/反序列化的库,该库在JVM和JS上都可以使用。 首先,万一后端在JVM上,问题和代码更少。 其次,传入的json的有效性是在反序列化过程中检查的,而不是在调用过程中的某个时间检查的,因此,在更改后端版本以及原则上进行集成时,问题会更快。

我们用过滤器画一个帽子


我们编写了一个无状态组件来显示两个下拉列表。 对于Kotlin,这只是一个函数,对于js,它是一个独立的组件,将在组装过程中由react loader生成。

javascript
科特林
 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 


引起您注意的第一件事-JS部分中的HomeHeaderProps,我们被迫分别声明传入的参数。 不方便。

Dropdown的语法有所变化。 我在这里使用primereact ,当然,我必须写一个kotlin包装器。 一方面,这是多余的工作(感谢上帝,那里有ts2kt ),但另一方面,这是使api在某些地方更方便的机会。

好吧,在下拉菜单项中形成了一些语法糖。 })))}在js版本中看起来很有趣,但是没关系。 但是理顺单词的顺序要好得多:“我们将颜色转换为项目,并默认将所有添加为all,而不是将all添加到转换为项目的颜色中”。 这似乎是一笔不小的奖金,但是当您连续发生几次此类政变时...

我们在查询中保存过滤器


现在,按品牌和颜色选择过滤器时,您需要更改状态,从后部装载汽车并更改url。

javascript
科特林
 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() } } 


同样,这里是参数默认值的问题。 由于某种原因,流程不允许我同时具有类型,析构函数和从状态获取的默认值。 也许只是一个错误。 但是,如果这样做,则有必要在类外声明类型,即 通常屏幕较高或较低。

画一张桌子


我们需要做的最后一件事是编写一个无状态组件以使用计算机呈现表。

javascript
科特林
 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}" } } } 


在这里,您可以看到如何矫正api primefaces以及如何用kotlin反应进行样式设置。 与js版本一样,这是常规json。 在我的项目中,我制作了一个外观相同的包装器,但具有强类型化,并尽可能使用html样式。

结论


涉足新技术是有风险的。 指南很少,堆栈溢出没有内容,缺少了一些基本的知识。 但是对于Kotlin,我的费用得到了回报。

在准备本文时,我了解了有关现代Javascript的许多新知识:流,babel,异步/等待,jsx模板。 我想知道这些知识过时多久了? 如果您使用Kotlin,则所有这些都是不必要的。 同时,您需要对React有所了解,因为大多数问题都可以借助该语言轻松解决。

您如何考虑用一种语言另外用一大包面包代替所有动物园?

对于那些感兴趣的人, 源代码

PS:计划撰写有关配置,与JVM集成以及有关形成react-dom和常规html的dsl的文章。

关于Kotlin的书面文章:

Kotlin的回味,第1部分
Kotlin的回味,第二部分
Kotlin的回味,第3部分。协同程序-共享处理器时间

Source: https://habr.com/ru/post/zh-CN418553/


All Articles