6.2.3.5 与 JavaScript 互操作

原文链接: https://kotlinlang.org/docs/wasm-js-interop.html

6.2.3.5 与 JavaScript 互操作

Beta

Kotlin/Wasm 允许你在 Kotlin 中使用 JavaScript 代码,也允许在 JavaScript 中使用 Kotlin 代码。

与 Kotlin/JS 一样,Kotlin/Wasm 编译器也具备与 JavaScript 的互操作能力。如果你熟悉 Kotlin/JS 互操作,会发现 Kotlin/Wasm 互操作与之类似。不过,有一些关键差异需要注意。

注意: Kotlin/Wasm 处于 Beta 阶段。它可能随时发生变化。请在非生产场景中使用。我们非常欢迎你在 YouTrack 上提供反馈。

在 Kotlin 中使用 JavaScript 代码

了解如何通过 external 声明、带 JavaScript 代码片段的函数以及 @JsModule 注解在 Kotlin 中使用 JavaScript 代码。

外部声明

外部 JavaScript 代码默认在 Kotlin 中不可见。要在 Kotlin 中使用 JavaScript 代码,你可以用 external 声明来描述它的 API。

JavaScript 函数

考虑这个 JavaScript 函数:

1
2
3
function greet (name) {
    console.log("Hello, " + name + "!");
}

你可以在 Kotlin 中把它声明为 external 函数:

1
external fun greet(name: String)

外部函数没有函数体,你可以像普通 Kotlin 函数一样调用它:

1
2
3
fun main() {
    greet("Alice")
}

JavaScript 属性

考虑这个全局 JavaScript 变量:

1
let globalCounter = 0;

你可以在 Kotlin 中使用外部的 var 或 val 属性声明它:

1
external var globalCounter: Int

这些属性是在外部初始化的。这些属性在 Kotlin 代码中不能带有 = value 初始化器。

JavaScript 类

考虑这个 JavaScript 类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Rectangle {
    constructor (height, width) {
        this.height = height;
        this.width = width;
    }

    area () {
        return this.height * this.width;
    }
}

你可以在 Kotlin 中把它用作外部类:

1
2
3
4
5
external class Rectangle(height: Double, width: Double) : JsAny {
    val height: Double
    val width: Double
    fun area(): Double
}

external 类中的所有声明都被隐式视为外部声明。

外部接口

你可以在 Kotlin 中描述 JavaScript 对象的形状。考虑这个 JavaScript 函数及其返回值:

1
2
3
function createUser (name, age) {
    return { name: name, age: age };
}

看看如何用 Kotlin 的 external interface User 类型描述它的形状:

1
2
3
4
5
6
external interface User : JsAny {
    val name: String
    val age: Int
}

external fun createUser(name: String, age: Int): User

外部接口没有运行时类型信息,只是编译期的概念。因此,与普通接口相比,外部接口有一些限制:

  • 不能在 is 检查的右侧使用它们。
  • 不能在类字面量表达式(例如 User::class)中使用它们。
  • 不能把它们作为 reified 类型实参传入。
  • 用 as 转换为外部接口总是成功。

外部对象

考虑这些持有对象的 JavaScript 变量:

1
2
3
4
5
6
7
let Counter = {
    value: 0,
    step: 1,
    increment () {
        this.value += this.step;
    }
};

你可以在 Kotlin 中把它们用作外部对象:

1
2
3
4
5
external object Counter : JsAny {
    fun increment()
    val value: Int
    var step: Int
}

外部类型层次结构

与普通类和接口类似,你可以声明外部声明来扩展其他外部类并实现外部接口。但是,你不能在同一个类型层次结构中混用外部声明和非外部声明。

使用 @nativeInvoke 调用 JavaScript 对象

实验性

你可以在 external 声明(类或接口)的 Kotlin 成员函数上使用 @nativeInvoke 注解,使其可以像 JavaScript 函数那样被调用。

使用该注解后,Kotlin 中对这个函数的每次调用都会转换为对 JavaScript 对象的直接调用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import kotlin.js.nativeInvoke

@OptIn(ExperimentalWasmJsInterop::class)
external class JsAction {
    @nativeInvoke
    operator fun invoke(data: String)
}

fun main() {
    val action = JsAction()
    action("Run task")
}

@nativeInvoke 注解是一个临时方案,直到我们设计出稳定的互操作机制。目前,当你使用 @nativeInvoke 时,编译器会给出警告。{style=“note”}

在 Kotlin 函数中嵌入 JavaScript 代码

你可以通过在 Kotlin/Wasm 代码中定义函数体为 = js("code") 的函数来添加 JavaScript 代码片段:

1
2
fun getCurrentURL(): String =
    js("window.location.href")

如果你想运行一段 JavaScript 语句块,请把代码包在字符串中的花括号 {} 里:

1
2
3
4
5
fun setLocalSettings(value: String): Unit = js(
    """{
        localStorage.setItem('settings', value);
}"""
)

如果你想返回一个对象,请把花括号 {} 用圆括号 () 包起来:

1
2
fun createJsUser(name: String, age: Int): JsAny =
    js("({ name: name, age: age })")

Kotlin/Wasm 对 js() 函数的调用有特殊处理,其实现有一些限制:

  • js() 函数调用必须提供字符串字面量实参。
  • js() 函数调用必须是该函数体中的唯一表达式。
  • js() 函数只允许从包级函数中调用。
  • 函数的返回类型必须显式给出。
  • 类型受到限制,与 external fun 类似。

Kotlin 编译器会把代码字符串放入生成的 JavaScript 文件中的一个函数里,并把它导入为 WebAssembly 格式。Kotlin 编译器不会校验这些 JavaScript 片段。如果存在 JavaScript 语法错误,会在你运行 JavaScript 代码时报告。

注意: @JsFun 注解具有类似的功能,很可能也会被弃用。

JavaScript 模块

默认情况下,外部声明对应 JavaScript 全局作用域。如果你用 @JsModule 注解标记一个 Kotlin 文件,那么其中的所有外部声明都会从指定的模块中导入。

考虑这个 JavaScript 代码示例:

1
2
3
4
5
6
7
8
// users.mjs
export let maxUsers = 10;

export class User {
    constructor (username) {
        this.username = username;
    }
}

使用 @JsModule 注解在 Kotlin 中使用这段 JavaScript 代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Kotlin
@file:JsModule("./users.mjs")

external val maxUsers: Int

external class User : JsAny {
    constructor(username: String)

    val username: String
}

数组互操作

你可以把 JavaScript 的 JsArray<T> 复制到 Kotlin 原生的 Array 或 List 类型中;同样,你也可以把这些 Kotlin 类型复制到 JsArray<T>。

要把 JsArray<T> 转换为 Array<T> 或反向转换,请使用可用的适配函数之一。

下面是一个泛型类型之间转换的示例:

1
2
3
4
5
6
7
8
9
val list: List<JsString> =
    listOf("Kotlin", "Wasm").map { it.toJsString() }

// 使用 .toJsArray() 把 List 或 Array 转换为 JsArray
val jsArray: JsArray<JsString> = list.toJsArray()

// 使用 .toArray() 和 .toList() 把它转换回 Kotlin 类型
val kotlinArray: Array<JsString> = jsArray.toArray()
val kotlinList: List<JsString> = jsArray.toList()

类似的适配函数也可用于把类型化数组转换为它们的 Kotlin 对应类型(例如 IntArray 和 Int32Array)。详细信息和实现请参阅 kotlinx-browser 仓库。

下面是一个类型化数组之间转换的示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import org.khronos.webgl.*

    // ...

    val intArray: IntArray = intArrayOf(1, 2, 3)

    // 使用 .toInt32Array() 把 Kotlin IntArray 转换为 JavaScript Int32Array
    val jsInt32Array: Int32Array = intArray.toInt32Array()

    // 使用 toIntArray() 把 JavaScript Int32Array 转换回 Kotlin IntArray
    val kotlinIntArray: IntArray = jsInt32Array.toIntArray()

在 JavaScript 中使用 Kotlin 代码

了解如何通过 @JsExport 注解在 JavaScript 中使用你的 Kotlin 代码。

带 @JsExport 注解的函数

要让 Kotlin/Wasm 函数可供 JavaScript 代码使用,请使用 @JsExport 注解:

1
2
3
4
// Kotlin/Wasm

@JsExport
fun addOne(x: Int): Int = x + 1

带 @JsExport 注解的 Kotlin/Wasm 函数在生成的 .mjs 模块的 default 导出上表现为属性。然后你就可以在 JavaScript 中使用这个函数:

1
2
3
4
5
// JavaScript

import exports from "./module.mjs"

exports.addOne(10)

Kotlin/Wasm 编译器能够根据 Kotlin 代码中的任何 @JsExport 声明生成 TypeScript 定义。这些定义可供 IDE 和 JavaScript 工具用于提供代码自动补全、辅助类型检查,并让从 JavaScript 和 TypeScript 使用 Kotlin 代码更容易。

Kotlin/Wasm 编译器会收集所有带 @JsExport 注解的顶层函数,并自动在 .d.ts 文件中生成 TypeScript 定义。

要生成 TypeScript 定义,请在你的 build.gradle.kts 文件的 wasmJs{} 块中添加 generateTypeScriptDefinitions() 函数:

1
2
3
4
5
6
7
8
kotlin {
    wasmJs {
        binaries.executable()
        browser {
        }
        generateTypeScriptDefinitions()
    }
}

警告: 在 Kotlin/Wasm 中生成 TypeScript 声明文件是实验性的。它可能随时被移除或更改。

类型对应关系

Kotlin/Wasm 在 JavaScript 互操作声明的签名中只允许特定类型。这些限制统一适用于带 external、= js("code") 或 @JsExport 的声明。

看看 Kotlin 类型如何对应 JavaScript 类型:

| Kotlin | JavaScript |

| Byte、Short、Int、Char、UByte、UShort、UInt、 | Number | | Float、Double、 | Number | | Long、ULong、 | BigInt | | Boolean、 | Boolean | | String、 | String | | 返回位置的 Unit | undefined | | 函数类型,例如 (String) -> Int | 函数 | | JsAny 及其子类型 | 任意 JavaScript 值 | | JsReference | 指向 Kotlin 对象的不透明引用 | | 其他类型 | 不支持 |

你也可以使用这些类型的可空版本。

JsAny 类型

JavaScript 值在 Kotlin 中使用 JsAny 类型及其子类型表示。

Kotlin/Wasm 标准库为其中一些类型提供了表示:

  • 包 kotlin.js:
  • JsAny
  • JsBoolean、JsNumber、JsString
  • JsArray
  • Promise

你也可以通过声明 external 接口或类来创建自定义的 JsAny 子类型。

JsReference 类型

Kotlin 值可以通过 JsReference 类型以不透明引用的形式传给 JavaScript。

例如,如果你想把这个 Kotlin 类 User 暴露给 JavaScript:

1
class User(var name: String)

你可以使用 toJsReference() 函数创建 JsReference<User> 并把它返回给 JavaScript:

1
2
3
4
@JsExport
fun createUser(name: String): JsReference<User> {
    return User(name).toJsReference()
}

这些引用在 JavaScript 中不能直接访问,其行为类似空的冻结 JavaScript 对象。要操作这些对象,你需要通过 get() 方法向 JavaScript 导出更多函数,并在其中解包引用值:

1
2
3
4
@JsExport
fun setUserName(user: JsReference<User>, name: String) {
    user.get().name = name
}

你可以创建一个类并从 JavaScript 中修改它的名称:

1
2
3
4
import UserLib from "./userlib.mjs"

let user = UserLib.createUser("Bob");
UserLib.setUserName(user, "Alice");

类型参数

如果 JavaScript 互操作声明的类型参数上界是 JsAny 或其子类型,那么它们可以有类型参数。例如:

1
external fun <T : JsAny> processData(data: JsArray<T>): T

异常处理

你可以使用 Kotlin 的 try-catch 表达式在 Kotlin/Wasm 代码中捕获 JavaScript 异常。异常处理的工作方式如下:

  • 从 JavaScript 抛出的异常:在 Kotlin 一侧可以获得详细信息。如果这类异常又传播回 JavaScript,它就不会再被包装为 WebAssembly 异常。

  • 从 Kotlin 抛出的异常:它们可以在 JavaScript 一侧作为普通 JS 错误被捕获。

下面是一个演示在 Kotlin 一侧捕获 JavaScript 异常的示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
external object JSON {
    fun <T: JsAny> parse(json: String): T
}

fun main() {
    try {
        JSON.parse("an invalid JSON")
    } catch (e: JsException) {
        println("Thrown value is: ${e.thrownValue}")
        // SyntaxError: Unexpected token 'a', "an invalid JSON" is not valid JSON

        println("Message: ${e.message}")
        // Message: Unexpected token 'a', "an invalid JSON" is not valid JSON

        println("Stacktrace:")
        // Stacktrace:

        // 打印完整的 JavaScript 堆栈跟踪
        e.printStackTrace()
    }
}

这种异常处理在支持 WebAssembly.JSTag 特性的现代浏览器中会自动生效:

  • Chrome 115+
  • Firefox 129+
  • Safari 18.4+

Kotlin/Wasm 与 Kotlin/JS 互操作的差异

尽管 Kotlin/Wasm 互操作与 Kotlin/JS 互操作有相似之处,但有一些关键差异需要注意:

| | Kotlin/Wasm | Kotlin/JS |

| 外部枚举 | 不支持外部枚举类。 | 支持外部枚举类。 | | 类型扩展 | 不支持用非外部类型扩展外部类型。 | 支持非外部类型。 | | JsName 注解 | 只在注解外部声明时生效。 | 可用于改变普通非外部声明的名称。 | | js() 函数 | 允许把 js("code") 函数调用作为包级函数的单一表达式体。 | 可以在任何上下文中调用 js("code") 函数,它返回 dynamic 值。 | | 模块系统 | 只支持 ES 模块。没有 @JsNonModule 注解的对应物。把导出作为 default 对象上的属性提供。只允许导出包级函数。 | 支持 ES 模块和旧式模块系统。提供具名的 ESM 导出。允许导出类和对象。 | | 类型 | 对所有互操作声明 external、= js("code") 和 @JsExport 统一应用更严格的类型限制。只允许有限数量的内置 Kotlin 类型和 JsAny 子类型。 | 允许在 external 声明中使用所有类型。限制可在 @JsExport 中使用的类型。 | | Long | 类型对应 JavaScript 的 BigInt。 | 在 JavaScript 中表现为自定义类。 | | 数组 | 目前互操作中不直接支持。你可以改用新的 JsArray 类型。 | 实现为 JavaScript 数组。 | | 其他类型 | 需要使用 JsReference<> 把 Kotlin 对象传给 JavaScript。 | 允许在外部声明中使用非外部 Kotlin 类类型。 | | 异常处理 | 你可以用 JsException 和 Throwable 类型捕获任何 JavaScript 异常。 | 可以用 Throwable 类型捕获 JavaScript Error。可以用 dynamic 类型捕获任何 JavaScript 异常。 | | 动态类型 | 不支持 dynamic 类型。请改用 JsAny(参见下方示例代码)。 | 支持 dynamic 类型。 |

注意: Kotlin/JS 中用于与无类型或松散类型对象互操作的 dynamic 类型在 Kotlin/Wasm 中不受支持。你可以用 JsAny 类型代替 dynamic 类型:kotlin // Kotlin/JS fun processUser(user: dynamic, age: Int) { // ... user.profile.updateAge(age) // ... } // Kotlin/Wasm private fun updateUserAge(user: JsAny, age: Int): Unit = js("{ user.profile.updateAge(age); }") fun processUser(user: JsAny, age: Int) { // ... updateUserAge(user, age) // ... }

kotlinx-browser 库是一个独立的库,提供 JavaScript 浏览器 API,包括:

  • 包 org.khronos.webgl:
  • 类型化数组,例如 Int8Array。
  • WebGL 类型。
  • 包 org.w3c.dom.*:
  • DOM API 类型。
  • 包 kotlinx.browser:
  • DOM API 全局对象,例如 window 和 document。

要使用 kotlinx-browser 库中的声明,请在项目的构建配置文件中把它添加为依赖:

1
2
3
4
5
val wasmJsMain by getting {
    dependencies {
        implementation("org.jetbrains.kotlinx:kotlinx-browser:0.3")
    }
}