当 Kotlin 爱上 React, 会发生什么反应

提及 Kotlin,据说过的大部人第一反应是一门开发 Android 的语言。不得不说 Google 对 Kotlin 的宣传远远的大于了 Kotlin 的创始公司 Jetbrains 。css

Kotlin 不只仅是能写Android,并且能够写服务端,能够说只要能够写Java的地方,就能够用Kotlin来进行替换。固然Kotlin远不止这些。目前Kotlin能够作到如下平台的开发和使用。jvm,android,js和native(beta)。正由于能够开发 Native ,因此Kotlin如今对 ios 开发也是支持的,虽然看起来效果不是那么的好。html

今天用 Kotlin 进行 react 开发,这只是对 KotlinJs 的一次尝试和使用。前端

环境

  • 浏览器:Microsoft Edge 77.0.235.5 (官方内部版本) dev(64 位)
  • Kotlin: 1.3.12

安装环境

很早之前,kotlin 官网就出现了两个库,这两个库基本就是来完善 react 在 kotlin 中的生态,第一个是 JetBrains/create-react-kotlin-app ,一个生成 React 工程的脚手架;第二个是JetBrains/kotlin-wrappers ,该库里存放了 react 周边生态组件,好比说 router,redux 等等。node

按照脚手架文档,一行代码便能生成咱们的项目。react

npx create-react-kotlin-app my-app
复制代码

在项目生成后,打开项目,使用 npm start 或者 yarn start 启动项目。android

项目会自动打开浏览器,而后看到咱们的项目,说明咱们的项目便启动了。ios

咱们能够看看官方提供的 wrappers 库里有什么,好像什么都有就是缺乏一个成熟的 UI 组件库。难道说全部的样式都要咱们本身写?git

固然不是。github

kotlinjs 是能够调用到 nodejs 模块的,只不过啊,有点烦。shell

安装 ant

ant 是 阿里前端 开发的一套 UI 组件,其中有着 React 版本。首先要安装该库。

经过 yarn add antd 进行安装,等待安装完成。

安装结束后,咱们将 ant的演示引入到咱们的项目中,打开 index.css 引入项目。

@import '~antd/dist/antd.css';
复制代码

咱们尝试的引入一个 Button 看看。

而后咱们看到页面上,仍是原来的button ,和 ant 的风格没有半毛钱关系。说明咱们仍是没有讲ant的组件使用到咱们的项目中去。

绑定 UI 控件

新创建一个包 ui/ant 来存放咱们与 ant 组件的绑定类。

如何编写咱们的绑定类?这是我遇到的第一个难题。

首先要找到咱们的 node_modules中的antd目录,找到咱们要绑定的控件目录。

咱们用 @file:JsModule("antd/lib/button") 来代表绑定目录。经过 @JsName("default") 来进行绑定,此时须要该控件是以 export default Button;

@file:JsModule("antd/lib/button")

package ui.ant

import react.RClass
import react.RProps

@JsName("default")
external val button: RClass<RProps>
复制代码

在咱们的 Main.kt 中从新引入咱们刚刚写的 button。此时的 button 不一样于 react.dom.button ,咱们所声明的 button 没法直接设置任何属性,方法等。固然咱们会给他设置的。

package app

import react.RBuilder
import react.RComponent
import react.RProps
import react.RState
import react.dom.div

class App : RComponent<RProps, RState>() {
    override fun RBuilder.render() {
        div {
            ui.ant.button {
                +"Button"
            }
        }
    }
}

fun RBuilder.app() = child(App::class) {}
复制代码

等待自动编译和刷新完成,查看咱们的页面。

终于见到了咱们的按钮了,并且样式和 Ant 官方文档上所示一致。

说明咱们的绑定如今已经成功。

而后如法炮制,把其余的要使用控件进行一样的绑定。

咱们 input 和 search 进行绑定。

这次绑定与上次有所改变,咱们对这两个属性能够设置一些值,以便咱们在 HTML DSL 中直接使用。这里咱们经过实现 RProps 来从新定制了咱们要使用的属性,对 Input 设置了一个 placeholder,而 SeacherRPorps 能够直接继承 InputRProps 来进行属性上的扩展。

@file:JsModule("antd/lib/input/Input")

package ui.antd

import react.*

external interface InputRProps:RProps {
    var placeholder:String
}

@JsName("default")
external val input: RClass<InputProps>
复制代码

对 search 控件进行绑定

@file:JsModule("antd/lib/input/Search")
 
package ui.antd

import org.w3c.dom.events.Event

import react.*

external interface SearchProps : InputRProps {
    var onChange: (Event) -> Unit
    var value: String
}

@JsName("default")
external val search: RClass<SearchProps>
复制代码

新建独立控件

咱们从新独立出来一个组件,叫作 Home.kt 。将咱们的 Button 存放进去,而后在 App.kt 上 使用 Home.kt。

package home

import react.RBuilder
import react.RComponent
import react.RProps
import react.RState
import react.dom.div
import ui.ant.button

class Home : RComponent<RProps, RState>() {
    override fun RBuilder.render() {
        div{
           button{
               +"Button"
           }
        }
    }
}

fun RBuilder.home() = child(Home::class) {}
复制代码

等待页面刷新完毕,仍旧是原来的样子。

添加点击事件

由于咱们的按钮中没有声明点击事件,因此没法直接调用,可是 kotlinjs 能够动态属性调用,经过 asDynamic 即可以调用咱们想要使用的任何 html 中存在的属性。

button {
	+"Button"
	attrs {
		asDynamic().onClick = {
			console.log("on click")
		}
	}
}
复制代码

此时就完成了按钮的点击事件,当咱们在页面中点击时,能够看到浏览器控制台上的信息进行打印。

进行双向绑定

对咱们要使用的数据存放在 RState 中,咱们重写来写 RState。 state 和 props 是 react 中两个重要的属性,state 是用来进行内部数据的修改更新,而 props 是能够将外部数据进行传入。

自定义 HomeState 来继承 RState,来进行对内部数据的修改。

interface HomeState : RState {
    var inputValue: Int
}
复制代码

将咱们 Home 中的 RState 类型改成 HomeState

class Home : RComponent<RProps, HomeState>()
复制代码

这样即可以在下方使用 state 中的数据。

用一个 div 来写咱们的数据。

div {
	+"${state.inputValue}"
}
复制代码

对 button 中的点击事件进行微小的修改,在 react 中对数据进行修改都须要调用 setState 。

button {
	+"Button"
	attrs.asDynamic().onClick ={
		setState {
			inputValue += 1
		}
	}
}
复制代码

然而,页面刷新后倒是一个报错。TypeError: Cannot read property 'toString' of undefined 没法读取 undefinded 的 toString 方法。

原来,单在 HomeState 中声明是不够的,还须要在 HomeState.init() 的重写方法中进行初始值的赋值。

从新等待页面刷新,出现了咱们预想的结果。每次点击按钮就会数字就会进行累加。

配合 Axios

axios 是前端中经常使用的一个 http 请求框架,kotlin 官方已经对它进行了一次简单的封装。咱们这里直接就可使用。

首先仍旧是先安装 axios

yarn add axios
复制代码

安装完成后新建一个文件夹axios,再新建Axios.kt。

package axios

import kotlin.js.Promise

@JsModule("axios")
external fun <T> axios(config: AxiosConfigSettings): Promise<AxiosResponse<T>>

// Type definition
external interface AxiosConfigSettings {
    var url: String
    var method: String
    var baseUrl: String
    var timeout: Number
    var data: dynamic
    var transferRequest: dynamic
    var transferResponse: dynamic
    var headers: dynamic
    var params: dynamic
    var withCredentials: Boolean
    var adapter: dynamic
    var auth: dynamic
    var responseType: String
    var xsrfCookieName: String
    var xsrfHeaderName: String
    var onUploadProgress: dynamic
    var onDownloadProgress: dynamic
    var maxContentLength: Number
    var validateStatus: (Number) -> Boolean
    var maxRedirects: Number
    var httpAgent: dynamic
    var httpsAgent: dynamic
    var proxy: dynamic
    var cancelToken: dynamic
}

external interface AxiosResponse<T> {
    val data: T
    val status: Number
    val statusText: String
    val headers: dynamic
    val config: AxiosConfigSettings
}
复制代码

咱们作一个简单的应用,经过输入名字,到 Github 上寻找相关的仓库。

首先定义咱们的数据类,来存放咱们请求获取到的数据。

data class Result(
        val total_count: Int,
        val items: Array<Item>
)

data class Item(val id: Long,
                val node_id: String,
                val name: String,
                val full_name: String,
                val html_url: String)
复制代码

而后在 HomeState 中声明所使用的类。

interface HomeState : RState {
    var inputValue: String
    var repos: Array<Item>
}
复制代码

下面是界面定义的代码

div(classes = "search-input") {
    search {
        attrs {
            onChange = {
                val element = it.target as HTMLInputElement
                setState {
                    inputValue = element.value
                }
            }
            placeholder = "请输入 Github 仓库名称"
        }
    }
}

button {
    +"搜索"
    attrs {
        asDynamic().onClick = {

        }
    }
}

div(classes = "list") {
    if (state.repos.isNotEmpty()) {
        ul {
            for ((index, item) in state.repos.withIndex()) {
                li {
                    a(href = item.html_url) {
                        +"${index + 1} / ${item.full_name}"
                    }
                }
            }
        }
    }
}
复制代码

不过当咱们此时预览界面的时候,发现又又又错了,提示 repos 没法进行迭代。

明明是数组为何没法迭代呢?原来是在 Home 组件建立的时候,尚未对 repos 进行初始化。这里须要了解 react 的生命周期,在组件创建前,对数据进行初始化。

override fun componentWillMount() {
        setState {
            repos = emptyArray()
        }
    }
复制代码

此时页面能够正常展现了。

完善 button 的点击事件。

button {
    +"搜索"
    attrs {
        asDynamic().onClick = {
            //https://api.github.com/search/repositories?q=
            val config: AxiosConfigSettings = jsObject {
                url = "https://api.github.com/search/repositories?q=${state.inputValue}"
            }
            axios.axios<Result>(config).then { response ->
                setState {
                    repos = response.data.items
                }
            }.catch { error ->
                console.log("error", error)
            }
        }
    }
}
复制代码

此时便完成了项目。

总结

首先声明 做者并不是专业前端,因此文章中有错误还望指正;本文更重要的目的是为了代表 kotlin 在其余领域的使用和使用体验。

kotlin 写 react,在书写习惯上应该和 ts 写是差很少的,并且给后端同窗带来了写前端的体验。可是生态不完整,就一个 UI 组件库没有一个官方用 kt 写的,并且编译速度慢,报错有时不明显,可查阅资料少。不过要说硬着头皮开发,我的感受是彻底能够的,不过估计要本身造很多轮子。

我的微信公众号

相关文章
相关标签/搜索