前言
学习 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.cssKMP 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 能力的浏览器 → 回退到 jsKotlin 从 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?