前言

上一篇梳理了 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() 同时适用于 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 却找不到类和函数?
Logo

一站式 AI 云服务平台

更多推荐