KMP Web 开发实战(二):js、wasmJs、browser、executable 到底是什么意思?
前言
上一篇梳理了 KMP Web 的整体实现路线:
Compose Multiplatform + Kotlin/Wasm
→ KMP 同时负责业务逻辑和 Web UI
Vue + Kotlin/JS
→ Vue 负责 Web UI,KMP 只提供共享业务逻辑
但真正打开 build.gradle.kts 后,仍然会看到各种相似配置:
js {
browser()
}
js {
browser()
binaries.executable()
}
wasmJs {
browser()
binaries.executable()
}
js {
browser {
commonWebpackConfig {
outputFileName = "webApp.js"
}
}
binaries.executable()
}
这些配置放在一起时,很容易产生误解:
js 是不是代表给 Vue 使用?
wasmJs 是不是代表 Web 页面?
browser() 是不是创建浏览器页面?
binaries.executable() 是不是生成 .exe?
不写 executable,是不是就一定生成 JS 库?
实际上,这些配置分别属于不同层次。
学习 KMP Web Gradle 配置时,不要把整段代码一次性理解,而要拆成四个问题:
1. Kotlin 要编译成什么?
2. 编译结果在哪里运行?
3. 当前模块是应用还是内部代码模块?
4. 最终如何打包?
只要把这四个问题分开,Gradle 配置就不会乱了。
一、先理解最外层的 kotlin {}
KMP 模块的 Gradle 配置通常从下面开始:
kotlin {
}
它的意思是:
在当前 Gradle 模块中配置 Kotlin Multiplatform。
在这个代码块中,可以声明当前模块支持哪些平台:
kotlin {
androidLibrary {
}
iosArm64()
iosSimulatorArm64()
js {
}
wasmJs {
}
}
这些声明叫作 Target,也就是编译目标。
例如:
androidLibrary
→ 编译 Android 平台代码
iosArm64
→ 编译 iPhone 真机代码
iosSimulatorArm64
→ 编译 Apple 芯片上的 iOS 模拟器代码
js
→ 编译为 JavaScript
wasmJs
→ 编译为可与 JavaScript 环境互操作的 WebAssembly
KMP 的 Target 本质上是在告诉 Kotlin 编译器:
同一份 Kotlin 代码需要产生哪些平台的编译结果。
官方 Gradle DSL 将 js、wasmJs、JVM、Android 和各种 Native 平台都视为不同的 Target。
二、js {} 到底是什么意思?
先看:
kotlin {
js {
}
}
它表示:
当前模块增加一个 Kotlin/JS 编译目标,把 Kotlin 代码编译成 JavaScript。
编译链路是:
Kotlin 源码
↓
Kotlin/JS 编译器
↓
JavaScript
这里的 js 只决定一件事:
最终主要编译成 JavaScript
它并没有决定:
- JavaScript 在浏览器还是 Node.js 运行;
- 当前模块是完整 Web 应用还是内部模块;
- 页面由 Vue、React 还是 Kotlin 编写;
- 是否生成可以直接部署的网站。
Kotlin/JS 既可以构建浏览器应用,也可以运行在 Node.js 中,还可以用于向 JavaScript、TypeScript 项目提供共享业务逻辑。
因此,下面这个理解是错误的:
看到 js
→ 一定是给 Vue 使用
正确理解应该是:
看到 js
→ 只能说明 Kotlin 会被编译成 JavaScript
至于生成完整应用还是共享代码,还要继续往下看。
三、wasmJs {} 到底是什么意思?
再看:
kotlin {
wasmJs {
}
}
它表示:
当前模块增加一个 Kotlin/Wasm 编译目标,最终产生运行在 JavaScript 环境中的 WebAssembly。
编译链路是:
Kotlin 源码
↓
Kotlin/Wasm 编译器
↓
WebAssembly
wasmJs 这个名称可以拆成两部分:
wasm
→ 编译成 WebAssembly
Js
→ 运行在可以与 JavaScript 互操作的环境中
在 Web 开发中,它通常用于浏览器中的 Compose Multiplatform 应用:
Kotlin 业务逻辑
+
Compose UI
↓
Kotlin/Wasm
↓
浏览器
官方目前将 Kotlin/Wasm 作为 Compose Multiplatform 共享 Web UI 的主要编译目标;当目标是跨 Android、iOS、桌面和 Web 共享 UI 时,推荐使用 Kotlin/Wasm。
因此:
js
→ Kotlin 编译成 JavaScript
wasmJs
→ Kotlin 编译成 WebAssembly
这才是二者最根本的区别。
四、browser() 到底是什么意思?
有了编译目标以后,还要说明编译结果运行在哪里。
例如:
js {
browser()
}
它表示:
编译结果:JavaScript
运行环境:浏览器
而:
wasmJs {
browser()
}
表示:
编译结果:WebAssembly
运行环境:浏览器
所以 browser() 只回答一个问题:
当前 Web 目标准备在哪种运行环境中执行?
Kotlin/JS 通常可以选择:
js {
browser()
}
或者:
js {
nodejs()
}
二者分别表示:
browser()
→ 浏览器客户端环境
nodejs()
→ Node.js 环境
官方文档将浏览器和 Node.js 列为 Kotlin/JS 的两类主要运行环境;配置运行环境后,Kotlin Gradle 插件会继续创建相应的运行、测试和构建任务。
browser() 并不代表创建了页面
这是一个非常常见的误区。
看到:
js {
browser()
}
不能直接理解成:
已经生成了一个完整的 Web 页面。
它只能说明:
这份 JavaScript 面向浏览器环境
它可能是:
- 一个完整 Web 应用;
- 一个被其他 KMP 模块依赖的模块;
- 一个准备导出给 JavaScript 的业务模块;
- 一个暂时只声明了 JS Target 的共享模块。
究竟属于哪一种,还需要看有没有:
binaries.executable()
以及项目中的入口、导出和打包配置。
五、browser() 和 browser {} 有什么区别?
有时看到的是:
js {
browser()
}
有时看到的是:
js {
browser {
}
}
两者的核心含义相同:
当前 Kotlin/JS 目标运行在浏览器中。
区别只是第二种写法需要继续配置浏览器相关内容。
例如:
js {
browser {
commonWebpackConfig {
outputFileName = "webApp.js"
}
}
}
可以简单理解为:
browser()
→ 只声明浏览器环境
browser { ... }
→ 声明浏览器环境,并继续配置打包、运行、测试等内容
这和 Android Gradle 中的普通函数调用、带配置代码块调用很像:
someFeature()
与:
someFeature {
// 继续配置
}
它们不是两种运行模式。
六、binaries.executable() 到底是什么意思?
下面是最容易混乱的一句:
binaries.executable()
它表示:
要求 Kotlin 编译器为当前 Target 生成能够独立运行的应用产物。
例如:
js {
browser()
binaries.executable()
}
完整翻译:
js
→ 编译成 JavaScript
browser()
→ 运行在浏览器
binaries.executable()
→ 生成一个可以独立运行的 Web 应用
而:
wasmJs {
browser()
binaries.executable()
}
完整翻译:
wasmJs
→ 编译成 WebAssembly
browser()
→ 运行在浏览器
binaries.executable()
→ 生成一个可以独立运行的 Web 应用
binaries.executable() 同时适用于 js 和 wasmJs 等 Web Target,并不属于 Wasm 专用配置。
它不是 Windows 的 .exe
这里的 executable 是“可执行应用”的概念,不是说一定生成:
xxx.exe
对于 Kotlin/JS,最终可能生成:
index.html
webApp.js
图片、CSS 等资源
对于 Kotlin/Wasm,最终可能生成:
index.html
JavaScript 加载文件
WebAssembly 文件
资源文件
它们都是浏览器可以启动的 Web 应用。
一般需要有程序入口
完整 Web 应用通常需要入口:
fun main() {
// 启动应用
}
例如 Compose Multiplatform Web:
fun main() {
ComposeViewport {
App()
}
}
可以将它类比为 Android 中:
class MainActivity : ComponentActivity()
main() 是程序从哪里启动,而 binaries.executable() 是告诉构建系统:
请按照一个最终应用来构建它。
七、不写 binaries.executable() 会怎样?
例如:
js {
browser()
}
没有:
binaries.executable()
这时只能说明:
当前模块支持 Kotlin/JS 浏览器目标
官方文档说明,省略 binaries.executable() 时,编译器主要生成 Kotlin 内部使用的库产物,这些产物可以被其他 Kotlin 项目使用,但不能直接作为完整应用独立运行。
这通常适用于:
sharedLogic
sharedCore
domain
data
这些非入口模块。
例如:
webApp
↓ 依赖
sharedLogic
可以写成:
// sharedLogic
kotlin {
js {
browser()
}
}
// webApp
kotlin {
js {
browser()
binaries.executable()
}
}
其中:
sharedLogic
→ 共享模块,本身不启动
webApp
→ 入口模块,可以运行
一个很重要的补充
不要直接认为:
没有 binaries.executable()
=
已经生成了可以让 Vue import 的 JS 库
这是不准确的。
js {
browser()
}
只是声明了 Kotlin/JS 浏览器 Target。
要真正提供给 Vue、React 或 TypeScript 项目使用,通常还要继续处理:
- 哪些 Kotlin API 对 JavaScript 导出;
@JsExport;- TypeScript 声明文件;
- JavaScript 模块格式;
- npm 包结构;
- Vue 项目如何安装和导入。
这些会在第三篇专门展开。
八、commonWebpackConfig 是什么?
再来看这段:
js {
browser {
commonWebpackConfig {
outputFileName = "webApp.js"
}
}
binaries.executable()
}
这里的:
commonWebpackConfig {
}
表示:
配置 Kotlin/JS 浏览器目标使用 Webpack 时的公共打包设置。
Kotlin 源码不会简单地直接变成一个最终网页文件。
中间大概会经过:
Kotlin 源码
↓
Kotlin/JS 编译
↓
JavaScript 模块和依赖
↓
Webpack 打包
↓
最终浏览器文件
Webpack 会处理:
- Kotlin 编译生成的 JavaScript;
- 模块依赖;
- npm 依赖;
- 开发和生产打包;
- 资源输出;
- 开发服务器。
Kotlin/JS 浏览器目标使用 Webpack 进行打包,commonWebpackConfig 中的配置可以共同作用于构建、运行和测试相关任务。
九、outputFileName = "webApp.js" 是什么?
这句:
outputFileName = "webApp.js"
只表示:
将 Webpack 最终输出的主要 JavaScript 文件命名为
webApp.js。
例如:
js {
browser {
commonWebpackConfig {
outputFileName = "webApp.js"
}
}
binaries.executable()
}
最终可能生成:
build/dist/js/productionExecutable/
├── index.html
├── webApp.js
└── 其他资源
然后 index.html 中加载:
<script src="webApp.js"></script>
官方文档中,outputFileName 就是用来控制 Webpack 输出文件名的,默认值通常与项目名称有关。
它不控制什么?
outputFileName 不会决定:
- 当前模块是不是完整应用;
- Kotlin 编译成 JS 还是 Wasm;
- npm 包叫什么;
- Kotlin Module 名称是什么;
- 页面入口在哪里;
- Vue 中如何导入。
它只控制:
Webpack 打包后的文件叫什么
十、commonWebpackConfig 为什么叫“common”?
因为 Webpack 不只用于正式打包。
在 Kotlin/JS 浏览器开发中,可能存在多类任务:
开发运行
生产打包
测试
commonWebpackConfig 表示这些相关流程可以共用的 Webpack 配置。
例如:
browser {
commonWebpackConfig {
outputFileName = "webApp.js"
cssSupport {
enabled.set(true)
}
}
}
这样开发、运行和打包时,都可以共用相关配置。官方文档也将 commonWebpackConfig 定义为应用于打包、运行和测试任务的公共 Webpack 配置入口。
十一、这几段配置逐个翻译
现在把常见配置放在一起。
配置一:Kotlin/JS 浏览器 Target
js {
browser()
}
翻译:
把 Kotlin 编译成 JavaScript
并且面向浏览器环境
但没有明确要求生成可独立运行的最终应用
常见位置:
sharedLogic
sharedCore
非入口模块
配置二:完整 Kotlin/JS Web 应用
js {
browser()
binaries.executable()
}
翻译:
把 Kotlin 编译成 JavaScript
运行在浏览器中
生成可以独立运行的 Web 应用
最终效果类似:
Kotlin 源码
↓
JavaScript
↓
Web 发布目录
↓
部署到服务器
官方 Kotlin/JS 插件会为这种浏览器应用提供开发运行和生产 Webpack 打包任务。
配置三:带 Webpack 文件名配置的 Kotlin/JS 应用
js {
browser {
commonWebpackConfig {
outputFileName = "webApp.js"
}
}
binaries.executable()
}
翻译:
把 Kotlin 编译成 JavaScript
运行在浏览器
生成完整 Web 应用
使用 Webpack 打包
主输出文件命名为 webApp.js
它和上一种仍然是同一条技术路线。
区别只是多了 Webpack 定制。
配置四:完整 Kotlin/Wasm Web 应用
wasmJs {
browser()
binaries.executable()
}
翻译:
把 Kotlin 编译成 WebAssembly
运行在浏览器中
生成可以独立部署的 Web 应用
这通常用于:
Compose Multiplatform Web
最终构建目录中一般包含:
index.html
JavaScript 启动文件
.wasm 文件
资源文件
官方 Kotlin/Wasm 教程通过 wasmJsBrowserDistribution 生成可以发布的网站产物,默认示例产物位于模块的 build/dist/wasmJs/productionExecutable 下。
配置五:同时声明 js 和 wasmJs
kotlin {
js {
browser()
binaries.executable()
}
wasmJs {
browser()
binaries.executable()
}
}
翻译:
同一套 Web 代码
分别编译出 JavaScript 版本
和 WebAssembly 版本
这不一定代表开发了两套页面。
官方当前提供了 Web 兼容模式:现代浏览器可以使用 Wasm 版本,在需要时回退到 JS 版本,因此一些项目结构会同时声明 js 和 wasmJs Target。
十二、@OptIn(ExperimentalWasmDsl::class) 又是什么?
有些示例会写:
@OptIn(ExperimentalWasmDsl::class)
wasmJs {
browser()
binaries.executable()
}
这个注解的意思是:
当前代码主动选择使用被标记为实验性的 Wasm Gradle DSL。
它不会改变:
- Kotlin 最终编译成什么;
- 是否运行在浏览器;
- 是否生成完整应用;
- 页面如何编写。
因此:
@OptIn(ExperimentalWasmDsl::class)
wasmJs {
browser()
binaries.executable()
}
和:
wasmJs {
browser()
binaries.executable()
}
从“这是什么 Web Target”的角度看,表达的都是:
Kotlin/Wasm 浏览器应用
是否需要 @OptIn,应以当前 Kotlin 插件版本、IDE 提示和具体 DSL 状态为准。官方当前部分项目结构示例中仍然可以看到该注解。
十三、Target 和 Source Set 是什么关系?
声明:
js {
browser()
}
之后,项目通常会出现或使用:
src/jsMain
src/jsTest
声明:
wasmJs {
browser()
}
之后,通常会对应:
src/wasmJsMain
src/wasmJsTest
编译 Kotlin/JS 时,大致组合:
commonMain
+
jsMain
编译 Kotlin/Wasm 时,大致组合:
commonMain
+
wasmJsMain
例如:
src/
├── commonMain/
│ └── 所有平台共享代码
│
├── jsMain/
│ └── Kotlin/JS 专属代码
│
└── wasmJsMain/
└── Kotlin/Wasm 专属代码
如果项目同时支持 js 和 wasmJs,还可以人为创建共享的 Web Source Set:
commonMain
↓
webMain
├── jsMain
└── wasmJsMain
这样浏览器两种 Target 共用的代码可以放在:
webMain
而不是在 jsMain 和 wasmJsMain 中重复编写。
十四、用 Android 模块类比
可以把 Web Gradle 配置理解成四个层次。
第一层:选择编译平台
androidLibrary
→ Android
iosArm64
→ iOS 真机
js
→ JavaScript
wasmJs
→ WebAssembly
第二层:选择运行环境
browser()
→ 浏览器
nodejs()
→ Node.js
第三层:选择模块定位
binaries.executable()
→ 最终应用,可以独立运行
没有 executable
→ 更偏内部模块或被依赖模块
第四层:配置打包
commonWebpackConfig
→ 配置 Webpack
outputFileName
→ 设置最终 JS 文件名
合起来就是:
js
→ 编译成什么
browser()
→ 在哪里运行
binaries.executable()
→ 是不是最终应用
commonWebpackConfig
→ 最终如何打包
十五、几个最容易踩的理解误区
误区一:js 就是给 Vue 使用
错误。
js {
browser()
binaries.executable()
}
可以是一个完整 Kotlin/JS Web 应用,和 Vue 没有关系。
误区二:wasmJs 就代表页面
错误。
wasmJs 只表示:
Kotlin 编译成 WebAssembly
页面是不是 Compose,还取决于是否引入 Compose Multiplatform,以及项目代码如何编写。
误区三:browser() 就代表完整网站
错误。
browser() 只表示运行环境是浏览器。
是否生成最终应用,还要看:
binaries.executable()
误区四:没有 executable 就能直接让 Vue 导入
错误。
没有 executable 只能说明它不是一个独立运行的最终应用。
想让 Vue 方便地导入,还需要处理 JS 导出、TypeScript 声明和 npm 包等问题。
误区五:outputFileName 决定模块名称
错误。
outputFileName = "webApp.js"
只控制 Webpack 最终输出文件的名称。
它不等于:
npm 包名
Kotlin Target 名称
Gradle Module 名称
JavaScript Module 名称
十六、最终总结
看到 KMP Web Gradle 配置时,按照下面顺序阅读。
第一步:看编译目标
js
→ 编译成 JavaScript
wasmJs
→ 编译成 WebAssembly
第二步:看运行环境
browser()
→ 浏览器
nodejs()
→ Node.js
第三步:看模块是不是最终应用
binaries.executable()
→ 生成可以独立运行的应用
没有 executable
→ 通常是内部模块或被依赖模块
第四步:看打包配置
commonWebpackConfig
→ Webpack 公共配置
outputFileName
→ 最终 JS 文件名
最终可以整理成这张表:
| 配置 | 含义 |
|---|---|
js |
Kotlin 编译成 JavaScript |
wasmJs |
Kotlin 编译成 WebAssembly |
browser() |
在浏览器环境运行 |
nodejs() |
在 Node.js 环境运行 |
binaries.executable() |
生成可独立运行的应用 |
commonWebpackConfig |
配置 Webpack 打包 |
outputFileName |
设置 Webpack 输出文件名 |
@OptIn(...) |
主动启用实验性 DSL |
把几段常见配置再次翻译:
js {
browser()
}
Kotlin → JavaScript
运行在浏览器
不是明确的最终应用
js {
browser()
binaries.executable()
}
Kotlin → JavaScript
运行在浏览器
生成完整 Web 应用
wasmJs {
browser()
binaries.executable()
}
Kotlin → WebAssembly
运行在浏览器
生成完整 Web 应用
js {
browser {
commonWebpackConfig {
outputFileName = "webApp.js"
}
}
binaries.executable()
}
Kotlin → JavaScript
运行在浏览器
生成完整 Web 应用
Webpack 输出文件叫 webApp.js
理解了这些配置以后,就不会再把:
编译目标
运行环境
应用类型
打包方式
混成同一个概念。
下一篇预告
下一篇进入真正的 Vue 集成实战:
《KMP sharedLogic 如何编译成 JS 库,供 Vue 项目调用?》
将重点讲清:
sharedLogic 如何增加 js Target?
commonMain 中哪些代码可以编译到 JS?
@JsExport 到底有什么作用?
如何生成 TypeScript 的 .d.ts 文件?
Kotlin 的类和函数如何提供给 Vue 调用?
Vue 项目如何安装本地 KMP JS 包?
Gradle、npm 和 Vite 三者是什么关系?
为什么生成了 JS,Vue 却找不到类和函数?更多推荐

所有评论(0)