前言

前两篇已经讲清楚了 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 ArrayMap 和 Set。例如,当前 Kotlin/JS 会将 ListMapSet 映射成相应的 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 页面

三者各自承担自己最擅长的工作。

Logo

一站式 AI 云服务平台

更多推荐