docs: 补充全栈 KMP 中的 kotlinx-rpc 实战与方案对比
This commit is contained in:
+257
-3
@@ -20,18 +20,272 @@
|
||||
|
||||
除了 iOS 部分必须由 Xcode 做额外设置后编译以外,其他的应用均可在 JDK 下完成编译(Android 需要相应 SDK,其他需要编译为字节码的需要其他编译器)。
|
||||
|
||||
Server 本身也是运行在 JVM 上面的,它和普通的 Java / Kotlin 后端项目基本没有区别,除非使用特殊的 Ktor 后端编译为字节码。不过,在 KMP 中,将它的部分代码写在 commonMain 中,可以在一定程度上减少代码的出错可能,还可以进行代码复用。
|
||||
Server 本身也是运行在 JVM 上面的,它和普通的 Java / Kotlin 后端项目基本没有区别,除非使用特殊的 Ktor 后端编译为字节码。不过,在 KMP 中,将它的部分代码写在 `core/commonMain` 中,可以在一定程度上减少代码的出错可能,还可以进行代码复用。
|
||||
|
||||
我们在编写全栈 KMP 时,应当优先考虑 Web(WASM) 和 Server 端能否完美运行。
|
||||
|
||||
`core` 的依赖关系在 `shared` 之上;`server` 与整个 `app` 模块平级,在 `core` 之下;`app` 内部模块在 `shared` 之下。
|
||||
在默认状况下,`core` 的依赖关系在 `shared` 之上;`server` 与整个 `app` 模块平级,在 `core` 之下;`app` 内部模块在 `shared` 之下。不建议自行改动。
|
||||
|
||||
## 整体的设计思路
|
||||
|
||||
既然 KMP 最大的特点是多平台代码共享,那么这里就要考虑到把 后端的 entity(vo, dto),网络接口(如果后端框架支持拆开写的话)等一并写在共享代码的部分,即 `core/commonMain` 内部。
|
||||
|
||||
## RPC 网络交互
|
||||
|
||||
在全栈开发中,前端与后端的通信是不可避免的一环。传统的开发流程中,前后端往往通过 RESTful API(HTTP + JSON)进行通信,但全栈 KMP 提供了一种更优雅、类型更安全的通信模式——**RPC(Remote Procedure Call,远程过程调用)**。
|
||||
|
||||
一般来讲,在域名后面,传统网络 api 以 `/api` 开头,websocket 服务以 `/ws` 开头,PRC 服务以 `/rpc` 开头。
|
||||
|
||||
### 为什么在全栈 KMP 中推荐 RPC?
|
||||
|
||||
在传统的双端分离架构(如前端 TS/Vue/React + 后端 Java/Go)中:
|
||||
- 后端编写 Controller 暴露 HTTP 路由与参数。
|
||||
- 前端需要查阅 Swagger / API 文档,手动拼写 URL、路径参数与请求体。
|
||||
- **痛点**:字段名称变更、类型变动无法在编译期间发现,一旦接口不一致只能在运行时报错。
|
||||
|
||||
而在**全栈 KMP**体系下,前端(Android、iOS、Desktop、Web)和后端(Ktor Server)全部使用 Kotlin 编写:
|
||||
1. **如同调用本地函数**:前端通过网络调用后端服务,就像调用本地对象的 `suspend` 函数一样简单自然,免去手动处理 URL 路径、请求方法和 HTTP 状态码的烦恼。
|
||||
2. **端到端类型安全(End-to-End Type Safety)**:RPC 接口定义在共享的 `core/commonMain` 中。一旦接口方法或传输对象(DTO)发生改变,前后端在编译期便会直接检查并报错,彻底杜绝接口联调中的低级错误。
|
||||
3. **重构极度友好**:在 IDE(IDEA / Android Studio / Fleet)中对数据字段或接口函数执行重命名或修改签名,全栈代码会自动联动更新。
|
||||
4. **原生支持双向流式传输(Streaming)**:借助于 Kotlin 协程与 `Flow<T>`,RPC 可以轻松实现实时消息流、双向数据流推送。
|
||||
|
||||
### JetBrains 官方方案:kotlinx-rpc
|
||||
|
||||
JetBrains 推出了官方库 **`kotlinx-rpc`**,它是专为 Kotlin Multiplatform 打造的轻量级、跨平台 RPC 框架,天然与 Ktor 和 Kotlinx Serialization 深度集成。
|
||||
|
||||
SpringBoot 的 RPC 与 Ktor 不完全一致,推荐使用 gRPC,Apache Dubbo 常用于服务器间的通信。
|
||||
|
||||
#### 项目依赖配置
|
||||
|
||||
RPC 的通信契约定义在 `core` 模块,服务端与客户端分别引入对应的运行时插件与依赖:
|
||||
|
||||
**各模块 `build.gradle.kts` 配置示例:**
|
||||
|
||||
```kotlin
|
||||
// 1. 在根工程或者对应模块中引入 kotlinx-rpc 插件
|
||||
plugins {
|
||||
id("org.jetbrains.kotlinx.rpc.plugin") version "0.5.1" // 请参考官方最新版本
|
||||
kotlin("plugin.serialization") version "2.0.0"
|
||||
}
|
||||
|
||||
// 2. core/build.gradle.kts (共享接口与数据模型)
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-rpc-core:0.5.1")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. server/build.gradle.kts (服务端)
|
||||
dependencies {
|
||||
implementation(project(":core"))
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-server:0.5.1")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-serialization-json:0.5.1")
|
||||
implementation("io.ktor:ktor-server-websockets:2.3.12")
|
||||
}
|
||||
|
||||
// 4. shared/build.gradle.kts (客户端共享层)
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
implementation(project(":core"))
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-client:0.5.1")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-serialization-json:0.5.1")
|
||||
implementation("io.ktor:ktor-client-websockets:2.3.12")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 在 `core/commonMain` 中定义接口与 DTO
|
||||
|
||||
在共享层中,我们只需要使用普通接口继承 `RPC`,并用 `@Serializable` 标记传输对象:
|
||||
|
||||
**最好一个文件只写一个 class**
|
||||
|
||||
```kotlin
|
||||
package com.example.core.rpc
|
||||
|
||||
import kotlinx.rpc.RPC
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
// 共享的数据传输对象 (DTO)
|
||||
@Serializable
|
||||
data class UserDto(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val role: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class NoticeMessage(
|
||||
val id: String,
|
||||
val content: String,
|
||||
val timestamp: Long
|
||||
)
|
||||
|
||||
// 共享的 RPC 接口定义
|
||||
interface UserService : RPC {
|
||||
// 普通 RPC 异步方法调用
|
||||
suspend fun getUserProfile(userId: Long): UserDto
|
||||
|
||||
// 接收参数并更新
|
||||
suspend fun updateUserName(userId: Long, newName: String): Boolean
|
||||
|
||||
// 使用 Flow 实现服务器向客户端的实时流式推送
|
||||
fun subscribeNotices(): Flow<NoticeMessage>
|
||||
}
|
||||
```
|
||||
|
||||
#### 在 `server` 模块实现业务并挂载
|
||||
|
||||
在服务端,编写接口的具体业务实现,并将其挂载到 Ktor 路由中(通常通过 WebSocket 通信):
|
||||
|
||||
```kotlin
|
||||
package com.example.server.services
|
||||
|
||||
import com.example.core.rpc.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
class UserServiceImpl : UserService {
|
||||
override suspend fun getUserProfile(userId: Long): UserDto {
|
||||
// 查数据库或执行业务计算
|
||||
return UserDto(id = userId, name = "KMP 开发者", role = "Admin")
|
||||
}
|
||||
|
||||
override suspend fun updateUserName(userId: Long, newName: String): Boolean {
|
||||
println("更新用户 $userId 的姓名为: $newName")
|
||||
return true
|
||||
}
|
||||
|
||||
override fun subscribeNotices(): Flow<NoticeMessage> = flow {
|
||||
var count = 0
|
||||
while (true) {
|
||||
emit(NoticeMessage("id_$count", "系统广播消息 #$count", System.currentTimeMillis()))
|
||||
count++
|
||||
delay(5000) // 每 5 秒推送一条
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在 Ktor 的 `Application.kt` 路由中挂载 RPC 引擎:
|
||||
|
||||
```kotlin
|
||||
package com.example.server
|
||||
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.routing.*
|
||||
import io.ktor.server.websocket.*
|
||||
import kotlinx.rpc.krpc.ktor.server.rpc
|
||||
import kotlinx.rpc.krpc.serialization.json.json
|
||||
import com.example.core.rpc.UserService
|
||||
import com.example.server.services.UserServiceImpl
|
||||
|
||||
fun Application.configureRpc() {
|
||||
install(WebSockets) // 安装 WebSocket 插件
|
||||
|
||||
routing {
|
||||
// 挂载 RPC 端点
|
||||
rpc("/rpc") {
|
||||
rpcConfig {
|
||||
serialization {
|
||||
json()
|
||||
}
|
||||
}
|
||||
// 注册服务实现类
|
||||
registerService<UserService> { UserServiceImpl() }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 在客户端(`shared/commonMain`)调用服务
|
||||
|
||||
在跨平台前端共享代码中,初始化 RPC Client 并获取服务代理:
|
||||
|
||||
```kotlin
|
||||
package com.example.shared.network
|
||||
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.plugins.websocket.*
|
||||
import kotlinx.rpc.krpc.ktor.client.rpc
|
||||
import kotlinx.rpc.krpc.ktor.client.rpcConfig
|
||||
import kotlinx.rpc.krpc.serialization.json.json
|
||||
import kotlinx.rpc.withService
|
||||
import com.example.core.rpc.UserService
|
||||
import com.example.core.rpc.UserDto
|
||||
|
||||
class RpcClientManager(
|
||||
private val host: String = "127.0.0.1",
|
||||
private val port: Int = 8080
|
||||
) {
|
||||
private val httpClient = HttpClient {
|
||||
install(WebSockets)
|
||||
}
|
||||
|
||||
suspend fun createUserService(): UserService {
|
||||
val rpcClient = httpClient.rpc {
|
||||
url {
|
||||
this.host = this@RpcClientManager.host
|
||||
this.port = this@RpcClientManager.port
|
||||
encodedPath = "/rpc"
|
||||
}
|
||||
rpcConfig {
|
||||
serialization {
|
||||
json()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取由 kotlinx-rpc 动态生成的 UserService 代理实现
|
||||
return rpcClient.withService<UserService>()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在 UI 层或业务层直接调用:
|
||||
|
||||
```kotlin
|
||||
// 先开一个协程
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
val userService = rpcClientManager.createUserService()
|
||||
|
||||
// 1. 调用远程挂起函数
|
||||
val user: UserDto = userService.getUserProfile(1001)
|
||||
println("成功获取用户: ${user.name}")
|
||||
|
||||
// 2. 收集服务端的实时 Flow 流
|
||||
userService.subscribeNotices().collect { notice ->
|
||||
println("收到广播: ${notice.content}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("RPC 通信异常: ${e.message}")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 其他 RPC 方案的对比与选型
|
||||
|
||||
根据项目场景的不同,全栈 KMP 还支持以下常见方案:
|
||||
|
||||
| 方案 | 协议与传输层 | 优势 | 适用场景 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **kotlinx-rpc** | WebSocket / HTTP 等 (kRPC) | Kotlin 官方出品,全平台开箱即用,纯 Kotlin 接口定义,开发效率最高 | 纯 Kotlin / KMP 全栈应用、前后端统一开发 |
|
||||
| **gRPC / Wire** | HTTP/2 + Protobuf | 跨语言支持最强(Java/Go/Python/C++),传输体积小、二进制序列化速度快 | 混合语言微服务架构,前端为 KMP、后端为既有微服务集群 |
|
||||
| **传统 RESTful (Ktor Client)** | HTTP/1.1 或 HTTP/2 + JSON | 标准通用,对外开放友好,与第三方平台对接便利 | 接口需对外公开(Open API)、或者对接既有老系统 |
|
||||
|
||||
> [!TIP]
|
||||
> 即使在某些必须使用传统 RESTful 架构的项目中,全栈 KMP 依然可以通过在 `core/commonMain` 中统一管理 `@Serializable` 数据类(Request/Response Model),让前后端共用同一套数据协议,从而显著减少联调错误。
|
||||
|
||||
## 接下来可以看
|
||||
|
||||
[目录](../README.md)
|
||||
|
||||
[下一篇:复杂逻辑与多平台不同逻辑](./复杂逻辑与多平台不同逻辑.md)
|
||||
[下一篇:复杂逻辑与 expect / actual](./复杂逻辑与多平台不同逻辑.md)
|
||||
Reference in New Issue
Block a user