7.2.5 从 JavaScript 使用 Kotlin 代码

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

7.2.5 从 JavaScript 使用 Kotlin 代码

根据所选的 JavaScript 模块系统,Kotlin/JS 编译器会生成不同的输出。但总的来说,Kotlin 编译器会生成普通的 JavaScript 类、函数和属性,你可以在 JavaScript 代码中自由使用它们。不过有一些细微之处需要记住。

在 plain 模式下把声明隔离到单独的 JavaScript 对象中

如果你把模块类型显式设置为 plain,Kotlin 会创建一个对象,其中包含当前模块的所有 Kotlin 声明。这样做是为了避免污染全局对象。这意味着对于模块 myModule,所有声明都可以通过 myModule 对象从 JavaScript 访问。例如:

1
fun foo() = "Hello"

这个函数可以这样从 JavaScript 调用:

1
alert(myModule.foo());

当你把 Kotlin 模块编译为 UMD(browser 和 nodejs 目标的默认设置)、ESM、CommonJS 或 AMD 这类 JavaScript 模块时,就不能这样直接调用函数了。在这些情况下,你的声明会按照所选的 JavaScript 模块系统暴露出来。例如,使用 UMD、ESM 或 CommonJS 时,调用处看起来是这样的:

1
alert(require('myModule').foo());

关于 JavaScript 模块系统的更多信息,请参阅 JavaScript 模块。

包结构

对于大多数模块系统(CommonJS、Plain 和 UMD),Kotlin 会向 JavaScript 暴露其包结构。除非你把声明定义在根包中,否则必须在 JavaScript 中使用完全限定名。例如:

1
2
3
package my.qualified.packagename

fun foo() = "Hello"

例如,使用 UMD 或 CommonJS 时,你的调用处可能像这样:

1
alert(require('myModule').my.qualified.packagename.foo())

把 plain 作为模块系统设置时,调用处会是:

1
alert(myModule.my.qualified.packagename.foo());

面向 ECMAScript 模块(ESM)时,包信息不会被保留,以改善应用包的体积并符合 ESM 包的典型布局。在这种情况下,用 ES 模块消费 Kotlin 声明看起来是这样的:

1
2
3
import { foo } from 'myModule';

alert(foo());

@JsName 注解

在某些情况下(例如为了支持重载),Kotlin 编译器会对生成到 JavaScript 代码中的函数名和属性名做名称改编。要控制生成的名称,你可以使用 @JsName 注解:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 模块 'kjs'
class Person(val name: String) {
    fun hello() {
        println("Hello $name!")
    }

    @JsName("helloWithGreeting")
    fun hello(greeting: String) {
        println("$greeting $name!")
    }
}

现在你可以这样从 JavaScript 使用这个类:

1
2
3
4
// 如有必要,按所选模块系统导入 'kjs'
var person = new kjs.Person("Dmitry"); // 指向模块 'kjs'
person.hello(); // 打印 "Hello Dmitry!"
person.helloWithGreeting("Servus"); // 打印 "Servus Dmitry!"

如果我们没有指定 @JsName 注解,对应函数的名称会包含一个根据函数签名计算出的后缀,例如 hello_61zpoe$。

注意,有些情况下 Kotlin 编译器不会做名称改编:

  • external 声明不会被改编。
  • 继承自 external 类的非 external 类中被重写的函数不会被改编。

@JsName 的参数必须是常量字符串字面量,并且是合法的标识符。任何向 @JsName 传入非标识符字符串的尝试都会让编译器报错。下面的例子会产生编译期错误:

1
2
@JsName("new C()") // 这里报错
external fun newC()

@JsExport 注解

实验性-通用

通过给顶层声明(例如类、接口或函数)加上 @JsExport 注解,你可以让 Kotlin 声明可以从 JavaScript 或 TypeScript 使用。该注解会以 Kotlin 中给定的名称导出所有嵌套声明。

例如,下面演示如何导出一个带嵌套类和具名伴生对象的 Kotlin 接口:

1
2
3
4
5
6
7
8
@JsExport
interface Identity {
     class Metadata(val tag: String)

    companion object Registry {
        val defaultTag = "GUEST"
    }
}

目前,@JsExport 注解是让你的函数对 Kotlin 可见的唯一方式。

@JsExport 注解还可用于:

  • 多平台项目的公共代码中。它只在为 JavaScript 目标编译时生效,并允许你同时导出非平台特定的 Kotlin 声明。
  • 与 @JsName 注解一起使用,为生成并导出的函数指定名称。这有助于解决导出中的歧义(例如同名函数的重载)。
  • 在文件级别使用 @file:JsExport。

导出值类

你可以把 Kotlin 的内联值类导出为普通的 TypeScript 类。

要导出值类,请在 Kotlin 一侧用 @JsExport 注解标记它:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// Kotlin
@JsExport
@JvmInline
value class Email(val address: String) {
    init { require(address.contains("@")) { "Invalid email" } }
}

@JsExport
class AuthService {
    suspend fun login(email: Email): String = ...
}

在 TypeScript 一侧,它看起来就是一个普通的类:

1
2
3
4
5
6
7
8
// TypeScript
import { AuthService, Email } from "..."
const auth = new AuthService();

console.log(await auth.login(new Email("jane@example.com")));
// "Welcome, jane@example.com!"
console.log(await auth.login(new Email("not-an-email")));
// "Invalid email"

导出挂起 lambda

你可以把 Kotlin 的挂起 lambda 表达式导出为 JavaScript 的 async 函数:

  1. 要启用该特性,请把以下编译器选项添加到你的 build.gradle.kts 文件中:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    kotlin {
        js {
            compilations.all {
                compileTaskProvider.configure {
                    compilerOptions {
                        freeCompilerArgs.add("-Xsuspend-lambda-exporting")
                    }
                }
            }
        }
    }
  1. 用 @JsExport 注解标记相关的 Kotlin 声明:
1
2
3
4
5
6
7
    // Kotlin
    @JsExport
    class TaskRunner {
        suspend fun runTask(task: suspend () -> String): String {
            return task()
        }
    }
  1. 在 TypeScript 一侧,suspend lambda 会被映射为普通的 async 函数:
1
2
3
4
5
6
    // TypeScript
    import { TaskRunner } from "..."

    const runner = new TaskRunner();
    const result = await runner.runTask(async () => "done");
    console.log(result); // "done"

@JsNoRuntime 注解

你可以使用 @JsNoRuntime 注解把 Kotlin 接口导出到 JavaScript/TypeScript。它支持直接映射为普通的 TypeScript 接口。

要导出一个 Kotlin 接口(例如来自 Kotlin Multiplatform 项目的接口):

  1. 在公共代码中用 @JsNoRuntime 标注该 Kotlin 接口:
1
2
3
4
5
6
7
    // commonMain
    import kotlin.js.JsNoRuntime

    @JsNoRuntime
    expect interface DataProcessor {
        fun process(data: String): Int
    }
  1. 在你的 JS 专用源代码中用 @JsNoRuntime 提供 actual 实现:
1
2
3
4
5
6
7
    // jsMain
    import kotlin.js.JsNoRuntime

    @JsNoRuntime
    actual interface DataProcessor {
        actual fun process(data: String): Int
    }
  1. 在 TypeScript 一侧,该接口会被映射为普通的 TypeScript 接口:
1
2
3
4
    // 生成的 .d.ts
    export interface DataProcessor {
        process(data: string): number;
    }

对于 Kotlin Multiplatform 项目,通用规则是:

  • expect 和 actual 接口声明都必须用 @JsNoRuntime 标注。唯一的例外是 actual 一侧平台特定代码中的 external 实现,它们不需要注解。
  • 禁止在 expect 一侧的公共代码中使用 external 接口声明。请改用带 @JsNoRuntime 注解的普通接口。

用 @JsNoRuntime 导出 Kotlin 接口有一些限制。该注解不允许用在:

  • external 接口上,因为它们默认就像带有 @JsNoRuntime 一样。添加它会导致编译器警告。
  • is 和 as 类型检查上。
  • 使用 ::class 语法的类引用上。
  • 作为具体化类型实参传递的接口上。

@JsStatic

实验性-通用

@JsStatic 注解指示编译器为目标声明生成额外的静态方法。这有助于你直接在 JavaScript 中使用 Kotlin 代码中的静态成员。

你可以把 @JsStatic 注解应用于具名对象中定义的函数,以及类与接口中声明的伴生对象里的函数。如果使用该注解,编译器会同时生成对象的静态方法和对象自身的实例方法。例如:

1
2
3
4
5
6
7
8
// Kotlin
class C {
    companion object {
        @JsStatic
        fun callStatic() {}
        fun callNonStatic() {}
    }
}

现在,callStatic() 函数在 JavaScript 中是静态的,而 callNonStatic() 函数不是:

1
2
3
4
5
// JavaScript
C.callStatic(); // 可以,访问静态函数
C.callNonStatic(); // 错误,在生成的 JavaScript 中不是静态函数
C.Companion.callStatic(); // 实例方法仍然保留
C.Companion.callNonStatic(); // 唯一可行的方式

也可以把 @JsStatic 注解应用于对象或伴生对象的属性,使其 getter 和 setter 方法成为该对象或包含伴生对象的类中的静态成员。

此特性是实验性的。欢迎在我们的问题跟踪器 YouTrack 中提供反馈。

用 BigInt 类型表示 Kotlin 的 Long 类型

实验性-通用

在编译为现代 JavaScript(ES2020)时,Kotlin/JS 使用 JavaScript 内置的 BigInt 类型来表示 Kotlin 的 Long 值。

要启用对 BigInt 类型的支持,你需要在 build.gradle(.kts) 文件中添加以下编译器选项:

1
2
3
4
5
6
7
8
9
// build.gradle.kts
kotlin {
    js {
        ...
        compilerOptions {
            freeCompilerArgs.add("-Xes-long-as-bigint")
        }
    }
}

此特性是实验性的。欢迎在我们的问题跟踪器 YouTrack 中提供反馈。

在导出的声明中使用 Long

由于 Kotlin 的 Long 类型可以编译为 JavaScript 的 BigInt 类型,Kotlin/JS 支持把 Long 值导出到 JavaScript。

要启用该特性:

  1. 允许在 Kotlin/JS 中导出 Long。请把以下编译器选项添加到 build.gradle(.kts) 文件的 freeCompilerArgs 属性中:
1
2
3
4
5
6
7
8
9
    // build.gradle.kts
    kotlin {
        js {
            ...
            compilerOptions {
                freeCompilerArgs.add("-XXLanguage:+JsAllowLongInExportedDeclarations")
            }
        }
    }
  1. 启用 BigInt 类型。启用方式见用 BigInt 类型表示 Kotlin 的 Long 类型。

用 BigInt64Array 类型表示 Kotlin 的 LongArray 类型

实验性-通用

在编译为 JavaScript 时,Kotlin/JS 可以使用 JavaScript 内置的 BigInt64Array 类型来表示 Kotlin 的 LongArray 值。

要启用对 BigInt64Array 类型的支持,请把以下编译器选项添加到你的 build.gradle(.kts) 文件中:

1
2
3
4
5
6
7
8
9
// build.gradle.kts
kotlin {
    js {
        ...
        compilerOptions {
            freeCompilerArgs.add("-Xes-long-as-bigint")
        }
    }
}

此特性是实验性的。欢迎在我们的问题跟踪器 YouTrack 中提供反馈。

JavaScript 中的 Kotlin 类型

看看 Kotlin 类型是如何映射到 JavaScript 类型的:

| Kotlin | JavaScript | 备注 |

| Byte、Short、Int、Float、Double | Number | | | Char | Number | 该数值表示字符的码点。 | | Long | BigInt | 需要配置 -Xes-long-as-bigint 编译器选项。 | | Boolean | Boolean | | | String | String | | | Array | Array | | | ByteArray | Int8Array | | | ShortArray | Int16Array | | | IntArray | Int32Array | | | CharArray | UInt16Array | 带有属性 $type$ == "CharArray"。 | | FloatArray | Float32Array | | | DoubleArray | Float64Array | | | LongArray | BigInt64Array | | | BooleanArray | Int8Array | 带有属性 $type$ == "BooleanArray"。 | | List、MutableList | KtList、KtMutableList | 通过 KtList.asJsReadonlyArrayView 或 KtMutableList.asJsArrayView 暴露 Array。 | | Map、MutableMap | KtMap、KtMutableMap | 通过 KtMap.asJsReadonlyMapView 或 KtMutableMap.asJsMapView 暴露 ES2015 的 Map。 | | Set、MutableSet | KtSet、KtMutableSet | 通过 KtSet.asJsReadonlySetView 或 KtMutableSet.asJsSetView 暴露 ES2015 的 Set。 | | Unit | Undefined | 作为返回类型时可以导出,作为参数类型时不行。 | | Any | Object | | | Throwable | Error | | | enum class Type | Type | 枚举项暴露为类的静态属性(Type.ENTRY)。 | | 可空 Type? | Type | null | 不支持 undefined | | | 除标记了 @JsExport 之外的所有其他 Kotlin 类型 | 不支持 | 包括 Kotlin 的无符号整数类型。 |

此外,还需要知道:

  • Kotlin 会保留 kotlin.Int、kotlin.Byte、kotlin.Short、kotlin.Char 和 kotlin.Long 的溢出语义。
  • Kotlin 在运行时无法区分数值类型(kotlin.Long 除外),因此下面的代码可以工作:
1
2
3
4
5
  fun f() {
      val x: Int = 23
      val y: Any = x
      println(y as Float)
  }
  • Kotlin 在 JavaScript 中保留对象的惰性初始化。