Compose/Wasm支持中文显示
Compose很好玩,但他的Wasm编译目标默认是无法显示中文的,怎么解决呢?

誓要救安卓开发于水火的Compose框架,有跨平台支持,能跑在桌面上,甚至跑在Web上(via Wasm)。这令我十分振奋,因为我不必学习另一门语言和技术就能让自己的Demo跑在浏览器上,分享一个链接就能让我的小伙伴体验,而不需要他安装JVM了。

尽管Compose/Wasm的支持还在alpha阶段,且因为依赖Wasm gc特性只能跑在较新的Chrome/Firefox浏览器上,还是很令人期待。

创建一个Compose/Wasm项目

最方便的办法是使用 Kotlin Multiplatform Wizard 。勾选需要的目标平台,直接生成和下载工程。

预览和部署

使用wasmJsBrowserRun任务可以编译并在浏览器预览效果,而使用wasmJsBrowserDistribution可以打包和发布应用。

我借助Compose/Wasm写了个简单的俄罗斯方块,可见在合适场景下还是值得期待的。

支持中文显示

修改按钮文本使其包含中文,可见中文没有正常显示出来,只显示了占位符。

中文无法显示

这个问题在YouTrack上已经有人在跟进,而记录显示上周版本1.7.0解决了这个问题。因此,首先尝试升级Compose版本,问题依旧。

升级Compose版本

于是一番查找找到了原始解决问题的PR,参照其中写法修改顺利显示了汉字,以下步骤:

准备一个中文字体,如资源圆体,复制到资源目录下。

资源圆体

修改代码加载字体


private const val resourceHanTTF = "./ResourceHanRoundedCN-Regular.ttf"

@OptIn(ExperimentalComposeUiApi::class)
fun main() {
    ComposeViewport(document.body!!) {
        val fontFamilyResolver = LocalFontFamilyResolver.current
        val fontsLoaded = remember { mutableStateOf(false) }

        if (fontsLoaded.value) {
            App()
        } else {
            Text("Loading Fonts...")
        }

        LaunchedEffect(Unit) {
            val fontBytes = loadRes(resourceHanTTF).toByteArray()
            val fontFamily = FontFamily(listOf(Font("ResourceHanRounded", fontBytes)))
            fontFamilyResolver.preload(fontFamily)
            fontsLoaded.value = true
        }
    }
}

suspend fun loadRes(url: String): ArrayBuffer {
    return window.fetch(url).await<Response>().arrayBuffer().await()
}

fun ArrayBuffer.toByteArray(): ByteArray {
    val source = Int8Array(this, 0, byteLength)
    return jsInt8ArrayToKotlinByteArray(source)
}

@JsFun(
    """ (src, size, dstAddr) => {
        const mem8 = new Int8Array(wasmExports.memory.buffer, dstAddr, size);
        mem8.set(src);
    }
"""
)
internal external fun jsExportInt8ArrayToWasm(src: Int8Array, size: Int, dstAddr: Int)

internal fun jsInt8ArrayToKotlinByteArray(x: Int8Array): ByteArray {
    val size = x.length

    @OptIn(UnsafeWasmMemoryApi::class)
    return withScopedMemoryAllocator { allocator ->
        val memBuffer = allocator.allocate(size)
        val dstAddress = memBuffer.address.toInt()
        jsExportInt8ArrayToWasm(x, size, dstAddress)
        ByteArray(size) { i -> (memBuffer + i).loadByte() }
    }
}

问题解决

中文问题解决

总结

Compose/Wasm能正常在较新的浏览器运行,开发也不算困难,但由于其自渲染的技术原理,SEO极其不友好。由于浏览器没有统一的获取本地字体API,我们不得不加载远程字体以支持中文。一个完整的中文字体包可能超过10M,实际使用时可能先进行裁剪。


最后修改于 2024-10-25