KMP Web 开发实战(二):js、wasmJs、browser、executable 到底是什么意思? 前言上一篇梳理了 KMP Web 的整体实现路线Compose Multiplatform Kotlin/Wasm → KMP 同时负责业务逻辑和 Web UI Vue Kotlin/JS → Vue 负责 Web UIKMP 只提供共享业务逻辑但真正打开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 环境互操作的 WebAssemblyKMP 的 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 编译器 ↓ WebAssemblywasmJs这个名称可以拆成两部分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 Webfun 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 导出JsExportTypeScript 声明文件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 srcwebApp.js/script官方文档中outputFileName就是用来控制 Webpack 输出文件名的默认值通常与项目名称有关。它不控制什么outputFileName不会决定当前模块是不是完整应用Kotlin 编译成 JS 还是 Wasmnpm 包叫什么Kotlin Module 名称是什么页面入口在哪里Vue 中如何导入。它只控制Webpack 打包后的文件叫什么十、commonWebpackConfig为什么叫“common”因为 Webpack 不只用于正式打包。在 Kotlin/JS 浏览器开发中可能存在多类任务开发运行 生产打包 测试commonWebpackConfig表示这些相关流程可以共用的 Webpack 配置。例如browser { commonWebpackConfig { outputFileName webApp.js cssSupport { enabled.set(true) } } }这样开发、运行和打包时都可以共用相关配置。官方文档也将commonWebpackConfig定义为应用于打包、运行和测试任务的公共 Webpack 配置入口。十一、这几段配置逐个翻译现在把常见配置放在一起。配置一Kotlin/JS 浏览器 Targetjs { 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和wasmJskotlin { js { browser() binaries.executable() } wasmJs { browser() binaries.executable() } }翻译同一套 Web 代码 分别编译出 JavaScript 版本 和 WebAssembly 版本这不一定代表开发了两套页面。官方当前提供了 Web 兼容模式现代浏览器可以使用 Wasm 版本在需要时回退到 JS 版本因此一些项目结构会同时声明js和wasmJsTarget。十二、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 SetcommonMain ↓ 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 文件名最终可以整理成这张表配置含义jsKotlin 编译成 JavaScriptwasmJsKotlin 编译成 WebAssemblybrowser()在浏览器环境运行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 三者是什么关系 为什么生成了 JSVue 却找不到类和函数