前言
好久没写文章了,有好多东西想分享,但是又不知道从何写起,正好最近在学习 Kotlin,那就先从 Kotlin-Multiplatform 开始吧。
本文章介绍,如何通过Kotlin-Multiplatform
实现共享逻辑代码,以及使用Kotlin/Js
开发Html5
工程和Kotlin/Jvm
开发Web
工程。
本次文章总共三篇
- 第一篇:《搭建Kotlin-Multiplatform工程》
- 第二篇:《通过React + Spring实现前后台简单列表》
- 第三篇:《通过Webpack + Gradle实现环境分离》
使用工具
- 开发工具:IntelliJ IDEA + Gradle
- 语言:Kotlin
- 框架:Spring + React
代码已上传至 GitHub
这是本文章的第一篇,介绍如何搭建 Kotlin-Multiplatform 前后端工程。
本文章默认你已经懂得
SpringBoot
相关知识、有使用Gradle
搭建过工程、并且懂得Kotlin
语言和部分React
架构使用。相关知识可以从以下渠道获知
- Spring官方文档
- Kotlin官方文档
- React官方文档
正文
一、Kotlin-Multiplatform介绍
Kotlin-Multiplatform
是Kotlin
推出的一种跨平台的开发技术,通过它可以共享你的逻辑代码,以此来实现一套逻辑代码全平台通用的目的。
注意上面的图,从实际上来说这是一种不同于 Flutter 的技术,Flutter 是编写一套全部代码然后最终通过底层渲染引擎绘制,在开发 Flutter 的过程中你基本不需要关心各个平台的技术标准。
但是 Kotlin-Multiplatform 却是 KotlinCommon + KotlinNative 的模式,也就是说除了公共部分可以共享,其他部分都必须按照各个平台标准开发,只不过语言从各个平台的原生语言切换为 Kotlin 语言开发,具体图示看Kotlin-Multiplatform官方文档。
这样看起来比起 Flutter 是弱爆了,而且从真正节省开发成本的角度来说它未必可以节省多少,但是从实际上来说二者其实是在不同的维度,Flutter 是一个大前端框架,它的野心是统一整个前端开发,而 Kotlin-Multiplatform 其实是想从语言的角度统一平台编程开发。
也就是说Flutter
是框架而Kotlin-Multiplatform
是语言,本次的文章也是通过使用Kotlin
语言同时开发前后台。
其实我觉得业界最可惜的就是为什么 Kotlin 不能开发 Flutter ←_←
- 你可以通过《Kotlin Multiplatform - 下一代全平台开发技术》这篇文章了解更多 Kotlin-Multiplatform 信息。
- KtAcademyPortal 这是一个使用Kotlin开发的真正全平台项目。
二、工程搭建
0.序
首先必须点名下 Kotlin-Multiplatform 的缺点:
- 1.虽然它可以使用一套语言来开发,但是你必须遵循各个平台的技术标准,而且各个平台下 Kotlin 实现都有所不同(比如
Kotlin/Js
下反射就用不了),有些问题解决起来就非常麻烦。 - 2.相关的文档和三方库都少的可怜(这里指与
Kotlin-Multiplatform
相关),虽然 Kotlin 官方提供了使用原生包的方式,但是你必须手动做一层转换,这也意味着你必须懂得当前平台的原生语言(假如你不是在开发全栈程序)。 - 3.因为最终会转换为本地语言(比如
Kotlin/Js
最终会转换为Js
运行),所以当你遇到需要调试的时候就会显得非常麻烦(Android
因为原生支持Kotlin
开发所以会好很多),而原生的庞大社区你只能用一半(这一半也要求你必须懂得原生语言)。
说了那么多缺点,但其实想想用同一种语言开发前后台也是一件很令人兴奋的事情。
1.目录结构
首先需要先说明下目录结构
- common 公共模块,存放所有公共代码,使用
kotlin-multiplatform
插件
Kotlin-Multiplatform 模块与其他模块最大的不同就是不再使用单一的 main 而是区分各个平台-
commonMain
纯 Kotlin 代码,存放业务模型 commonTest
-
jsMain
Kotlin/Js 公共代码,只会被 Kotlin/Js 模块引用 jsTest
-
jvmMain
Kotlin/Jvm 公共代码,只会被 Kotlin/Jvm 模块引用 jvmTest
- 其他所支持平台
-
- dashboard 前端模块,存放所有前端逻辑代码,使用
org.jetbrains.kotlin.js
插件main
test
- server 后端模块,存放所有后端逻辑代码,使用
java
和kotlin
插件main
test
具体如下图:
2.插件引入
开始前需要引入 Kotlin-Multiplatform 需要的插件,在根目录的 build.gradle 添加如下代码:
这里需要说明的是,我这里使用的是 gradle 旧版本使用插件的方式,如果想要使用新的方式可以查看 org.jetbrains.kotlin.multiplatform
allprojects {
repositories {
mavenCentral()
//Kotlin-Multiplatform的插件和包在这三个仓库里
maven { url 'https://dl.bintray.com/kotlin/kotlinx' }
maven { url 'https://dl.bintray.com/kotlin/kotlin-eap' }
maven { url 'https://kotlin.bintray.com/kotlin-js-wrappers' }
}
}
buildscript {
ext {
//kotlin版本
kotlinVersion = '1.4.10'
}
repositories {
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}"
}
}
subprojects {
apply plugin: 'idea'
version = '1.0.0'
}
3.构建common模块
接下来我们开始构建公共模块,在 common 文件夹下创建 build.gradle 文件:
apply plugin: 'kotlin-multiplatform'
kotlin {
jvm {
withJava()
}
js {
browser {
}
//指示将Kotlin编译为Js代码,默认行为可以为不添加
binaries.executable()
}
sourceSets {
commonMain {
dependencies {
//这里引入纯Kotlin代码库
implementation 'org.jetbrains.kotlin:kotlin-stdlib-common'
}
}
jvmMain {
dependencies {
//这里引入Java或者Kotlin/Jvm代码库
}
}
jsMain {
dependencies {
//这里引入Js或者Kotlin/Js代码库
}
}
}
}
然后我们就可以在 common 模块下添加代码,比方说加个通用异常类:
class ServerException(override val code: Int,
override val errorMessage: String?
) : RuntimeException() {
constructor(error: IServerError) : this(error.code, error.errorMessage)
constructor(error: IServerError, errorMessage: String?) : this(error.code, errorMessage)
}
这样一个通用的模块就创建好了,其他的jsMain
和jvmMain
也是一样的用法,但是在引包的时候需要注意是否为相关平台的库。
最后别忘了在 setting.gradle 里面添加
include ':common'
4.构建dashboard模块
第二步构建 Kotlin/Js 前端模块,这个模块存放我们前端的业务和UI代码(相对的common
模块的jsMain
用于存放逻辑代码)。
4.1 创建模块
一样的先在 dashboard 文件夹下创建 build.gradle 文件:
apply plugin: 'org.jetbrains.kotlin.js'
dependencies {
testImplementation "org.jetbrains.kotlin:kotlin-test-js"
implementation 'org.jetbrains.kotlin:kotlin-stdlib-js'
//引入common模块,commonMain和jsMain的代码会被引入
implementation project(':common')
}
kotlin {
js {
//指示使用jsMain代码
useCommonJs()
browser {
webpackTask {
//当使用webpack时添加对于css的支持
cssSupport.enabled = true
}
runTask {
//当直接运行时添加对于css的支持
cssSupport.enabled = true
}
testTask {
useKarma {
useChromeHeadless()
webpackConfig.cssSupport.enabled = true
}
}
}
binaries.executable()
}
}
4.2 引入支持库
为了能够在 Kotlin/Js 里直接开发网页,我们还需要在 build.gradle 导入几个官方库:
dependencies {
//ktor是JetBrains推出的一个Web框架,这里使用它的Client部分
implementation "io.ktor:ktor-client-js:${ktorVersion}"
implementation "io.ktor:ktor-client-json-js:${ktorVersion}"
implementation "io.ktor:ktor-client-serialization-js:${ktorVersion}"
//官方的Kotlin/React框架库
implementation "org.jetbrains:kotlin-react:${kotlinReactJsVersion}"
implementation "org.jetbrains:kotlin-react-dom:${kotlinReactJsVersion}"
implementation npm('react', reactJsVersion)
implementation npm('react-dom', reactJsVersion)
//官方的Kotlin/Css库
implementation "org.jetbrains:kotlin-styled:${kotlinStyledVersion}"
implementation npm('styled-components', styledComponentsVersion)
implementation npm('inline-style-prefixer', stylePrefixer)
}
npm
是 Kotlin/Js 插件支持的一种导入 Js 库的方式,具体查看官方文档
同时在根目录下的 build.gradle 添加版本号:
buildscript {
ext {
//kotlin
ktorVersion = '1.4.0'
//js
reactJsVersion = '^16.13.1'
kotlinReactJsVersion = '16.13.1-pre.124-kotlin-1.4.10'
reactRouterJsVersion = '5.1.2'
kotlinReactRouterJsVersion = '5.1.2-pre.124-kotlin-1.4.10'
styledComponentsVersion = '~5.2.0'
stylePrefixer = '~6.0.0'
kotlinStyledVersion = '5.2.0-pre.124-kotlin-1.4.10'
}
}
4.3 引入资源文件
这里还有最后一个步骤,在 resources 下创建 index.html 文件,并引入我们的 dashboard.js 文件:
Kotlin/Js Sample Dashboard
这里需要注意
dashboard.js
这个文件的名称默认是与工程名相同的
完成上述操作后我们就可以开发网页了,添加一个 main.kt 文件:
fun main() {
render(document.getElementById("root")) {
//h1本质是kotlin的一个函数,对应html的h1标签
h1 {
+"Hello, React+Kotlin/JS!"
}
}
}
然后通过 Gradle > dashboard Tasks > kotlin browser > browserDevelopmentRun 启动前端程序:
最终启动后访问 http://localhost:8080 结果如下图:
5.构建server模块
第三步构建 Kotlin/Jvm 后端模块,这个模块存放我们后端的业务代码。
我这里使用的是 Spring + Kotlin 作为后端,详细构建可以参考这篇文章《使用SpringBoot和Kotlin构建Web程序》。
1.创建模块
同样先在 server 文件夹下创建 build.gradle 文件:
apply plugin: 'java'
apply plugin: 'kotlin'
apply plugin: 'kotlin-spring'
apply plugin: 'kotlinx-serialization'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
compileKotlin {
kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8
}
compileTestKotlin {
kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8
}
test {
useJUnitPlatform()
}
dependencies {
//测试库
testImplementation "org.jetbrains.kotlin:kotlin-test-junit5:${kotlinVersion}"
testImplementation 'org.springframework.boot:spring-boot-starter-test'
//SpringBoot和WebFlux库
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
//Jackson-Kotlin库,用于解决data class没有空构造函数导致无法解析的问题
implementation "com.fasterxml.jackson.module:jackson-module-kotlin:${jacksonVersion}"
implementation project(':common')
}
2.添加配置文件
在 resources 下添加一个 Spring 配置文件 application.yml:
spring:
application:
name: server
profiles:
active: ${SMART_FOX_ACTIVE:local}
server:
port: 9511
3.创建Application
导入库后创建一个 SampleServerApplication.kt 启动类:
@SpringBootApplication
class SampleServerApplication
fun main(args: Array) {
runApplication(*args)
}
启动图如下:
三、业务开发
在第二节我们成功搭建了一个 Kotlin-Multiplatform 项目,接下来我们尝试使用通用业务模型来实现前后台的通信,以此来感受 Kotlin-Multiplatform 的优势。
1.common部分
common 这是 Kotlin-Multiplatform 最精华的部分,通过它我们可以使用一套代码实现所有平台的逻辑,甚至可以把这部分抽离出来打成公共库。
想象一下,当整个部门不管是前端还是后端都使用同一种语言,那么我们在平时开发过程中遇到的问题,甚至一些非常好的代码都可以共享,它们可以成为整个部门的积累。
而且各个岗位之间的界线都将变得糢糊,让所有人都能更加容易的触类旁通,跨越障碍。
在这里我将通用模型放在 common 模块下,然后在 server 和 dashboard 模块下引用,如下图:
为了实现这一目的我们需要先引入 kotlinx.serialization 工程,它是 Kotlin 官方的解析库,支持JSON
、Protobuf
、CBOR
、Hocon
和Properties
格式。
在 common 下的 build.gradle 引入插件:
apply plugin: 'kotlinx-serialization'
kotlin {
sourceSets {
commonMain {
dependencies {
//忽略其他部分...
implementation "org.jetbrains.kotlinx:kotlinx-serialization-core:${kserializationVersion}"
}
}
}
然后就可以添加通用模型 ServerResult.kt,省略后的写法如下:
//解析注解
@Serializable
data class ServerResult(
val result: T? = null,
val code: Int
) {
companion object {
fun success(result: T): ServerResult {
return ServerResult(result, 200)
}
}
}
2.server部分
上面我们在 common 模块创建了一个通用模型,然后我们就可以在其他模块直接使用。
在 server 模块下创建一个 HomeController.kt 类:
@RestController
@RequestMapping("/home")
class HomeController {
@GetMapping
fun welcome(): Mono> {
return Mono.just(ServerResult.success("Welcome"))
}
}
启动 Spring 后访问接口 http://localhost:9511/home 返回如下字符串:
//这里返回值已做简化
{"result":"Welcome","code":200}
最后为了能让dashboard独立运行后能访问,添加 CorsGlobalConfiguration.kt 解决跨域问题:
@Configuration
class CorsGlobalConfiguration : WebFluxConfigurer {
override fun addCorsMappings(corsRegistry: CorsRegistry) {
corsRegistry.addMapping("/home/**")
.allowedOrigins("*")
.allowedMethods("*")
}
}
3.dashboard部分
现在我们已经完成了后端部分,下面需要开发我们的前端模块。
这里需要说明的是,在传统的类似 Thymeleaf 或者 Jsp 的模板引擎上,是通过服务器渲染完成后在返回,而我们这里开发的实际上是前后端分离的项目,所以它需要做网络请求,这也是前面引用ktor-client
的用处。
3.1 封装网络请求类
为了实现网络请求,我们还需要对ktor-client
做一点小小的封装,添加 Server.kt 文件:
//请求地址 如果是获取本地url则使用 window.location.origin
val endpoint = "http://localhost:9511"
//json解析器
val json = Json {
ignoreUnknownKeys = true
}
//客户端
val client = HttpClient {
install(JsonFeature) {
serializer = KotlinxSerializer(json)
}
}
//发起get请求
suspend fun get(path: String,
serializer: KSerializer,
block: HttpRequestBuilder.() -> Unit = {}): T {
//这里默认解析为字符串
val response = client.get(endpoint + path, block)
return parse(serializer, response)
}
private fun parse(serializer: KSerializer, response: String): T {
//这里这样写是因为Ktor/Js还不支持泛型返回类解析,所以需要传入解析器
val result: ServerResult = json.decodeFromString(ServerResult.serializer(serializer), response)
if (!result.code != 200) {
throw ServerException(result)
}
return result.result ?: throw ServerException(500, "返回值为空")
}
Ktor 的 issues 看这里
第二步添加 HomeRepository.kt 文件,用于接口请求:
//挂起函数,协程调用
suspend inline fun welcome(): String {
return get("/home", String.serializer())
}
3.2 装饰页面
通过welcome()
方法已经可以访问接口并且获取数据,现在我们改造下 Main.kt 文件,我们为页面添加一个标题和一个按钮,然后通过点击按钮请求服务器,最后通过弹窗显示返回的结果:
fun main() {
render(document.getElementById("root")) {
//style...开头的是kotlin-styled包里的控件
styledH1 {
//直接设置css
css {
justifyContent = JustifyContent.center
display = Display.flex
}
+"Hello Kotlin/Js"
}
styledDiv {
css {
justifyContent = JustifyContent.center
display = Display.flex
marginTop = 100.px
}
styledButton {
css {
width = 120.px
height = 60.px
}
//设置属性
attrs {
//注册单击函数
onClickFunction = {
//点击后通过协程发出请求
MainScope().launch {
val result = welcome()
//获取结果后通过弹窗显示返回值
window.alert("请求结果:$result")
}
}
}
+"发起请求"
}
}
}
}
Kotlin-React/DSL 看《官方教程》或者《kotlinx.html》
启动后访问 http://localhost:8080 最终效果如下图:
[图片上传失败...(image-3cd4db-1603951807955)]
点击按钮后如下图:
4.合并部署
通过上面的步骤我们已经成功开发出了前端和后端,但其实还差一个东西,在正常的前后端分离开发中,前端模块通常会放到 Cdn 进行分流加速,或者打包到 Native 运行。
但是如果我是一个在内部或者主功能是提供接口的服务,只需要一个仪表盘用来显示数据,那么放 Cdn 明显是不合适的。
通常对于这样的需求有2种做法,第一种一起打到 Docket 镜像中,在 Docker 启动时一起启动,或者直接将前后端打到一个 Jar 包里一起启动,这也是我这里的做法。
要实现这样的需求我们要利用 Gradle 的功能,在 server 模块下的 build.gradle 下添加如下代码:
//继承processResources任务
processResources {
//获取dashboard模块
def jsProject = project(':dashboard')
//获取dashboard模块中的browserProductionWebpack任务
def task = jsProject.tasks.getByName('browserProductionWebpack')
//设置processResources任务的/static文件夹,来源为browserProductionWebpack任务的目标目录
from(task.destinationDirectory) {
into 'static'
}
//在processResources任务之前执行browserProductionWebpack任务
dependsOn(task)
}
添加脚本前的 build 过程如下图:
在执行processResources
任务前,会先执行 dashboard 模块。
当然这样带来的问题就是构建时间会大量增加,主要是因为 npm
获取包时非常慢,并且 kotlin 需要编译完整编译为 Js,这一点我们将在第二篇解决。
添加脚本后的 build 过程如下图:
build 过后会在 server 模块下的 build/resources 下的生成 static 文件夹,如下图:
重新启动 Spring 访问 http://localhost:9511,点击按钮后显示如下图:
[图片上传失败...(image-c302da-1603951807955)]
结尾
第一篇到这里就结束了,这篇文章介绍了如何搭建基础工程和前后端的交互,下一篇会介绍 React 在 Kotlin/Js 中的使用。
除了像我这样实现一个前后台的开发项目,搭建 Kotlin/Native 工程的过程也是大同小异。
Kotlin-Multiplatform 是一个非常有意思的工程,而 Kotlin 现在也正在通过它将触角探入到各个平台开发中去,随着 Kotlin 版本的更新,Kotlin-Multiplatform 也将愈加完善,也许以后同一个部门,甚至一个公司都用同一种语言的事情也会发生,那会变得多么有趣。
最后,如果它有解决你的问题的话,请下点个赞,谢谢。