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 将 jswasmJs、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() 同时适用于 jswasmJs 等 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 下。


配置五:同时声明 jswasmJs

复制代码
kotlin {
    js {
        browser()
        binaries.executable()
    }

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

翻译:

复制代码
同一套 Web 代码
分别编译出 JavaScript 版本
和 WebAssembly 版本

这不一定代表开发了两套页面。

官方当前提供了 Web 兼容模式:现代浏览器可以使用 Wasm 版本,在需要时回退到 JS 版本,因此一些项目结构会同时声明 jswasmJs 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 专属代码

如果项目同时支持 jswasmJs,还可以人为创建共享的 Web Source Set:

复制代码
commonMain
    ↓
webMain
├── jsMain
└── wasmJsMain

这样浏览器两种 Target 共用的代码可以放在:

复制代码
webMain

而不是在 jsMainwasmJsMain 中重复编写。


十四、用 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 却找不到类和函数?
相关推荐
万少11 小时前
用腻了 WorkBuddy 的默认界面?这个开源小工具给它换上了毛玻璃
前端·javascript·后端
To_OC12 小时前
LC 15 三数之和:双指针不难,难的是把去重做对
javascript·算法·leetcode
触底反弹13 小时前
🔥 前端也能玩转 AI 流式输出!从二进制流到打字机效果,一篇讲透
javascript·人工智能·node.js
胡萝卜术13 小时前
深度重构:Agent Skills——从 Prompt 工程到能力工程
javascript·架构·github
benchmark_cc14 小时前
如何用 Python + QuantDash 快速构建高胜率“配对交易(Pairs Trading)”策略?
开发语言·人工智能·python·pandas·量化交易·quantdash
海上彼尚15 小时前
Nodejs也能写Agent - 16.LangGraph篇 - 条件分支与循环
前端·后端·langchain·node.js
程序员无隅15 小时前
Coding Agent 为什么压缩上下文后还能继续工作?上下文模块设计拆解
java·开发语言·数据库
Python+9915 小时前
Java 枚举类(Enum)详解:从基础到高级应用
java·开发语言·python
二炮手亮子15 小时前
浅记java线程池
java·开发语言
To_OC15 小时前
从 “卡死半天” 到 “打字机效果”:大模型流式输出前端实现全记录
前端·javascript·llm