KMP Web 开发实战(三):sharedLogic 如何编译成 JS 库,供 Vue 项目调用?
前言
前两篇已经讲清楚了 KMP Web 的两条主要路线:
路线一:
Compose Multiplatform + wasmJs
→ KMP 同时负责 Web UI 和业务逻辑
→ 最终生成完整 Web 应用
路线二:
Vue + Kotlin/JS
→ Vue 继续负责 Web UI
→ KMP 只提供共享业务逻辑
这一篇进入第二条路线的实战:
把 KMP 中的
sharedLogic编译成 JavaScript/TypeScript 库,再由 Vue 项目导入使用。
这时,sharedLogic 不是一个网站,也不负责生成 Web 页面。
它的定位更像一个前端业务 SDK:
sharedLogic/commonMain
↓
Kotlin/JS 编译
↓
JavaScript 文件 + TypeScript 声明
↓
Vue 项目 import
↓
Vue 调用共享业务逻辑
↓
Vue 最终生成 dist
一、先明确最终目标
假设我们已经有一个 KMP 项目:
lift-kmp-core
├── sharedLogic
├── sharedUI
├── androidApp
└── iosApp
公司原有的 Web 项目使用 Vue:
web-vue
├── package.json
├── vite.config.ts
└── src
├── App.vue
├── views
└── components
我们希望最终形成:
lift-kmp-core
├── sharedLogic
│ ├── commonMain
│ ├── androidMain
│ ├── iosMain
│ └── jsMain
│
├── sharedUI
├── androidApp
├── iosApp
│
└── web-vue
其中各模块负责:
| 模块 | 主要职责 |
|---|---|
sharedLogic |
三端共享业务逻辑 |
sharedUI |
Android、iOS 共享 Compose UI |
androidApp |
Android 应用入口 |
iosApp |
iOS 应用入口 |
web-vue |
Vue 页面和 Web 交互 |
最终共享范围是:
Android + iOS
→ 共享业务逻辑
→ 共享 Compose UI
Android + iOS + Web
→ 共享业务逻辑
Web
→ 继续使用 Vue UI
二、哪些内容适合共享给 Vue?
适合放进 sharedLogic/commonMain 并提供给 Vue 使用的内容包括:
数据模型
表单参数校验
手机号、邮箱校验
二维码内容解析
设备协议解析
业务状态计算
权限判断规则
金额计算
日期规则
分页参数处理
接口参数组装
接口返回数据转换
统一错误码
业务状态机
例如:
class PhoneValidator {
fun validate(phone: String): Boolean {
return phone.length == 11 &&
phone.all { it.isDigit() }
}
}
这段逻辑可以同时被:
Android
iOS
Vue
调用。
不适合直接共享给 Vue 的内容
例如:
Android Context
Activity
CameraX
高德地图 Android SDK
UIKit
AVFoundation
Compose 页面
Android SharedPreferences
iOS NSUserDefaults
浏览器 DOM
这些属于平台能力,需要分别放在:
androidMain
iosMain
jsMain
中实现。
因此,KMP 并不是把所有代码都塞进 commonMain,而是:
把真正跨平台、与 UI 和系统 API 无关的业务规则放进
commonMain。
三、sharedLogic 和完整 Web 应用有什么区别?
Compose Web 完整应用一般使用:
wasmJs {
browser()
binaries.executable()
}
它会生成一个能够独立运行的网站。
而给 Vue 使用的 sharedLogic 使用:
js {
browser()
binaries.library()
}
它生成的是一个 JavaScript 库。
两者的区别是:
binaries.executable()
→ 我自己就是最终应用
→ 我需要独立运行
binaries.library()
→ 我是一个库
→ 由 Vue、React 等其他项目调用
因此,sharedLogic 不需要:
fun main()
也不需要:
index.html
因为最终网站入口仍然在 Vue 项目中。
四、给 sharedLogic 增加 JS Target
打开:
sharedLogic/build.gradle.kts
增加 Kotlin/JS Target。
一个适合作为起点的配置如下:
plugins {
alias(libs.plugins.kotlinMultiplatform)
}
kotlin {
androidLibrary {
namespace = "com.sqx.shared.logic"
compileSdk = 36
minSdk = 24
}
iosArm64()
iosSimulatorArm64()
js {
outputModuleName = "sharedLogic"
browser()
binaries.library()
generateTypeScriptDefinitions()
compilerOptions {
target = "es2015"
}
}
compilerOptions {
optIn.add("kotlin.js.ExperimentalJsExport")
}
sourceSets {
commonMain.dependencies {
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.serialization.json)
}
androidMain.dependencies {
// Android 平台依赖
}
iosMain.dependencies {
// iOS 平台依赖
}
jsMain.dependencies {
// JavaScript 平台依赖
}
}
}
这套配置与 Kotlin 官方提供的“将 KMP 模块构建成 JavaScript/TypeScript 库”的示例结构基本一致:使用 browser()、binaries.library()、generateTypeScriptDefinitions() 和 outputModuleName。
下面逐行拆解。
五、js {}:把 Kotlin 编译成 JavaScript
js {
}
表示:
给当前 KMP 模块增加 Kotlin/JS Target。
编译时会把:
commonMain
+
jsMain
组合起来,编译成 JavaScript。
完整链路:
sharedLogic/commonMain
+
sharedLogic/jsMain
↓
Kotlin/JS 编译器
↓
JavaScript
Kotlin/JS 的作用,就是把 Kotlin 代码、标准库以及兼容的依赖转换成 JavaScript,使其能够在 JavaScript 环境中运行。
六、browser():这个 JS 库用于浏览器
browser()
表示:
当前 JavaScript 目标主要运行在浏览器环境。
因为 Vue 项目最终运行在浏览器,所以这里使用:
browser()
如果是提供给 Node.js 后端使用,则可以配置:
nodejs()
Kotlin/JS 官方支持浏览器和 Node.js 两类主要执行环境。
但是要注意:
browser()
不代表生成了 Web 页面。
它只说明:
这个 JS 产物面向浏览器环境
页面仍然由 Vue 编写。
七、binaries.library():生成 JavaScript 库
binaries.library()
表示:
当前模块不是最终 Web 应用,而是要生成一个供其他 JavaScript/TypeScript 项目调用的库。
这和上一篇讲到的:
binaries.executable()
正好相反。
binaries.executable()
→ 最终应用
→ 有 main() 入口
→ 能够独立运行
binaries.library()
→ JavaScript 库
→ 不负责启动页面
→ 等待 Vue import
所以这里不要写:
fun main() {
}
sharedLogic 只负责提供 API。
八、generateTypeScriptDefinitions():生成 TypeScript 类型声明
generateTypeScriptDefinitions()
表示:
除了生成 JavaScript 文件,还要生成 TypeScript 的
.d.ts类型声明文件。
假设 Kotlin 中有:
@JsExport
class SharedSdk {
fun validatePhone(phone: String): Boolean {
return phone.length == 11
}
}
生成的 TypeScript 类型大致类似:
export class SharedSdk {
constructor()
validatePhone(phone: string): boolean
}
有了 .d.ts 文件以后,Vue 的 TypeScript 代码可以获得:
方法自动补全
参数类型提示
返回值类型提示
编译期类型检查
IDE 跳转和提示
Kotlin/JS 编译器会收集使用 @JsExport 标记的顶层声明,并据此生成 TypeScript 类型定义;官方也特别说明,这项能力很适合跨端共享业务逻辑的场景。
九、outputModuleName 控制什么?
outputModuleName = "sharedLogic"
表示:
设置 Kotlin/JS 生成的 JavaScript 模块名称。
它会影响类似目录和文件:
build/js/packages/sharedLogic/
以及生成的:
sharedLogic.js
sharedLogic.d.ts
但它不一定等于:
npm 公共仓库里的包名
npm 发布时使用的包名,可以另外配置。
官方文档说明,outputModuleName 会影响生成的 JavaScript 模块,以及对应的 .js 和 .d.ts 文件,但不会改变 Webpack 的最终输出名称。
十、为什么使用 target = "es2015"?
compilerOptions {
target = "es2015"
}
表示:
生成面向 ES2015,也就是 ES6 及以上 JavaScript 环境的代码。
这使生成结果可以使用更现代的 JavaScript 能力,例如:
JavaScript class
ES Module
generator
更现代的模块结构
对于 Vue 3、Vite 等现代前端项目,通常更合适。
Kotlin 官方文档说明,设置 ES2015 目标后,可以启用模块、类和生成器等现代 JavaScript 特性;当前文档还说明,ES2015 目标默认使用 ES Module 形式。
也可以显式写:
js {
browser()
useEsModules()
binaries.library()
generateTypeScriptDefinitions()
}
useEsModules() 表示生成使用:
import
export
语法的 ES Module,通常更符合 Vue 和 Vite 的使用习惯。Kotlin/JS 也支持 UMD、CommonJS、AMD、Plain 和 ES Module 等模块系统。
十一、不是所有 commonMain 依赖都能编译到 JS
增加:
js {
browser()
}
以后,commonMain 中的代码还需要满足一个条件:
所有参与编译的依赖都必须支持 Kotlin/JS。
例如下面这些跨平台库通常可以放在 commonMain:
kotlinx-coroutines
kotlinx-serialization
Ktor Client Core
纯 Kotlin 数据模型
纯 Kotlin 业务规则
但下面这些 Android 专属库不能参与 JS 编译:
androidx.lifecycle
CameraX
Room Android
Android Context
高德地图 Android SDK
Android SharedPreferences
否则在编译 JS Target 时会找不到对应产物。
官方文档明确指出,并不是所有 Kotlin 库都支持 JavaScript Target;只有包含 Kotlin/JS 产物的依赖才能用于 JS 编译。
因此,KMP 模块中应该这样划分:
commonMain
→ 只依赖真正跨平台的库
androidMain
→ Android 专属依赖
iosMain
→ iOS 专属依赖
jsMain
→ 浏览器和 JavaScript 专属依赖
十二、使用 @JsExport 暴露 API
仅仅写:
class SharedSdk
并不能保证 Vue 能够通过稳定、清晰的名称直接调用。
需要使用:
@JsExport
例如在 commonMain 中创建:
sharedLogic/src/commonMain/kotlin/
└── web
└── SharedSdk.kt
代码如下:
package com.sqx.shared.web
import kotlin.js.JsExport
@JsExport
class ValidationResult(
val valid: Boolean,
val message: String
)
@JsExport
class SharedSdk {
fun validatePhone(phone: String): ValidationResult {
val normalizedPhone = phone.trim()
val valid = normalizedPhone.length == 11 &&
normalizedPhone.all { it.isDigit() }
return if (valid) {
ValidationResult(
valid = true,
message = "手机号格式正确"
)
} else {
ValidationResult(
valid = false,
message = "请输入11位手机号"
)
}
}
fun parseQrCode(rawValue: String): String {
return rawValue
.trim()
.removePrefix("QR:")
}
fun calculateMaintenanceStatus(
remainingDays: Int
): String {
return when {
remainingDays < 0 -> "OVERDUE"
remainingDays <= 7 -> "EXPIRING_SOON"
else -> "NORMAL"
}
}
}
这里的:
@JsExport
可以理解成:
将这个 Kotlin 类作为公共 JavaScript API 导出。
@JsExport 可以写在 KMP 的 commonMain 中,它只会在编译 JavaScript Target 时产生导出效果,不会影响 Android 和 iOS 的正常调用。
十三、为什么建议增加一个 SharedSdk 门面?
假设 sharedLogic 内部有:
Repository
UseCase
Ktor Client
Result<T>
StateFlow
sealed class
缓存实现
数据库实现
不建议把这些内部结构全部暴露给 Vue。
错误做法:
Vue
↓
直接调用 Repository
↓
直接订阅 Flow
↓
直接处理 Kotlin Result
↓
直接感知各种内部类型
这样会导致:
KMP 内部实现和 Vue 强耦合
Kotlin 类型很难映射到 TypeScript
后续重构影响前端
生成的 API 不够友好
更合理的结构是:
Vue
↓
SharedSdk
↓
UseCase
↓
Repository
↓
网络、缓存和业务实现
也就是说,对外只暴露一层稳定的 JS 门面:
@JsExport
class SharedSdk
而内部仍然可以自由使用:
Repository
UseCase
DTO
Mapper
Ktor
协程
Vue 不需要知道内部实现。
十四、哪些类型适合直接暴露?
对 JavaScript、TypeScript 最友好的类型包括:
String
Boolean
Int
Double
普通导出类
数组
可空类型
例如:
@JsExport
class DeviceInfo(
val id: String,
val name: String,
val online: Boolean
)
Vue 使用时比较直观:
const device = sdk.getDevice()
console.log(device.id)
console.log(device.name)
console.log(device.online)
不建议直接暴露的类型
例如:
Flow<T>
StateFlow<T>
Result<T>
复杂泛型
平台类型
内部 Repository
Kotlin 特有集合嵌套
未经设计的 sealed class 层级
Kotlin 集合在 JavaScript 中并不总是直接表现为原生 JavaScript Array、Map 和 Set。例如,当前 Kotlin/JS 会将 List、Map、Set 映射成相应的 Kotlin 集合包装类型,并提供转换视图。
因此,对外 API 最好做一次转换:
// 内部可以使用复杂类型
internal fun loadDevices(): List<Device>
// 对外转换成更适合 JS 的结构
@JsExport
fun getDeviceNames(): Array<String> {
return loadDevices()
.map { it.name }
.toTypedArray()
}
设计原则是:
KMP 内部可以保持 Kotlin 风格,对 Vue 暴露的边界应该尽量符合 JavaScript 风格。
十五、执行构建
Gradle 配置和导出类完成后,可以先执行:
./gradlew :sharedLogic:clean :sharedLogic:build
这里使用通用的:
build
任务,避免不同 Kotlin Gradle 插件版本下具体任务名称略有区别。
构建完成后,产物通常位于类似目录:
sharedLogic/build/dist/js/productionLibrary/
可能包含:
productionLibrary/
├── package.json
├── sharedLogic.js
├── sharedLogic.d.ts
└── 相关运行依赖
Kotlin/JS 的默认发布目录遵循:
build/dist/<targetName>/<binaryName>
而生成的未经过 Webpack 合并的 JavaScript 和 .d.ts 文件,也可能出现在:
build/js/packages/<moduleName>/kotlin/
中。具体目录会受到 Kotlin 插件版本、Target 名称和构建任务影响。
如果没有找到产物,可以查看当前模块的 Gradle 任务:
./gradlew :sharedLogic:tasks --all
然后搜索:
Library
Distribution
Production
Js
十六、检查生成的 TypeScript 声明
打开生成的:
sharedLogic.d.ts
大概可以看到:
export class ValidationResult {
constructor(
valid: boolean,
message: string
)
readonly valid: boolean
readonly message: string
}
export class SharedSdk {
constructor()
validatePhone(phone: string): ValidationResult
parseQrCode(rawValue: string): string
calculateMaintenanceStatus(
remainingDays: number
): string
}
如果 .d.ts 几乎是空的,优先检查:
1. 是否配置了 generateTypeScriptDefinitions()
2. 是否使用了 binaries.library()
3. 类和函数是否加了 @JsExport
4. 构建的是否为生产 Library
5. 导出方法是否使用了不支持的参数类型
只有真正导出的声明,才会进入对外 TypeScript 类型文件。
十七、Vue 项目如何安装本地 JS 库?
假设项目结构是:
project-root
├── sharedLogic
└── web-vue
先构建:
./gradlew :sharedLogic:build
然后进入 Vue 项目:
cd web-vue
安装本地生成的包:
npm install ../sharedLogic/build/dist/js/productionLibrary
也可以在 Vue 的 package.json 中声明:
{
"dependencies": {
"shared-logic": "file:../sharedLogic/build/dist/js/productionLibrary"
}
}
npm 支持将包含 package.json 的本地目录作为依赖安装;本地路径依赖适合联调和测试,正式发布则通常使用 npm Registry 或公司私有 npm 仓库。
需要注意:
Vue 中实际使用的 import 名称,以生成目录中
package.json的name字段为准。
例如生成的 package.json 是:
{
"name": "shared-logic",
"version": "0.1.0"
}
Vue 中就应该写:
import { SharedSdk } from "shared-logic"
而不是根据 Gradle 模块名猜测。
十八、在 Vue 中调用 SharedSdk
例如 Vue 3 页面:
<script setup lang="ts">
import { ref } from "vue"
import {
SharedSdk,
ValidationResult
} from "shared-logic"
const sdk = new SharedSdk()
const phone = ref("")
const message = ref("")
const valid = ref(false)
function validatePhone() {
const result: ValidationResult =
sdk.validatePhone(phone.value)
valid.value = result.valid
message.value = result.message
}
</script>
<template>
<div class="phone-form">
<input
v-model="phone"
placeholder="请输入手机号"
/>
<button @click="validatePhone">
校验手机号
</button>
<p>
校验结果:{{ message }}
</p>
</div>
</template>
此时执行链路是:
用户点击 Vue 按钮
↓
Vue 调用 SharedSdk
↓
执行 commonMain 中的 Kotlin 逻辑
↓
返回 ValidationResult
↓
Vue 更新页面
这里的业务规则只写了一份:
Android 调用同一份 Kotlin 逻辑
iOS 调用同一份 Kotlin 逻辑
Vue 调用编译后的 JavaScript
十九、Vue 页面最终如何打包?
KMP 生成的 JS 库并不是最终网站。
完整流程是:
sharedLogic Kotlin 源码
↓
Gradle 构建
↓
shared-logic JS/TS 库
↓
Vue npm install
↓
Vue import SharedSdk
↓
npm run build
↓
Vue dist
最终部署的仍然是 Vue 的:
web-vue/dist/
├── index.html
├── assets
│ ├── index.js
│ └── index.css
└── 其他资源
KMP 生成的逻辑代码会被 Vite 打包进 Vue 的最终 JavaScript 产物中。
所以要分清两个构建过程:
Gradle
→ 负责把 Kotlin sharedLogic 构建成 JS 库
Vite
→ 负责把 Vue 页面和 JS 库一起构建成网站
它们是前后衔接关系,不是互相替代关系。
二十、Gradle、npm 和 Vite 分别负责什么?
Gradle
负责:
编译 Kotlin
处理 KMP Source Set
处理 Kotlin 依赖
生成 JavaScript
生成 TypeScript 声明
生成 npm 包结构
Kotlin/JS 项目使用 Gradle 管理构建,同时会在后台处理 npm 依赖并生成 package.json。
npm
负责:
安装 shared-logic
管理 Vue 项目依赖
管理 package.json
管理 node_modules
Vite
负责:
编译 Vue 文件
处理 HTML 和 CSS
打包 Vue 组件
合并前端依赖
生成最终 dist
所以整个链路是:
Gradle
→ 生成 shared-logic
npm
→ 把 shared-logic 安装进 Vue
Vite
→ 把 Vue 和 shared-logic 一起打包
二十一、异步接口和 suspend 怎么处理?
实际项目中,sharedLogic 很可能包含:
suspend fun login()
suspend fun loadDevices()
suspend fun requestData()
Kotlin 2.3.0 开始支持实验性的 suspend 函数直接导出给 JavaScript。启用后,JavaScript 侧可以把它当成异步函数和 Promise 使用,但目前仍需开启对应编译选项。
Gradle 中增加:
kotlin {
compilerOptions {
freeCompilerArgs.add(
"-Xenable-suspend-function-exporting"
)
}
}
然后可以导出:
@JsExport
class LoginSdk {
suspend fun login(
username: String,
password: String
): LoginResult {
return loginUseCase.execute(
username = username,
password = password
)
}
}
Vue 中的调用形式大致是:
const result = await loginSdk.login(
username,
password
)
不过在企业项目中,更建议把异步接口也放在专门的 Web 门面中,不要让 Vue 直接感知:
CoroutineScope
Flow
Job
Dispatcher
Repository
也就是说,Vue 只应该看到:
普通同步方法
Promise 风格异步方法
JavaScript 友好的 DTO
清晰的错误对象
二十二、不要直接把 Flow 暴露给 Vue
例如 KMP 内部可能有:
val uiState: StateFlow<DeviceState>
不要直接设计成:
Vue 订阅 Kotlin StateFlow
虽然理论上可以继续做桥接,但会让前端和 Kotlin 协程模型强绑定。
更合理的方式有两种。
方式一:Vue 自己管理状态
Vue
↓
调用 SharedSdk 的方法
↓
获得结果
↓
存入 Pinia 或 ref
方式二:jsMain 做事件回调桥接
Kotlin 对外暴露:
fun observeDeviceStatus(
callback: (String) -> Unit
)
内部再把 Flow 转换成回调。
这样 Vue 不需要理解:
StateFlow
collect
CoroutineScope
Job
共享的是业务能力,而不是强行把 Kotlin 的全部编程模型暴露给前端。
二十三、涉及平台能力怎么办?
假设 commonMain 中需要保存 Token。
可以定义:
interface TokenStorage {
fun saveToken(token: String)
fun getToken(): String?
}
Android 使用:
SharedPreferences
DataStore
iOS 使用:
NSUserDefaults
Keychain
Web 使用:
localStorage
sessionStorage
IndexedDB
项目结构可以是:
commonMain
└── TokenStorage 接口和业务逻辑
androidMain
└── AndroidTokenStorage
iosMain
└── IosTokenStorage
jsMain
└── WebTokenStorage
所以增加 JS Target 后,并不是所有平台能力都自动跨端了。
正确思路仍然是:
commonMain 定义抽象
androidMain 实现 Android
iosMain 实现 iOS
jsMain 实现浏览器
二十四、是否必须发布到 npm?
不必须。
开发阶段可以直接使用本地路径:
npm install ../sharedLogic/build/dist/js/productionLibrary
适合:
本地联调
同一仓库开发
快速验证
暂时不搭建私有 npm
正式项目则可以发布到:
npm 公共仓库
公司私有 npm Registry
GitHub Packages
其他兼容 npm 的制品仓库
官方当前提供了完整的 KMP JavaScript 库 npm 发布教程,并使用 npm 发布插件配置包名、版本、认证信息和 package.json 元数据。
一个简化的发布配置形式类似:
plugins {
alias(libs.plugins.kotlinMultiplatform)
kotlin("npm-publish") version "当前适配版本"
}
npmPublish {
packages {
named("js") {
packageName = "shared-logic"
version = "0.1.0"
}
}
}
发布以后,Vue 项目可以直接:
npm install shared-logic
然后:
import { SharedSdk } from "shared-logic"
二十五、同一个仓库还是两个仓库?
两种方式都可以。
方式一:放在同一个仓库
project
├── sharedLogic
├── androidApp
├── iosApp
└── web-vue
优点:
修改业务逻辑后方便立即联调
版本统一
提交记录集中
适合小团队
但是它仍然有两套构建体系:
KMP 部分
→ Gradle
Vue 部分
→ npm + Vite
这很正常。
方式二:分成两个仓库
kmp-shared-sdk
└── 发布 shared-logic npm 包
web-project
└── npm install shared-logic
优点:
移动端和 Web 团队独立开发
版本边界清晰
适合 SDK 化管理
适合多个 Web 项目复用
这时 KMP 项目每次发布:
shared-logic 1.0.0
shared-logic 1.1.0
shared-logic 2.0.0
Vue 项目按版本依赖:
{
"dependencies": {
"shared-logic": "^1.1.0"
}
}
二十六、常见问题排查
问题一:生成了 JS,但 Vue 找不到类
检查 Kotlin 类是否加了:
@JsExport
没有导出的声明,不会以预期的公共 API 形式出现在 JavaScript 和 TypeScript 中。
问题二:生成了 .d.ts,但是内容为空
检查:
是否调用 generateTypeScriptDefinitions()
是否有 @JsExport 声明
是否构建了生产 Library
导出类型是否受支持
问题三:Vue import 的包名不对
不要只看:
outputModuleName
应打开产物中的:
package.json
查看:
{
"name": "..."
}
Vue 的 import 名称以这个字段为准。
问题四:commonMain 原本能编译,增加 js 后报错
通常是某个依赖只支持 Android、JVM 或 Native,没有 Kotlin/JS 产物。
检查:
commonMain.dependencies
把平台专属依赖移动到:
androidMain
iosMain
jsMain
官方文档也强调,只有提供 Kotlin/JS 产物的库才能参与 JavaScript Target 编译。
问题五:Vue 能导入,但方法名很奇怪
Kotlin 为了处理重载和签名冲突,部分声明可能发生名称修饰。
可以通过:
@JsName("validatePhone")
指定更稳定的 JavaScript 名称。Kotlin 官方文档说明,@JsName 可以用于控制生成的 JavaScript 名称,尤其适合处理重载带来的名称变化。
例如:
@JsExport
class SharedSdk {
@JsName("validatePhone")
fun validatePhoneValue(
phone: String
): Boolean {
return phone.length == 11
}
}
二十七、完整关系图
最终项目链路可以整理成:
commonMain
│
┌────────────────┼────────────────┐
│ │ │
Android 编译 iOS 编译 Kotlin/JS 编译
│ │ │
Android App iOS App shared-logic
│
↓
Vue import
│
↓
Vite 打包
│
↓
dist
也就是说:
Android 和 iOS
→ 可以共享业务逻辑和 Compose UI
Android、iOS 和 Web
→ 共享 commonMain 业务逻辑
Vue
→ 继续负责 Web 页面
二十八、最终总结
把 sharedLogic 提供给 Vue 使用,需要完成四件事。
第一步:增加 Kotlin/JS Target
js {
browser()
}
表示:
把 Kotlin 编译成浏览器使用的 JavaScript
第二步:声明它是一个库
binaries.library()
表示:
不是完整网站
而是供 Vue 调用的 JavaScript 库
第三步:导出 TypeScript API
generateTypeScriptDefinitions()
配合:
@JsExport
生成:
JavaScript 实现
+
TypeScript .d.ts 类型声明
第四步:Vue 安装并调用
npm install <sharedLogic生成目录>
Vue 中:
import { SharedSdk } from "shared-logic"
const sdk = new SharedSdk()
完整链路:
commonMain
↓
Kotlin/JS
↓
JS + d.ts
↓
Vue import
↓
Vite build
↓
dist
因此,这条路线最准确的架构描述是:
Android 和 iOS 可以使用 KMP/CMP 共享业务逻辑和 UI;Web 继续使用 Vue,只复用
sharedLogic中真正跨平台的业务能力。
这不是脱离了 KMP 项目,也不是又开发了一套后端。
它只是让:
Gradle 构建 Kotlin 业务库
npm 管理前端依赖
Vite 构建最终 Web 页面
三者各自承担自己最擅长的工作。
更多推荐

所有评论(0)