跳至主要内容

與 JavaScript 的互通性

Kotlin/Wasm 允許您在 Kotlin 中使用 JavaScript 代碼,並在 JavaScript 中使用 Kotlin 代碼。

Kotlin/JS 類似,Kotlin/Wasm 編譯器也具有與 JavaScript 的互通性(Interoperability)。如果您熟悉 Kotlin/JS 的互通性,您會注意到 Kotlin/Wasm 的互通性與之相似。但是,有一些關鍵差異需要考慮。

備註

Kotlin/Wasm 處於 Alpha 階段。它可能隨時更改。請在正式環境前使用它。我們感謝您在 YouTrack 中的回饋。

在 Kotlin 中使用 JavaScript 代碼

了解如何透過使用 external 宣告、帶有 JavaScript 代碼片段的函數和 @JsModule 註解,在 Kotlin 中使用 JavaScript 代碼。

External 宣告

預設情況下,外部 JavaScript 代碼在 Kotlin 中是不可見的。 要在 Kotlin 中使用 JavaScript 代碼,您可以使用 external 宣告來描述其 API。

JavaScript 函數

考慮以下 JavaScript 函數:

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

您可以在 Kotlin 中將其宣告為 external 函數:

external fun greet(name: String)

External 函數沒有主體,您可以像調用常規 Kotlin 函數一樣調用它:

fun main() {
greet("Alice")
}

JavaScript 屬性

考慮以下全域 JavaScript 變數:

let globalCounter = 0;

您可以使用 external varval 屬性在 Kotlin 中宣告它:

external var globalCounter: Int

這些屬性是在外部初始化的。這些屬性在 Kotlin 代碼中不能有 = value 初始化器(initializer)。

JavaScript 類別(Class)

考慮以下 JavaScript 類別:

class Rectangle {
constructor (height, width) {
this.height = height;
this.width = width;
}

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

您可以在 Kotlin 中將其用作 external 類別:

external class Rectangle(height: Double, width: Double) : JsAny {
val height: Double
val width: Double
fun area(): Double
}

external 類別中的所有宣告都隱式地被視為 external。

External 介面(Interface)

您可以在 Kotlin 中描述 JavaScript 物件的形狀。考慮以下 JavaScript 函數及其返回值:

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

查看如何在 Kotlin 中使用 external interface User 類型描述其形狀:

external interface User : JsAny {
val name: String
val age: Int
}

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

External 介面沒有運行時類型訊息,並且僅是編譯時概念。 因此,與常規介面相比,external 介面有一些限制:

  • 您不能在 is 檢查的右側使用它們。
  • 您不能在類別字面量表達式(Class literal expression)(例如 User::class)中使用它們。
  • 您不能將它們作為具體化的類型參數(Reified type argument)傳遞。
  • 使用 as 轉換為 external 介面始終成功。

External 物件

考慮以下 JavaScript 變數,它們持有一個物件:

let Counter = {
value: 0,
step: 1,
increment () {
this.value += this.step;
}
};

您可以在 Kotlin 中將它們用作 external 物件:

external object Counter : JsAny {
fun increment()
val value: Int
var step: Int
}

External 類型層級(Type Hierarchy)

與常規類別和介面類似,您可以宣告 external 宣告來擴展其他 external 類別並實現 external 介面。 但是,您不能在同一類型層級中混合 external 和非 external 宣告。

具有 JavaScript 代碼的 Kotlin 函數

您可以透過定義一個帶有 = js("code") 主體的函數,將 JavaScript 片段新增到 Kotlin/Wasm 代碼中:

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

如果要運行一個 JavaScript 語句塊,請將您的代碼用花括號 {} 括起來:

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

如果要返回一個物件,請用圓括號 () 括住花括號 {}

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

Kotlin/Wasm 以特殊的方式處理對 js() 函數的調用,並且該實現有一些限制:

  • js() 函數調用必須提供一個字串字面量參數。
  • js() 函數調用必須是函數主體中唯一的表達式。
  • js() 函數僅允許從套件級別(Package-level)函數調用。
  • 類型受到限制,類似於 external fun

Kotlin 編譯器將代碼字串放入生成的 JavaScript 檔案中的一個函數中,並將其匯入 WebAssembly 格式。 Kotlin 編譯器不驗證這些 JavaScript 片段。 如果存在 JavaScript 語法錯誤,則在運行 JavaScript 代碼時會報告這些錯誤。

備註

@JsFun 註解具有類似的功能,並且可能會被棄用。

JavaScript 模組(Module)

預設情況下,external 宣告對應於 JavaScript 全域作用域。如果使用 @JsModule 註解註解 Kotlin 檔案,則其中的所有 external 宣告都將從指定的模組匯入。

考慮以下 JavaScript 代碼範例:

// users.mjs
export let maxUsers = 10;

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

使用 @JsModule 註解在 Kotlin 中使用此 JavaScript 代碼:

// Kotlin
@file:JsModule("./users.mjs")

external val maxUsers: Int

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

val username: String
}

陣列(Array)互通性

您可以將 JavaScript 的 JsArray<T> 複製到 Kotlin 的原生 ArrayList 類型中;同樣, 您可以將這些 Kotlin 類型複製到 JsArray<T>

要將 JsArray<T> 轉換為 Array<T> 或反之,請使用可用的 adapter 函數之一。

以下是一個在泛型類型之間轉換的範例:

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()

類似的 adapter 函數可用於將類型化的陣列(Typed array)轉換為它們的 Kotlin 等效項 (例如,IntArrayInt32Array)。有關詳細訊息和實現, 請參閱 kotlinx-browser 儲存庫

以下是一個在類型化的陣列之間轉換的範例:

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 kotlnIntArray: IntArray = jsInt32Array.toIntArray()

在 JavaScript 中使用 Kotlin 代碼

了解如何透過使用 @JsExport 註解在 JavaScript 中使用您的 Kotlin 代碼。

帶有 @JsExport 註解的函數

要使 Kotlin/Wasm 函數對 JavaScript 代碼可用,請使用 @JsExport 註解:

// Kotlin/Wasm

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

標記有 @JsExport 註解的 Kotlin/Wasm 函數在生成的 .mjs 模組的 default 匯出(Export)上可見為屬性。 然後,您可以在 JavaScript 中使用此函數:

// 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() 函數:

kotlin {
wasmJs {
binaries.executable()
browser {
}
generateTypeScriptDefinitions()
}
}
備註

在 Kotlin/Wasm 中生成 TypeScript 宣告檔案是 實驗性的(Experimental)。 它可能隨時被刪除或更改。

類型對應(Type Correspondence)

Kotlin/Wasm 僅允許在 JavaScript 互通性(Interop)宣告的簽章中使用某些類型。 這些限制統一應用於帶有 external= js("code")@JsExport 的宣告。

查看 Kotlin 類型如何對應於 Javascript 類型:

KotlinJavaScript
Byte, Short, Int, Char, UByte, UShort, UInt,Number
Float, Double,Number
Long, ULong,BigInt
Boolean,Boolean
String,String
處於返回位置的 Unitundefined
函數類型,例如 (String) -> IntFunction
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:

class User(var name: String)

您可以使用 toJsReference() 函數建立 JsReference<User> 並將其返回給 JavaScript:

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

這些引用在 JavaScript 中不可直接使用,並且表現得像空的凍結 JavaScript 物件。 要對這些物件進行操作,您需要使用 get() 方法向 JavaScript 匯出更多函數,在其中解包引用值:

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

您可以建立一個類別並從 JavaScript 更改其名稱:

import UserLib from "./userlib.mjs"

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

類型參數(Type Parameter)

如果 JavaScript 互通性宣告具有 JsAny 或其子類型的上限,則它們可以具有類型參數。例如:

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

異常處理(Exception Handling)

您可以使用 Kotlin try-catch 表達式來捕獲 JavaScript 異常。 但是,預設情況下無法在 Kotlin/Wasm 中訪問有關拋出值的特定詳細訊息。

您可以配置 JsException 類型以包含來自 JavaScript 的原始錯誤訊息和堆疊追蹤(Stack trace)。 為此,請將以下編譯器選項新增到您的 build.gradle.kts 檔案:

kotlin {
wasmJs {
compilerOptions {
freeCompilerArgs.add("-Xwasm-attach-js-exception")
}
}
}

此行為取決於 WebAssembly.JSTag API,該 API 僅在某些瀏覽器中可用:

  • Chrome: 從版本 115 開始支援
  • Firefox: 從版本 129 開始支援
  • Safari: 尚未支援

以下範例示範了此行為:

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()
}
}

啟用 -Xwasm-attach-js-exception 編譯器選項後,JsException 類型會提供來自 JavaScript 錯誤的特定詳細訊息。 如果不啟用此編譯器選項,JsException 僅包含一條通用訊息,指出在運行 JavaScript 代碼時拋出了異常。

如果您嘗試使用 JavaScript try-catch 表達式來捕獲 Kotlin/Wasm 異常,它看起來像一個 通用的 WebAssembly.Exception,沒有可直接訪問的訊息和數據。

Kotlin/Wasm 和 Kotlin/JS 互通性的差異

雖然 Kotlin/Wasm 互通性與 Kotlin/JS 互通性有相似之處,但有一些關鍵差異需要考慮:

Kotlin/WasmKotlin/JS
External 列舉(Enum)不支援 external 列舉類別。支援 external 列舉類別。
類型擴展(Type Extension)不支援非 external 類型擴展 external 類型。支援非 external 類型。
JsName 註解僅在註解 external 宣告時有效。可用於更改常規非 external 宣告的名稱。
js() 函數允許 js("code") 函數調用作為套件級別函數的單個表達式主體。js("code") 函數可以在任何上下文中調用,並返回一個 dynamic 值。
模組系統(Module System)僅支援 ES 模組。沒有 @JsNonModule 註解的類似物。將其匯出作為 default 物件上的屬性提供。僅允許匯出套件級別函數。支援 ES 模組和傳統模組系統。提供具名的 ESM 匯出。允許匯出類別和物件。
類型將更嚴格的類型限制統一應用於所有互通性宣告 external= js("code")@JsExport。允許選擇數量的 內置 Kotlin 類型和 JsAny 子類型允許 external 宣告中的所有類型。限制 可在 @JsExport 中使用的類型
Long類型對應於 JavaScript BigInt在 JavaScript 中可見為自定義類別。
陣列尚未直接在互通性中支援。您可以改為使用新的 JsArray 類型。實現為 JavaScript 陣列。
其他類型需要 JsReference<> 才能將 Kotlin 物件傳遞給 JavaScript。允許在 external 宣告中使用非 external Kotlin 類別類型。
異常處理您可以使用 JsExceptionThrowable 類型捕獲任何 JavaScript 異常。可以使用 Throwable 類型捕獲 JavaScript Error。它可以使用 dynamic 類型捕獲任何 JavaScript 異常。
Dynamic 類型不支援 dynamic 類型。請改用 JsAny (請參閱下面的代碼範例)。支援 dynamic 類型。

Kotlin/JS dynamic 類型 用於與無類型或鬆散類型物件的互通性,在 Kotlin/Wasm 中不受支援。您可以改為使用 JsAny 類型,而不是 dynamic 類型:

// 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)
// ...
}

Web 相關的瀏覽器 API

kotlinx-browser 函式庫 是一個獨立的 函式庫,提供 JavaScript 瀏覽器 API,包括:

  • 套件 org.khronos.webgl:
    • 類型化的陣列,如 Int8Array
    • WebGL 類型。
  • 套件 org.w3c.dom.*:
    • DOM API 類型。
  • 套件 kotlinx.browser:
    • DOM API 全域物件,如 windowdocument

要使用 kotlinx-browser 函式庫中的宣告,請將其作為依賴項新增到您的 專案的建置配置檔案中:

val wasmJsMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-browser:0.3")
}
}