KMP Web 开发实战(一):Compose Web、Kotlin/JS、Vue + KMP 到底是什么关系?
前言
学习 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 中,js、wasmJs 和 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?更多推荐


所有评论(0)