前言

学习 KMP 时,Android 和 iOS 的路线相对容易理解:

commonMain
├── 共享业务逻辑
├── 共享数据模型
└── 共享网络请求

androidMain
└── Android 平台实现

iosMain
└── iOS 平台实现

但是到了 Web 端,Gradle 中突然出现了很多配置:

js {
    browser()
}
js {
    browser()
    binaries.executable()
}
wasmJs {
    browser()
    binaries.executable()
}

于是很容易产生一系列疑问:

  • js 和 wasmJs 有什么区别?
  • KMP 到底能不能直接开发 Web 页面?
  • wasmJs 打包出来的是网站吗?
  • 已经有 Vue 项目了,还需要 Compose Web 吗?
  • sharedLogic 能不能编译成 JS,让 Vue 直接调用?
  • 为什么有些项目使用 js,有些项目使用 wasmJs

这些问题之所以容易混乱,是因为我们把以下三个维度混到了一起:

1. Web 页面由谁开发?
2. Kotlin 最终编译成什么?
3. 当前模块是完整应用,还是共享代码库?

这一篇先不深入 Gradle 细节,而是先把 KMP Web 的整体路线讲清楚。


一、先给出整体结论

从项目架构角度看,KMP Web 主要有两种落地方式。

方式一:KMP 同时负责业务逻辑和 Web UI

Kotlin 业务逻辑
+
Compose Multiplatform UI
        ↓
    Kotlin/Wasm
        ↓
     浏览器运行

对应配置通常是:

wasmJs {
    browser()
    binaries.executable()
}

这种方式下,Web 页面不再由 Vue 或 React 编写,而是直接使用 Compose:

@Composable
fun LoginScreen() {
    Column {
        Text("用户登录")

        Button(
            onClick = {
                // 登录
            }
        ) {
            Text("登录")
        }
    }
}

这套 Compose 页面可以和 Android、iOS、桌面端共享。

当前官方将 Kotlin/Wasm 与 Compose Multiplatform 结合,作为浏览器端共享 UI 的主要路线;Compose Multiplatform Web 当前处于 Beta 阶段。


方式二:Vue 负责 Web UI,KMP 只共享业务逻辑

KMP sharedLogic
        ↓
编译成 JavaScript
        ↓
Vue 项目导入调用
        ↓
Vue 负责页面和交互

对应配置的核心是:

js {
    browser()
}

这种方式下:

Android UI → Compose
iOS UI     → Compose Multiplatform 或 SwiftUI
Web UI     → Vue

但是三端可以继续共享:

数据模型
参数校验
二维码解析
接口 Result
业务规则
状态计算
协议解析
网络请求逻辑

Kotlin/JS 会将 Kotlin 代码转换为 JavaScript,因此这些业务能力可以提供给 JavaScript、TypeScript、Vue 或 React 项目使用。


二、为什么又会看到三种配置?

虽然项目架构主要是两条路线,但是从编译形式看,确实可能看到三种情况。

① wasmJs + executable
② js + executable
③ js 共享库

它们分别解决不同问题。


三、第一种:Compose Multiplatform 开发完整 Web 应用

配置通常是:

kotlin {
    wasmJs {
        browser()
        binaries.executable()
    }
}

逐步翻译就是:

wasmJs
→ 把 Kotlin 编译成 WebAssembly

browser()
→ 程序运行在浏览器中

binaries.executable()
→ 当前模块是一个可以独立运行的完整应用

完整链路:

Kotlin 业务代码
+
Compose 页面
        ↓
编译为 Wasm
        ↓
生成网站发布文件
        ↓
部署到 Nginx
        ↓
浏览器访问

1. Compose 代码就是 Web 页面源码

例如:

@Composable
fun App() {
    Column {
        Text("设备管理系统")

        Button(
            onClick = {
                println("查询设备")
            }
        ) {
            Text("查询")
        }
    }
}

在 Android 中,这段代码会显示成 Android 页面。

在 iOS 中,它可以显示成 iOS 页面。

在 Web 中,则通过 Kotlin/Wasm 在浏览器中渲染。

因此,这里的 Compose 代码相当于 Vue 项目中的:

App.vue
LoginView.vue
HomeView.vue
DeviceList.vue

只是页面不再使用 HTML、CSS、Vue 编写,而是使用 Kotlin 和 Compose 编写。

Compose Multiplatform 的目标,就是让同一套声明式 UI 代码能够运行在 Android、iOS、桌面和 Web 等平台。


2. 打包结果类似 Vue 的 dist

Vue 项目通常是:

Vue 源码
↓
npm run build
↓
dist/
├── index.html
├── assets/index.js
└── assets/index.css

KMP Compose Web 则是:

Kotlin + Compose 源码
↓
Gradle 构建
↓
Web 发布目录
├── index.html
├── JavaScript 加载文件
├── WebAssembly 文件
└── 图片、字体等资源

所以可以简单理解为:

Vue 的 dist
≈
KMP Wasm 的 Web 发布目录

两者都属于:

已经构建完成、能够部署到 Web 服务器的网站文件。

区别只在于它们的源码和主要运行产物不同:

Vue
→ TypeScript/JavaScript + HTML + CSS
→ JavaScript

Compose Multiplatform Web
→ Kotlin + Compose
→ WebAssembly + JavaScript 加载代码

官方 Kotlin/Wasm 教程也将最终结果描述为可以发布成网站的构建产物。


3. index.html 只是启动外壳

Compose Web 项目中仍然可能存在:

index.html

但这不代表页面需要重新使用 HTML 编写。

这个 HTML 的主要职责是:

打开浏览器页面
↓
加载 JavaScript 启动代码
↓
加载 Wasm
↓
启动 Compose UI

类似 Android 项目中:

AndroidManifest.xml
+
MainActivity

负责提供应用入口。

真正的页面依然写在:

@Composable
fun App()

中。


四、第二种:Kotlin/JS 开发完整 Web 应用

有时我们还会看到:

js {
    browser {
        commonWebpackConfig {
            outputFileName = "webApp.js"
        }
    }

    binaries.executable()
}

它的意思是:

Kotlin
↓
编译成 JavaScript
↓
通过 Webpack 打包
↓
生成完整 Web 应用

这里依然是完整网站,只是没有编译成 Wasm,而是编译成 JavaScript。


它和 wasmJs 的区别

Kotlin/JS

js {
    browser()
    binaries.executable()
}

编译链路:

Kotlin
↓
JavaScript
↓
浏览器

最终主要产物可能是:

index.html
webApp.js
资源文件

Kotlin/JS 本身可以直接开发浏览器应用,并能够访问浏览器和 DOM API。


Kotlin/Wasm

wasmJs {
    browser()
    binaries.executable()
}

编译链路:

Kotlin
↓
WebAssembly
↓
浏览器

最终主要产物可能是:

index.html
app.js
app.wasm
资源文件

为什么现在 Compose Web 更常看到 wasmJs

因为当前 Compose Multiplatform 的 Web 共享 UI 路线,主要建立在 Kotlin/Wasm 之上:

Compose UI
↓
Kotlin/Wasm
↓
浏览器

而 Kotlin/JS 更适合以下场景:

  • 直接开发 JavaScript 浏览器程序;
  • 操作 DOM;
  • 使用 JavaScript 生态;
  • 把 Kotlin 业务逻辑提供给 JS/TS 项目;
  • 和 Vue、React 等前端框架集成。

因此:

js

不等于“只能做共享库”。

wasmJs

也不等于“唯一能做完整网站”。

两者都能产生浏览器程序,只是编译结果不同。官方分别提供了 Kotlin/JS 浏览器应用和 Kotlin/Wasm Compose 应用的完整入门路径。


五、第三种:sharedLogic 编译成 JS 给 Vue 使用

这才是已有 Vue 项目最关心的方式。

假设 KMP 项目结构是:

project
├── sharedLogic
├── sharedUI
├── androidApp
├── iosApp
└── web-vue

其中:

sharedLogic

负责:

数据模型
接口请求
业务规则
参数校验
二维码解析
状态计算
数据转换

而:

web-vue

继续负责:

HTML
CSS
Vue 组件
Vue Router
Pinia
Element Plus
浏览器交互

整体调用链:

sharedLogic/commonMain
        ↓
Kotlin/JS 编译
        ↓
JavaScript + TypeScript 声明
        ↓
Vue import
        ↓
Vue 页面调用

1. KMP 业务代码

例如:

class PhoneValidator {

    fun validate(phone: String): Boolean {
        return phone.length == 11 &&
            phone.all { it.isDigit() }
    }
}

Android 可以调用:

val valid = PhoneValidator().validate(phone)

iOS 可以通过 KMP 导出的 Framework 调用。

Web 端则可以把这段逻辑编译成 JavaScript,再提供给 Vue 使用。

Kotlin/JS 支持将 Kotlin 声明导出给 JavaScript 和 TypeScript;通过 @JsExport,可以让指定的 Kotlin API 以稳定名称暴露给外部 JavaScript。


2. Vue 中调用

前端的使用方式最终可能类似:

import { PhoneValidator } from "shared-logic"

const validator = new PhoneValidator()

const result = validator.validate("13800138000")

console.log(result)

这里 Vue 并不知道 Kotlin 内部如何实现。

Vue 只需要把 shared-logic 当成一个普通的 JavaScript 或 TypeScript 依赖。


3. Vue 最后仍然生成自己的 dist

完整流程是:

KMP sharedLogic
↓
编译成 JS 业务库
↓
Vue 导入 sharedLogic
↓
npm run build
↓
Vue dist
↓
部署网站

也就是说:

sharedLogic.js

只是 Vue 项目的一个依赖。

它自己并不是网站。

真正完整的网站仍然由 Vue 打包生成:

dist/
├── index.html
├── assets/index.js
├── assets/index.css
└── 其他资源

这是它和:

wasmJs {
    browser()
    binaries.executable()
}

最大的区别。


六、两条项目架构路线怎么区分?

现在回到最初的问题。

从项目架构角度看,主要还是两种方式。


路线一:KMP 直接负责 Web UI

Android UI ─┐
iOS UI     ├── Compose Multiplatform
Web UI     ┘

业务逻辑
└── commonMain

典型配置:

wasmJs {
    browser()
    binaries.executable()
}

结果:

KMP 自己生成完整网站

适合:

  • 新项目;
  • Kotlin 团队主导;
  • Android 已经全面使用 Compose;
  • 希望 Android、iOS、Web 共享页面;
  • 后台系统、内部工具、业务型应用;
  • 没有大量历史 Vue 组件。

路线二:Vue 负责 UI,KMP 共享逻辑

Android UI → Compose
iOS UI     → Compose 或 SwiftUI
Web UI     → Vue

三端业务逻辑
└── sharedLogic/commonMain

核心配置:

js {
    browser()
}

结果:

KMP 生成 JS 业务库
↓
Vue 导入
↓
Vue 生成完整网站

适合:

  • 已经有成熟 Vue 项目;
  • 前端团队熟悉 Vue;
  • 已经积累大量 Web 组件;
  • Web 页面需要充分使用 HTML、CSS、DOM;
  • 只希望复用业务规则和数据逻辑;
  • 不准备统一三端 UI。

七、那 js + executable 属于哪一类?

js {
    browser()
    binaries.executable()
}

它处于两条常见路线之间:

Kotlin 自己负责完整 Web 应用
但是最终编译成 JavaScript

它不是 Vue 项目,也不是给 Vue 使用的普通共享逻辑库。

可以将三种技术形式整理为:

配置形式 最终结果 页面由谁开发
wasmJs + executable 完整 Wasm Web 应用 Compose
js + executable 完整 JavaScript Web 应用 Kotlin
js 共享模块 JavaScript 业务库 Vue、React 等

所以:

项目架构上通常讨论两种路线,但底层技术上可能看到三种构建形态。

这正是很多初学者感觉混乱的根源。


八、不要把几个概念绑定死

误区一:看到 js 就认为是给 Vue 使用

不一定。

js {
    browser()
    binaries.executable()
}

这是一个可以独立运行的 Kotlin/JS Web 应用。

而:

js {
    browser()
}

如果没有生成可执行应用,才更可能是一个被其他模块或项目使用的代码模块。官方文档说明,省略 binaries.executable() 时,Kotlin/JS 默认生成的是 Kotlin 内部库产物,而不是可独立运行的应用文件。


误区二:只有 wasmJs 才能开发网站

不对。

下面两种都可以是完整 Web 应用:

js {
    browser()
    binaries.executable()
}
wasmJs {
    browser()
    binaries.executable()
}

区别只是:

js
→ 主要编译成 JavaScript

wasmJs
→ 主要编译成 WebAssembly

误区三:browser() 表示生成了页面

不对。

browser()

只表示:

当前编译结果运行在浏览器环境。

它没有决定:

  • 页面使用 Compose 还是 DOM;
  • 当前模块是应用还是库;
  • 最终编译成 JS 还是 Wasm。

这些分别由其他配置决定。


误区四:binaries.executable() 就是 Wasm 配置

也不对。

它既可以用于:

js {
    binaries.executable()
}

也可以用于:

wasmJs {
    binaries.executable()
}

它只负责表达:

当前模块需要生成可以独立运行的应用产物。

官方 Gradle DSL 中,jswasmJs 和 wasmWasi 等 Web 目标都支持 binaries.executable()


九、为什么有些新项目同时出现 js 和 wasmJs

某些较新的 Compose Multiplatform Web 项目中,可能同时看到:

js {
    browser()
    binaries.executable()
}

wasmJs {
    browser()
    binaries.executable()
}

这不一定代表项目同时开发了两套页面。

一种用途是:

现代浏览器
→ 使用 wasmJs

不满足 Wasm 能力的浏览器
→ 回退到 js

Kotlin 从 2.2.20 开始提供了 Compose Web 的 JS/Wasm 交叉编译与兼容回退能力,因此新模板中同时出现两种目标时,要结合项目配置整体判断。

这属于浏览器兼容方案,不是又增加了一种新的业务架构。


十、结合实际项目应该怎么选?

假设当前情况是:

Android 已经全面使用 Compose
iOS 正在接入 KMP/CMP
Web 前端长期使用 Vue
已经存在 Vue 项目和前端团队

更稳妥的架构通常是:

sharedLogic
├── commonMain
├── androidMain
├── iosMain
└── jsMain

sharedUI
├── commonMain
├── androidMain
└── iosMain

androidApp
iosApp
web-vue

平台关系:

模块 Android iOS Web
sharedLogic 使用 使用 使用
sharedUI 使用 使用 不使用
androidApp 使用 不使用 不使用
iosApp 不使用 使用 不使用
web-vue 不使用 不使用 使用

最终实现:

Android + iOS
→ 共享业务逻辑
→ 共享 Compose UI

Android + iOS + Web
→ 共享业务逻辑

Web
→ 保留 Vue UI

这样既不会推翻已有 Vue 技术体系,又能让真正容易重复和出错的业务规则保持一致。


十一、什么时候适合 Compose Web?

Compose Multiplatform Web 更适合:

新项目
内部管理系统
业务后台
设备控制台
跨平台工具
高度强调多端 UI 一致
Kotlin 团队主导

因为它最大的价值是:

不仅共享业务逻辑
还可以共享 UI

但当前 Compose Multiplatform Web 仍处于 Beta。对于公开网站、强 SEO 页面、复杂 Web 原生生态集成或成熟前端团队项目,需要结合实际要求评估。


十二、什么时候适合 Vue + KMP?

Vue + KMP 更适合:

已有 Vue 项目
已有成熟前端团队
大量使用 Web 组件库
页面强调 HTML 和 CSS 控制
希望保留 Web 原生生态
只需要共享业务逻辑

此时不要为了“跨平台”而强行把所有 Vue 页面迁移成 Compose。

KMP 的价值并不是:

所有平台必须完全使用同一套代码。

而是:

把真正适合复用的部分放到 commonMain,平台 UI 和平台能力继续保留自己的优势。


十三、最终总结

KMP Web 最容易混乱,是因为:

Web 架构路线
编译目标
运行环境
应用或代码库

这几个概念经常同时出现在 Gradle 中。

先记住下面这组关系:

js
= Kotlin 编译成 JavaScript

wasmJs
= Kotlin 编译成 WebAssembly

browser()
= 编译结果运行在浏览器

binaries.executable()
= 生成能够独立运行的完整应用

再看三种常见形式:

① Compose + wasmJs + executable
   → KMP 负责页面和业务逻辑
   → 生成完整 Web 应用
   → 类似 Vue 的 dist

② Kotlin/JS + executable
   → Kotlin 负责完整 Web 应用
   → 最终主要编译成 JavaScript

③ sharedLogic + Kotlin/JS
   → KMP 只提供业务逻辑
   → Vue 负责页面
   → Vue 最后生成 dist

站在实际项目架构角度,重点关注两条路线即可:

Web 页面也准备使用 Compose
→ Compose Multiplatform + wasmJs

Web 页面继续使用 Vue
→ Vue + Kotlin/JS sharedLogic

对已有 Vue 技术栈的项目而言,更稳妥的选择通常是:

Android、iOS 共享 UI 和业务逻辑
Android、iOS、Web 共享业务逻辑
Web 继续使用 Vue

这不是跨平台做得不彻底,而是根据不同平台和现有团队能力,选择更合理的共享边界。


下一篇预告

下一篇进入 Gradle 配置本身:

《KMP Web Gradle 配置看不懂?js、wasmJs、browser、executable 逐行拆解》

将重点讲清:

js 和 wasmJs 分别创建了什么 Target?

browser() 到底配置了什么?

binaries.executable() 为什么有时写、有时不写?

commonWebpackConfig 是干什么的?

outputFileName = "webApp.js" 控制了什么?

commonMain、jsMain、wasmJsMain 如何参与编译?

为什么有些新项目会同时存在 js 和 wasmJs?
Logo

一站式 AI 云服务平台

更多推荐