当 Kotlin 爱上 React, 会发生什么反应
说起 Kotlin,听说过的大部人第一反应是一门开发 Android 的语言。不得不说 Google 对 Kotlin 的宣传远远的大于了 Kotlin 的创始公司 Jetbrains 。
Kotlin 不仅仅是能写Android,而且可以写服务端,可以说只要可以写Java的地方,就可以用Kotlin来进行替换。当然Kotlin远不止这些。目前Kotlin可以做到以下平台的开发和使用。jvm,android,js和native(beta)。正因为可以开发 Native ,所以Kotlin现在对 ios 开发也是支持的,虽然看起来效果不是那么的好。
今天用 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 等等。
按照脚手架文档,一行代码便能生成我们的项目。
npx create-react-kotlin-app my-app
在项目生成后,打开项目,使用 npm start
或者 yarn start
启动项目。
项目会自动打开浏览器,然后看到我们的项目,说明我们的项目便启动了。
我们可以看看官方提供的 wrappers 库里有什么,好像什么都有就是缺少一个成熟的 UI 组件库。难道说所有的样式都要我们自己写?
当然不是。
kotlinjs 是可以调用到 nodejs 模块的,只不过啊,有点烦。
安装 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 写的,而且编译速度慢,报错有时不明显,可查阅资料少。不过要说硬着头皮开发,个人感觉是完全可以的,不过估计要自己造不少轮子。
个人微信公众号
本文来自博客园,作者:youngxhui,转载请注明原文链接:https://www.cnblogs.com/youngxhui/p/17730471.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通