Skip to content

Web/JS 互操作

源:Kotlin/JS Interoperability

Kotlin/JS 允许直接调用 JavaScript 代码,并将 Kotlin 代码编译为 JS 模块供前端使用。这为 Web 平台开发提供了强大的跨平台能力。

Kotlin 调用 JavaScript

kotlin
// jsMain
import kotlin.js.JSON
import kotlin.js.json

// 调用全局 JavaScript 函数
external fun alert(message: String)
external fun confirm(message: String): Boolean

fun showMessage() {
    alert("Hello from Kotlin!")
    val result = confirm("Continue?")
}

// 访问 window 对象
external val window: dynamic

fun getWindowSize(): Pair<Int, Int> {
    return Pair(
        window.innerWidth as Int,
        window.innerHeight as Int
    )
}

// 访问 document
external val document: dynamic

fun setTitle(title: String) {
    document.title = title
}

声明 JavaScript 库

kotlin
// 声明外部模块
@JsModule("lodash")
external object Lodash {
    fun debounce(func: () -> Unit, wait: Int): () -> Unit
    fun throttle(func: () -> Unit, wait: Int): () -> Unit
}

// 使用
fun setupSearch() {
    val debouncedSearch = Lodash.debounce(::performSearch, 300)
    document.getElementById("search").addEventListener("input", debouncedSearch)
}

fun performSearch() {
    // 执行搜索
}

TypeScript 定义映射

kotlin
// TypeScript:
// interface User {
//     id: number;
//     name: string;
//     email?: string;
// }

// Kotlin 映射
external interface User {
    var id: Int
    var name: String
    var email: String?
}

// 创建实例
fun createUser(): User = js("({id: 1, name: 'Alice'})")

// 或使用 json() 函数
fun createUser2(): User = json(
    "id" to 1,
    "name" to "Alice"
).unsafeCast<User>()

DOM API 访问

标准代码块

kotlin
// jsMain
import org.w3c.dom.*
import kotlinx.browser.document
import kotlinx.browser.window

// 查询元素
fun getElementById(id: String): HTMLElement? {
    return document.getElementById(id) as? HTMLElement
}

// 创建元素
fun createButton(text: String, onClick: () -> Unit): HTMLButtonElement {
    return (document.createElement("button") as HTMLButtonElement).apply {
        textContent = text
        addEventListener("click", { onClick() })
    }
}

// 添加元素
fun appendElement(parent: String, child: Element) {
    document.getElementById(parent)?.appendChild(child)
}

// 示例:创建用户列表
fun renderUsers(users: List<User>) {
    val container = document.getElementById("users") ?: return
    container.innerHTML = ""
    
    users.forEach { user ->
        val div = document.createElement("div").apply {
            className = "user-item"
            innerHTML = """
                <h3>${user.name}</h3>
                <p>${user.email ?: ""}</p>
            """.trimIndent()
        }
        container.appendChild(div)
    }
}

事件处理

kotlin
import org.w3c.dom.events.Event
import org.w3c.dom.events.MouseEvent

fun setupEventListeners() {
    document.getElementById("submit-btn")?.addEventListener("click", { event ->
        event.preventDefault()
        submitForm()
    })
    
    document.getElementById("input")?.addEventListener("input", { event ->
        val target = event.target as? HTMLInputElement
        val value = target?.value
        console.log("Input value: $value")
    })
}

Fetch API 与网络请求

标准代码块

kotlin
// jsMain
import kotlinx.browser.window
import kotlin.js.Promise

// 使用 dynamic 类型
suspend fun fetchData(url: String): String {
    return window.fetch(url)
        .then { response -> response.text() }
        .await()
}

// 或声明 Fetch API
external fun fetch(url: String, init: dynamic = definedExternally): Promise<Response>

external interface Response {
    fun text(): Promise<String>
    fun json(): Promise<dynamic>
    val ok: Boolean
    val status: Int
}

// 使用示例
suspend fun getUser(id: String): User {
    val response = fetch("/api/users/$id").await()
    if (!response.ok) {
        throw Exception("HTTP ${response.status}")
    }
    return response.json().await().unsafeCast<User>()
}

// POST 请求
suspend fun createUser(user: User): User {
    val response = fetch("/api/users", js("""{
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(user)
    }""")).await()
    
    return response.json().await().unsafeCast<User>()
}

JavaScript 调用 Kotlin

导出 Kotlin 函数

kotlin
// jsMain
@JsExport
fun greet(name: String): String {
    return "Hello, $name!"
}

@JsExport
class Calculator {
    fun add(a: Int, b: Int): Int = a + b
    fun multiply(a: Int, b: Int): Int = a * b
}
javascript
// JavaScript 使用
import { greet, Calculator } from './kotlin-app.mjs';

console.log(greet('Alice')); // "Hello, Alice!"

const calc = new Calculator();
console.log(calc.add(2, 3)); // 5

自定义导出名称

kotlin
@JsExport
@JsName("formatCurrency")
fun format(amount: Double): String {
    return "$$amount"
}
javascript
import { formatCurrency } from './kotlin-app.mjs';
console.log(formatCurrency(99.99)); // "$99.99"

NPM 依赖集成

Gradle 配置

kotlin
// build.gradle.kts
kotlin {
    js(IR) {
        browser {
            commonWebpackConfig {
                cssSupport {
                    enabled.set(true)
                }
            }
        }
        binaries.executable()
    }
    
    sourceSets {
        val jsMain by getting {
            dependencies {
                // Kotlin/JS 标准库
                implementation("org.jetbrains.kotlinx:kotlinx-html-js:0.11.0")
                
                // NPM 依赖
                implementation(npm("axios", "1.6.2"))
                implementation(npm("react", "18.2.0"))
                implementation(npm("react-dom", "18.2.0"))
            }
        }
    }
}

使用 NPM 包

kotlin
@file:JsModule("axios")
@file:JsNonModule

external object axios {
    fun get(url: String): Promise<AxiosResponse>
    fun post(url: String, data: dynamic): Promise<AxiosResponse>
}

external interface AxiosResponse {
    val data: dynamic
    val status: Int
}

// 使用
suspend fun fetchUsers(): List<User> {
    val response = axios.get("/api/users").await()
    return (response.data as Array<dynamic>)
        .map { it.unsafeCast<User>() }
}

Promise 与协程互操作

Promise 转协程

kotlin
import kotlin.js.Promise

// 扩展函数
suspend fun <T> Promise<T>.await(): T = suspendCoroutine { cont ->
    then(
        onFulfilled = { cont.resume(it) },
        onRejected = { cont.resumeWithException(Exception(it.toString())) }
    )
}

// 使用示例
suspend fun loadData() {
    try {
        val data = fetch("/api/data").await()
        val json = data.json().await()
        // 处理数据
    } catch (e: Exception) {
        console.error("Failed to load data", e)
    }
}

协程转 Promise

kotlin
fun loadDataAsync(): Promise<String> = GlobalScope.promise {
    delay(1000)
    fetchData()
}
javascript
// JavaScript 调用
loadDataAsync()
    .then(data => console.log(data))
    .catch(error => console.error(error));

Dynamic 类型使用

动态访问

kotlin
fun processData(data: dynamic) {
    console.log(data.name)          // 访问属性
    console.log(data.items[0])      // 访问数组
    data.method()                   // 调用方法
}

// 从 JSON 解析
fun parseUser(json: String): dynamic {
    return JSON.parse(json)
}

val user = parseUser("""{"id":1,"name":"Alice"}""")
console.log(user.name) // "Alice"

类型安全转换

kotlin
// 不安全转换
val user: User = dynamicData.unsafeCast<User>()

// 安全转换(带检查)
fun safeCast(data: dynamic): User? {
    return try {
        if (data.id != null && data.name != null) {
            data.unsafeCast<User>()
        } else {
            null
        }
    } catch (e: Exception) {
        null
    }
}

常见框架集成

React 集成

kotlin
@file:JsModule("react")

external interface ReactElement

external object React {
    fun <P> createElement(
        type: Any,
        props: P,
        vararg children: dynamic
    ): ReactElement
}

// 使用
fun MyComponent() = React.createElement(
    "div",
    json("className" to "container"),
    "Hello, React!"
)

Kotlin React DSL

kotlin
// 使用 kotlin-react
import react.*
import react.dom.*

val App = fc<Props> {
    div {
        className = "app"
        h1 { +"Welcome to Kotlin/JS" }
        button {
            onClick = { console.log("Clicked!") }
            +"Click Me"
        }
    }
}

最佳实践

✅ 实践 1:声明外部接口而非使用 dynamic

kotlin
// ✅ 推荐
external interface ApiResponse {
    val data: Array<User>
    val status: String
}

// ❌ 不推荐
fun processResponse(response: dynamic) {
    val data = response.data // 无类型检查
}

✅ 实践 2:使用 @JsModule 声明第三方库

kotlin
// ✅ 类型安全
@JsModule("moment")
external object Moment {
    fun (): MomentObj
}

external interface MomentObj {
    fun format(pattern: String): String
}

// ❌ 动态调用
val moment: dynamic = js("require('moment')")

✅ 实践 3:优先使用协程而非 Promise

kotlin
// ✅ 协程风格(清晰)
suspend fun loadUserData() {
    val user = fetchUser().await()
    val posts = fetchPosts(user.id).await()
    render(user, posts)
}

// ❌ Promise 链式调用(复杂)
fun loadUserData(): Promise<Unit> {
    return fetchUser()
        .then { user -> fetchPosts(user.id) }
        .then { posts -> /* ... */ }
}

❌ 避免过度使用 js()

kotlin
// ❌ 不推荐
val result = js("someComplexJSCode()")

// ✅ 推荐:声明外部函数
external fun someComplexJSCode(): dynamic
val result = someComplexJSCode()

构建配置

Webpack 配置

kotlin
// build.gradle.kts
kotlin {
    js(IR) {
        browser {
            commonWebpackConfig {
                devServer = devServer?.copy(
                    port = 3000,
                    open = true
                )
                
                cssSupport {
                    enabled.set(true)
                }
            }
            
            webpackTask {
                output.libraryTarget = "umd"
            }
        }
        
        binaries.executable()
    }
}

模块系统选择

kotlin
kotlin {
    js(IR) {
        // UMD(Universal Module Definition)
        browser {
            webpackTask {
                output.libraryTarget = "umd"
            }
        }
        
        // CommonJS
        nodejs {
            /* ... */
        }
    }
}

Kotlin/JS 提供了与 JavaScript 生态无缝集成的能力,允许开发者在保持类型安全的同时,充分利用 Web 平台的强大功能。