引言
最近在研究 KMP Web 时,我看到了一段配置:
@OptIn(ExperimentalWasmDsl::class)
wasmJs {
browser()
binaries.executable()
}
前面的 browser() 还比较容易理解,表示程序运行在浏览器中。
但 wasmJs 又是什么?
继续往下了解,就会遇到一个此前可能很少听说的概念:
WebAssembly
简称 Wasm
如果以前主要从事 Android、Java 后端或者普通 Vue 业务开发,确实可能很少直接接触 WebAssembly。
我一开始也有几个疑问:
WebAssembly 是不是一种新的前端框架?
它和 Vue 打包出来的 dist 有什么区别?
JavaScript 调用 Wasm,为什么感觉很像 Android 的 JNI?
KMP 为什么可以直接使用 Wasm 开发完整的 Web 页面?
这一篇就站在 Android 开发者的角度,把这些问题一次讲清楚。
一、WebAssembly 到底是什么?
WebAssembly,简称 Wasm。
官方将它定义为一种面向栈式虚拟机的二进制指令格式,同时也是多种编程语言可以选择的可移植编译目标。C、C++、Rust、Kotlin 等语言都可以将代码编译成 Wasm,再交给支持 WebAssembly 的运行环境执行。
可以先把它理解成:
C / C++ / Rust / Kotlin
↓
编译
↓
.wasm
↓
WebAssembly 运行时
因此,WebAssembly 并不是 Vue、React 这样的前端框架,也不是专门用来写网页样式的语言。
它和下面这些东西不属于同一层:
Vue、React、Compose
属于 UI 框架层
JavaScript、JVM Bytecode、WebAssembly
更接近编译目标和运行格式层
从 Android 开发者熟悉的技术来看,可以进行一个初步类比:
Java / Kotlin
↓
编译成 JVM 字节码
↓
由 JVM 或 ART 执行
而 WebAssembly 是:
Kotlin / Rust / C++
↓
编译成 Wasm
↓
由 Wasm 运行时执行
这只是帮助理解,JVM 字节码和 Wasm 在运行模型、类型系统、内存管理等方面仍然存在明显区别。
二、为什么以前很少听说 WebAssembly?
这并不代表 WebAssembly 没有人使用,而是普通业务开发者很少需要直接接触它。
例如,传统 Android 开发链路是:
Kotlin / Java
↓
Android Runtime
↓
Android 应用
Java 后端开发链路是:
Java / Kotlin
↓
JVM
↓
服务器应用
Vue 前端开发链路通常是:
Vue / TypeScript
↓
JavaScript
↓
浏览器
这些技术链路已经能够完成绝大多数业务开发,没有必要额外引入 Wasm。
WebAssembly 更容易出现在下面这些场景中:
图像处理
音视频编解码
文件压缩
加密算法
浏览器游戏
3D 与图形渲染
CAD
科学计算
已有 C/C++ 或 Rust 库迁移到浏览器
跨平台 UI 框架
在这些场景中,开发者可能希望:
复用已有的非 JavaScript 代码
在浏览器中执行计算密集型逻辑
让同一套核心代码运行在多个平台
因此,很多前端开发者虽然使用过依赖 Wasm 的第三方库,却不一定直接编写过 .wasm 模块。
而我现在突然遇到 WebAssembly,是因为开始研究:
Kotlin Multiplatform
+
Compose Multiplatform
+
Web
这已经进入了跨平台编译、浏览器运行时和多目标产物这一层。
三、Kotlin 进入 Web 的两条路线
Kotlin 官方目前提供两种主要的 Web 开发路线:
Kotlin/JS
Kotlin/Wasm
二者的编译链路不同。
1. Kotlin/JS
Kotlin
↓
编译或转换成 JavaScript
↓
浏览器执行 JavaScript
对应的 Gradle 配置可能是:
js {
browser()
}
Kotlin/JS 更适合:
将 Kotlin 业务逻辑提供给 Vue、React 等项目使用
和现有 JavaScript、TypeScript 生态深度交互
使用 Web 原生 HTML、DOM 和 CSS 开发页面
例如:
sharedLogic
↓
编译成 JavaScript
↓
Vue 项目导入使用
这种模式下,Vue 仍然负责页面,Kotlin 主要负责共享业务逻辑。
2. Kotlin/Wasm
Kotlin
↓
编译成 WebAssembly
↓
浏览器中的 Wasm 运行时执行
对应配置可能是:
wasmJs {
browser()
binaries.executable()
}
Kotlin/Wasm 可以配合 Compose Multiplatform,共享 Android、iOS、桌面端和 Web 端的部分业务逻辑与 UI。Kotlin 官方目前将 Kotlin/Wasm 标记为 Beta,并将“共享业务逻辑和 UI”列为它的重要使用方向。
四、Vue 的 dist 和 WebAssembly 有什么区别?
这是最容易混淆的地方。
Vue 项目执行:
npm run build
通常会生成一个 dist 目录:
dist/
├── index.html
├── assets/
│ ├── index-xxxx.js
│ ├── index-xxxx.css
│ └── 图片、字体等资源
Vue 官方文档说明,生产构建默认会生成可以部署的 dist 目录。
这个目录是一个完整的部署产物。
浏览器访问网站时,大致流程是:
打开 index.html
↓
加载 JavaScript 和 CSS
↓
启动 Vue
↓
创建并更新页面 DOM
那么 Kotlin/Wasm 呢?
执行:
./gradlew wasmJsBrowserDistribution
官方示例项目会将可发布产物生成到:
webApp/build/dist/wasmJs/productionExecutable
这个目录可以作为网站产物进行部署。
目录中通常会包含:
productionExecutable/
├── index.html
├── 应用程序.wasm
├── JavaScript 或 MJS 启动文件
├── Compose 资源
├── 图片
├── 字体
└── 其他静态资源
具体文件名称会随着项目名、Kotlin 版本和构建配置变化,但整体作用类似。
因此,不能这样比较:
Vue 的 dist
VS
WebAssembly
因为二者不属于同一层。
dist 是一个完整的部署目录,而 .wasm 只是其中一种程序文件格式。
正确的对应关系应该是:
Vue 的 dist 目录
≈
Kotlin/Wasm 的 productionExecutable 目录
进一步拆开看:
Vue:
dist/
├── index.html
├── JavaScript
├── CSS
└── 静态资源
Kotlin/Wasm:
productionExecutable/
├── index.html
├── .wasm
├── JS/MJS 启动与桥接代码
└── 静态资源
所以,Wasm 不是用来替代 dist 的。
更准确地说:
.wasm 是 Kotlin/Wasm 最终部署目录中的核心程序文件之一。
五、两者的部署方式其实很像
从部署人员的角度看,Vue 和 Kotlin/Wasm 没有想象中差别那么大。
Vue:
Vue 源码
↓
npm run build
↓
dist
↓
Nginx / CDN / 静态服务器
Kotlin/Wasm:
Kotlin + Compose 源码
↓
wasmJsBrowserDistribution
↓
productionExecutable
↓
Nginx / CDN / 静态服务器
最终都是把构建后的静态资源目录发布到 Web 服务器。
用户访问:
https://example.com
浏览器再加载对应的 HTML、脚本、Wasm 和资源文件。
所以,从“构建并部署一个前端应用”的角度看:
Vue dist
和
Kotlin/Wasm productionExecutable
承担的是相同层面的职责。
真正的区别在于,应用主体最终被编译成了什么,以及页面由什么框架负责显示。
六、Vue 与 Kotlin/Wasm 的运行方式有什么不同?
Vue 的运行链路
Vue / TypeScript
↓
打包成 JavaScript
↓
浏览器执行 JavaScript
↓
操作 HTML DOM
↓
显示页面
例如:
<template>
<button @click="count++">
Count: {{ count }}
</button>
</template>
Vue 的核心仍然建立在:
HTML
CSS
DOM
JavaScript
之上。
Kotlin/Wasm + Compose 的运行链路
Kotlin + Compose
↓
编译成 WebAssembly
↓
浏览器加载 Wasm
↓
Compose Runtime 运行
↓
显示页面
例如:
@Composable
fun App() {
var count by remember { mutableStateOf(0) }
Button(
onClick = { count++ }
) {
Text("Count: $count")
}
}
这段代码和 Android Jetpack Compose 非常接近。
也就是说,当项目使用 Kotlin/Wasm + Compose Multiplatform 时,开发者可以直接使用 Kotlin 和 Compose 实现 Web UI,而不一定需要再使用 Vue 编写同一套页面。
七、为什么我觉得 WebAssembly 很像 JNI?
真正让我快速理解 Wasm 的,是 Android 中的 JNI。
Android 中一条典型的 JNI 调用链路是:
Kotlin / Java
↓
JNI 桥接
↓
C / C++ Native 方法
↓
.so 动态库
例如,Android 上层可能声明:
external fun decode(data: ByteArray): ByteArray
实际实现位于 C 或 C++ 动态库中。
而传统 WebAssembly 的调用链路可能是:
Vue / JavaScript
↓
JavaScript 与 Wasm 互操作
↓
C++ / Rust / Kotlin 等代码
↓
.wasm 模块
JavaScript 可能调用 Wasm 导出的函数:
const result = wasmModule.decode(data)
从开发者视角看,两条链路确实很像:
上层业务代码
↓
跨运行环境调用
↓
另一种语言生成的编译产物
↓
执行底层能力
WebAssembly 标准提供了 JavaScript API,用于加载、编译、实例化 Wasm 模块,并处理模块的导入与导出。
因此,可以建立下面这组对应关系:
| Java/Kotlin | JavaScript/Vue |
| JNI 调用边界 | JS 与 Wasm 互操作边界 |
| C/C++ 方法 | Wasm 导出函数 |
| .so 动态库 | .wasm 模块 |
| Native 内存 | Wasm 线性内存 |
| Native 回调 Java | Wasm 调用导入的 JavaScript 函数 |
所以,严格来说:
.wasm 更接近 Android 的 .so
JavaScript 与 Wasm 的互操作机制
更接近 JNI
不过,从整个调用流程和开发体验来看,把传统 Wasm 理解为“Web 里的 JNI 模式”,是一个非常有效的类比。
八、为什么二者的开发体验很相似?
JNI 和 JavaScript/Wasm 互操作都要面对跨边界调用问题。
例如:
参数如何传递?
字符串怎么转换?
数组怎么传递?
对象能不能直接传?
内存由谁分配和释放?
异常如何跨边界处理?
回调如何实现?
高频调用是否会有额外开销?
JNI 中,Java 字符串需要转换为 Native 能够处理的数据:
Java String
↓
jstring
↓
char* 或其他 Native 表示
在传统 Wasm 互操作中,字符串和复杂对象也通常不能像普通 JavaScript 对象那样直接存在于 Wasm 线性内存中,往往需要编码、复制、地址和长度等配合。
概念上可以理解为:
JavaScript String
↓
编码和桥接
↓
Wasm 线性内存中的数据
因此,两种模式都会强调:
尽量减少没有必要的跨边界调用
接口尽量设计得粗粒度一些
避免频繁传递大量复杂对象
明确数据和内存的生命周期
这也是为什么有 JNI 经验的 Android 开发者,会很自然地感觉 Wasm 的调用方式似曾相识。
九、但 Wasm 和 JNI 不能完全画等号
虽然调用流程相似,但两者仍然存在重要区别。
1. JNI 是桥接机制,Wasm 是程序格式和运行模型
JNI 的核心职责是:
让 Java/Kotlin 与 Native 代码互相调用
而 WebAssembly 的核心是:
定义一种可移植的二进制指令格式
以及对应的执行模型
因此:
JNI 更像一座桥
Wasm 更像桥另一边运行的程序格式
更准确的对照仍然是:
JNI
≈
JS/Wasm 互操作机制
.so
≈
.wasm
2. Native .so 与 Wasm 的运行环境不同
JNI 加载的 .so 是本机机器码,运行在应用进程的 Native 环境中。
Wasm 模块运行在 WebAssembly 运行时中。WebAssembly 在 Web 环境下采用沙箱执行模型,并受到浏览器同源策略和权限模型约束。
所以 Wasm 并不是把一段普通 Native 机器码直接扔进浏览器运行。
3. Wasm 不一定只是底层能力库
JNI 在 Android 中通常是:
Kotlin/Java 负责应用主体
Native 负责部分底层能力
但 Kotlin/Wasm 可以不止做到这一点。
十、WebAssembly 有两种不同的使用方式
第一种:作为 Web 项目的底层能力模块
这是最接近 JNI 的模式。
Vue / React
↓
JavaScript
↓
Wasm
↓
算法、压缩、图像、音视频等能力
例如,一个在线图片处理网站可以这样分工:
Vue:
页面布局
菜单
按钮
文件选择
进度展示
用户交互
Wasm:
图片解码
滤镜计算
格式转换
图片压缩
复杂算法
这和 Android 项目中的分工很像:
Kotlin / Compose:
页面和业务交互
JNI + .so:
图片处理、音视频、算法等底层能力
在这种模式下,可以把 Wasm 理解为 Web 项目的 Native 能力模块。
第二种:作为完整 Web 应用的运行主体
Kotlin/Wasm + Compose Multiplatform 又向前走了一步。
Kotlin + Compose
↓
Kotlin/Wasm
↓
完整 Web 应用
此时,Kotlin/Wasm 不只是提供一个算法函数,而是可能承担:
UI
状态管理
业务逻辑
网络请求
导航
跨平台共享代码
例如:
wasmJs {
browser()
binaries.executable()
}
这里的 binaries.executable() 表明当前目标需要生成可以运行和分发的应用产物,而不只是一个供其他模块依赖的普通代码模块。
这种模式就不能简单理解为:
Vue 调用一个 Wasm 基础库
而应该理解为:
整个 Web 应用的大部分 Kotlin 代码
被编译成 Wasm 并运行在浏览器中
JavaScript 或生成的启动代码仍然可能负责加载 Wasm、连接浏览器环境以及完成必要的互操作,但应用主体已经可以由 Kotlin 和 Compose 实现。
十一、放到 KMP 项目中应该怎么理解?
假设一个 KMP 项目采用下面的结构:
sharedLogic
sharedUI
androidApp
iosApp
webApp
其中:
sharedLogic
负责业务模型、网络、数据处理和状态逻辑
sharedUI
负责 Compose Multiplatform 共享 UI
androidApp
负责 Android 应用壳和平台能力
iosApp
负责 iOS 应用壳和平台能力
webApp
负责 Web 入口和 Web 平台配置
如果 webApp 使用:
wasmJs {
browser()
binaries.executable()
}
那么整体链路是:
sharedLogic + sharedUI + webApp
↓
Kotlin/Wasm 编译
↓
productionExecutable 部署目录
↓
浏览器运行
最终产物可以像 Vue 的 dist 一样部署到静态服务器。
这时 Web 端并不是:
先写一个 Vue 项目
再把 KMP 页面嵌进去
而是可以直接:
使用 Compose Multiplatform 编写 Web UI
使用 Kotlin 编写状态和业务逻辑
最终编译成 Kotlin/Wasm Web 应用
十二、Kotlin/JS 和 Kotlin/Wasm 应该怎么选择?
可以根据项目目标进行区分。
已经有成熟 Vue 或 React 项目
如果 Web 页面继续由前端团队使用 Vue 或 React 开发,只想复用部分 Kotlin 业务逻辑:
优先考虑 Kotlin/JS
结构可能是:
KMP sharedLogic
↓
编译成 JavaScript
↓
Vue / TypeScript 项目导入
Kotlin 官方也将 Kotlin/JS 推荐给需要与现有 JavaScript/TypeScript 代码直接互操作、使用 Web 原生 UI 的场景。
希望 Android、iOS 和 Web 共享 UI
如果希望:
Android 使用 Compose
iOS 使用 Compose Multiplatform
Web 也使用 Compose Multiplatform
那么可以考虑:
Kotlin/Wasm + Compose Multiplatform
结构是:
sharedLogic
+
sharedUI
+
各平台少量适配代码
此时,Wasm 是完整 Web 应用的重要运行基础,而不只是一个底层算法库。
十三、最终应该怎样理解 WebAssembly?
经过这一轮梳理,可以把 WebAssembly 总结为四句话。
第一句:Wasm 不是前端框架
Vue / React / Compose
是 UI 框架
WebAssembly
是二进制指令格式和编译目标
第二句:Vue 的 dist 不对应单个 .wasm
正确对应关系是:
Vue dist
≈
Kotlin/Wasm productionExecutable
而 .wasm 只是 Kotlin/Wasm 部署目录中的核心程序文件之一。
第三句:传统 Wasm 的调用流程很像 JNI
Android:
Kotlin / Java
↓
JNI
↓
.so
Web:
JavaScript / Vue
↓
JS/Wasm 互操作
↓
.wasm
严格来说:
.so ≈ .wasm
JNI ≈ JS 与 Wasm 的互操作机制
第四句:Kotlin/Wasm 不只可以做基础库
传统 Wasm 经常作为 Vue、React 项目的底层计算模块。
而在 KMP 中:
Kotlin/Wasm
+
Compose Multiplatform
还可以承载完整 Web 应用的 UI、状态和业务逻辑。
总结
Android 开发者第一次看到 WebAssembly 时,完全可以先从 JNI 入手理解。
传统 WebAssembly 模式确实很像:
上层 JavaScript
通过互操作边界
调用底层 Wasm 模块
就像:
Android 上层 Kotlin/Java
通过 JNI
调用 Native .so
这个类比能够快速建立基本认知。
但还需要再往前走一步:
WebAssembly 并不只是 Web 端的 Native 基础库。在 Kotlin/Wasm + Compose Multiplatform 中,它还可以成为完整 Web 应用的运行主体。
所以,在 KMP Web 中看到:
wasmJs {
browser()
binaries.executable()
}
可以理解为:
把 Kotlin 和 Compose 编写的 Web 应用
编译成浏览器能够运行的 Wasm 产物
而它最终生成的:
productionExecutable
就相当于 Vue 项目构建后的:
dist
到这里,Vue、Wasm、KMP Web 和 JNI 之间的关系就基本串起来了。




