开源鸿蒙平台 KMP_CMP 三方库「kotlinx.html」适配全流程
本文记录
kotlinx.html接入 OpenHarmony 的完整过程,覆盖 KMP/CMP 工程盘点、ohosArm64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。本次适配复用 Kotlin 侧的 HTML DSL、转义规则、文档结构检查和 Native 渲染结果,再由 ArkTS 调用
libentry.so的 N-API 方法显示网页。这样验证的是同一份kotlinx.html代码在 OpenHarmony ARM64 设备上的运行结果,而不是重新在页面里手写一套 HTML。
项目地址: AtomGit/oh-tpc/kotlinx.html
开发工具: 华为云码道
一、背景
1.1 为什么做开源鸿蒙平台 KMP/CMP 适配
kotlinx.html 是一个用 Kotlin DSL 构造 HTML 的 Kotlin Multiplatform 库。它的核心价值在于:标签、属性、转义和流式输出由公共 Kotlin 代码统一实现,业务项目可以在多个平台复用同一套 HTML 生成逻辑。
如果只把示例页面重新写成 ArkTS,页面可以显示几段固定文本,却无法证明公共 HTML DSL、Kotlin/Native 动态库、C ABI、C++ N-API 和 ArkUI Web 组件已经连通。本次适配需要同时解决以下问题:
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 根 KMP 模块默认没有 OpenHarmony 目标,必须加入 ohosArm64() 才能生成对应 KLIB 和动态库。 |
| 工具链不一致 | Kotlin/Native、OpenHarmony LLVM、Native SDK、Gradle 和 DevEco/Hvigor 版本需要匹配。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin data class,必须经过 C ABI、C++ N-API 和 JSON。 |
| 内存生命周期 | Kotlin/Native 返回的 UTF-8 缓冲区由 Native heap 分配,C++ 创建 JS 字符串后必须调用 HtmlFree。 |
| HTML 输入边界 | 文本内容、属性值和显式 unsafe 的行为必须由共享 Kotlin 代码检查,不能在 ArkTS 侧复制规则。 |
| Web 生命周期 | ArkWeb 的渲染面在 Web 组件挂载后才可稳定调用 loadData,否则首次预览可能空白或需要重复点击。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名、设备安装和 ARM64 依赖需要分别验收。 |
因此,本项目把适配边界放在三个地方:Kotlin/Native 目标配置、C ABI/N-API 桥接层和 ArkUI 页面层。HTML 语义仍然由 KMP 公共源码维护,ArkTS 只负责调用、页面状态和 Web 生命周期。
1.2 库提供的能力
kotlinx.html 公共模块提供 HTML 标签 DSL、属性编码、文本转义、unsafe 原始输出和流式渲染能力。OpenHarmony 示例将这些能力收敛到 HtmlExamples 门面,提供三组确定性模板:
escaped-text:验证普通文本中的<、>和&会被转义;attributes:验证链接、嵌套标签和data-label属性的编码;unsafe-content:验证只有显式调用unsafe才会输出原始 HTML。
示例生成的文档包含以下内容:
| 示例 | 关键输出 | 用途 |
|---|---|---|
escaped-text | <h1>HTML from Kotlin</h1>、<hello> & goodbye | 检查文本转义和完整文档结构 |
attributes | href="https://kotlinlang.org"、data-label="a & b" | 检查属性转义和标签嵌套 |
unsafe-content | <span class="raw">raw</span> | 检查原始内容必须显式 opt-in |
Native 层对外提供五个 C ABI 函数:
| 函数 | 作用 |
|---|---|
HtmlCatalog | 返回三个示例的 JSON 数组 |
HtmlGet | 按索引返回一个示例的 JSON |
HtmlRender | 生成一次新的 HTML 文档 |
HtmlRunChecks | 执行八项共享检查并返回 JSON |
HtmlFree | 释放 Native 返回的字符串 |
八项检查全部通过时,ArkUI 页面显示 8/8 公共检查通过。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | HTML 模板、转义规则、标签结构检查和 JSON 生成由 Kotlin 共享。 |
| 平台目标 | 公共库和示例加入 ohosArm64,生成 libkotlinx_html.so。 |
| 桥接稳定 | 只暴露少量 C ABI 函数,使用 UTF-8 JSON,不把 Kotlin 对象地址交给 ArkTS。 |
| UI 完整 | 页面支持网页预览、原始标签内容、Native 状态和八项检查结果。 |
| 可测试 | JVM 测试、Native 链接、ELF 依赖、HAP 构建、安装和 Web 交互分别验收。 |
| 生命周期 | Web 挂载后延迟加载 HTML,切换到源码时清除 ready 状态,避免重复点击。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面是独立的真机验收宿主,公共
kotlinx.htmlAPI 仍然保持平台无关。其他 KMP/CMP 应用可以复用同一套 HTML DSL,再自行决定使用 Compose Multiplatform、ArkUI 或其他 UI 展示层。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点 KMP 公共 API、示例门面和 OpenHarmony 工程边界
第 2 阶段:目标与依赖打通 ── 加入 ohosArm64、独立 example 工程和 Native 构建任务
第 3 阶段:HTML 与序列化 ── 建立模板、转义检查、稳定 JSON 和错误边界
第 4 阶段:原生桥接 ── Kotlin/Native C ABI、C++ N-API、参数检查和内存释放
第 5 阶段:Web 页面封装 ── ArkTS JSON 客户端、Web 生命周期、网页/源码切换
第 6 阶段:示例与验证 ── HAP 构建、签名、设备安装、空白修复和效果图
每个阶段都使用真实产物作为下一阶段输入:先用 JVM 验证共享 HTML,再把相同代码链接为 ARM64 动态库,随后由 CMake 和 N-API 加载到 Stage 工程,最后安装签名 HAP 观察 ArkUI Web 页面。
三、逐步实现过程
第 1 阶段:项目初始化
1.1 盘点公共 API 和工程边界
项目保留上游的公共源码集,并新增 OpenHarmony 示例层:
kotlinx.html/ HTML DSL 和多平台实现
├── src/commonMain/ 标签、属性、流式消费者
├── src/commonTest/ 公共 API 测试
├── example/shared/ HtmlExamples 门面和共享测试
├── example/nativeApp/ ohosArm64 Kotlin/Native 动态库
├── example/ohosApp/ DevEco Stage 工程和 ArkUI 页面
├── scripts/ Native、HAP 和依赖检查脚本
└── docs/openharmony/ OpenHarmony 验证记录和效果图
example 是独立 Gradle 工程,不把 DevEco 工程当作 Kotlin 子模块。这样可以分别执行 Gradle 和 Hvigor,也可以把 ohosApp 复制到另一个目录后在 DevEco Studio 中配置签名。
1.2 固定工具链和版本矩阵
| 项目 | 配置 | 用途 |
|---|---|---|
| Kotlin Multiplatform | 2.2.21-1.0.0 | JVM、Kotlin/Native 和 KLIB |
| 项目版本 | 0.12.0 | kotlinx.html 版本标识 |
| JDK | 21 | Gradle、Kotlin 编译和 Native 任务 |
| OpenHarmony 目标 | ohosArm64 | ARM64 真机动态库 |
| DevEco product | 6.0.0(20) | Stage 工程兼容和目标版本 |
| ABI | arm64-v8a | HAP 原生库架构 |
执行 Gradle 脚本前先选择 JDK 21:
export JAVA_HOME="/path/to/jdk-21"
java -version
OpenHarmony 设备是否能运行 Web 组件还要单独验证。目标版本满足要求,只说明工程可以编译和安装,不能替代设备验收。
1.3 创建 OpenHarmony 示例目录
示例页面围绕“当前文档、HTML 内容、渲染状态和切换动作”组织:
标题区 kotlinx.html 工作台 / Kotlin Multiplatform / OpenHarmony
文档区 rendered / A fresh document rendered by kotlinx.html
预览区 ArkUI Web 显示 HTML 文档
操作区 恢复标签内容 / 显示网页
检查区 8/8 公共检查通过 · 已渲染 N 次
“恢复标签内容”只显示 Native 返回的原始 HTML 字符串,“显示网页”才显示 ArkWeb 预览。两条路径共用同一个 selected 文档,便于定位是 Kotlin 生成、桥接还是 Web 生命周期的问题。
第 2 阶段:目标与依赖打通
2.1 加入 ohosArm64 目标
根工程在保留原有 KMP 目标的基础上增加 OpenHarmony:
kotlin {
jvm()
js(IR) { browser() }
wasmJs { browser() }
ohosArm64()
sourceSets {
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}
共享示例也声明 jvm() 和 ohosArm64(),保证 HtmlExamples 既能在 JVM 测试中运行,也能被 Kotlin/Native 动态库链接。
2.2 Native focused build 的作用
example/nativeApp 构建受 linker map 约束的 shared library:
kotlin {
ohosArm64 {
binaries.sharedLib {
baseName = "kotlinx_html"
linkerOpts(
"--entry=0",
"--version-script=${project.file("src/ohosArm64Main/linker/shared-library.map")}",
)
linkerOpts("-lace_napi.z", "-luv", "-lhilog_ndk.z")
}
}
}
导出范围由 shared-library.map 固定为:
HtmlCatalog
HtmlGet
HtmlRender
HtmlRunChecks
HtmlFree
这样 ArkTS 只能通过明确的 ABI 获取文档和检查结果,Kotlin/Native 内部实现不会变成不受控的 ABI。
2.3 配置仓库和独立消费工程
example/settings.gradle.kts 使用本地根工程、Maven Central 和 OpenHarmony 社区 Maven:
pluginManagement {
repositories {
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
mavenCentral()
gradlePluginPortal()
}
}
includeBuild("..")
include(":shared", ":nativeApp")
example 通过 includeBuild("..") 消费当前库,而不是依赖一个可能过期的远程二进制。这样修改 src/commonMain 后,下一次 Native 链接会直接使用当前源码。
2.4 通过构建产物消费共享库
prepareOhos 在 Native 链接成功后复制动态库和头文件:
val prepareOhos by tasks.registering(Copy::class) {
dependsOn("linkDebugSharedOhosArm64")
from(layout.buildDirectory.dir("bin/ohosArm64/debugShared")) {
include("libkotlinx_html.so")
into("libs/arm64-v8a")
}
from(layout.buildDirectory.dir("bin/ohosArm64/debugShared")) {
include("libkotlinx_html_api.h")
into("src/main/cpp/include")
}
into(rootProject.layout.projectDirectory.dir("../example/ohosApp/entry"))
}
产物位置固定为:
example/ohosApp/entry/libs/arm64-v8a/libkotlinx_html.so
example/ohosApp/entry/src/main/cpp/include/libkotlinx_html_api.h
动态库、生成头文件和 HAP 都属于构建产物,仓库通过 .gitignore 排除它们,每台开发机都可以根据自己的 SDK 重新生成。
第 3 阶段:HTML 与序列化
3.1 为什么需要统一 JSON 契约
ArkTS、C++ 和 Kotlin/Native 不能直接共享 Kotlin 对象,因此边界使用 UTF-8 JSON:
HtmlExamples.render()
↓ HtmlExample
Kotlin/Native HtmlRender()
↓ const char* JSON
C++ N-API renderHtml()
↓ JavaScript string
HtmlClient.ets JSON.parse()
↓ HtmlExample
ArkUI Index.ets / Web
页面只解析 JSON,不复制 HTML 模板和转义规则。这样 JVM 测试、Native 动态库和设备页面使用同一份 Kotlin 输出。
3.2 HtmlExample 和 createHTML
公共示例门面定义一个稳定的数据结构:
public data class HtmlExample(
val id: String,
val html: String,
val description: String,
)
HTML 文档由 kotlinx.html 生成:
private fun escapedDocument(): String = createHTML(prettyPrint = false).html {
head { title { +"kotlinx.html" } }
body {
h1 { +"HTML from Kotlin" }
p { +"<hello> & goodbye" }
}
}
最终文本中的 <hello> & goodbye 会变成 <hello> & goodbye,而不是由 ArkTS 重新实现替换规则。
3.3 属性、unsafe 和检查
属性模板验证链接和编码:
private fun attributeDocument(): String = createHTML(prettyPrint = false).html {
head { title { +"Attributes" } }
body {
div {
a("https://kotlinlang.org") {
attributes["data-label"] = "a & b"
+"Kotlin"
}
}
}
}
原始内容必须显式调用 unsafe:
private fun unsafeDocument(): String = createHTML(prettyPrint = false).html {
head { title { +"Unsafe" } }
body {
div { unsafe { raw("<span class=\"raw\">raw</span>") } }
}
}
HtmlExamples.runChecks() 检查三个模板、html/head/body 结构、文本转义、属性转义、嵌套结构、unsafe 输出、非法索引和平台无关 API,共八项。
3.4 JSON 和错误边界
Native 成功返回示例对象:
{
"id": "rendered",
"html": "<html><head><title>kotlinx.html</title></head>...",
"description": "A fresh document rendered by kotlinx.html"
}
异常会编码成 JSON 错误对象:
{"error":"Unknown HTML example index: 9"}
ArkTS 通过 JSON.parse 读取结果,C++ 在 napi_create_string_utf8 成功后立即调用 HtmlFree,避免 Native 堆泄漏。
第 4 阶段:原生桥接(技术难点)
4.1 Kotlin/Native 对象不能直接交给 ArkTS
HtmlExample 属于 Kotlin 运行时对象,不能把对象地址直接当作 JavaScript 对象。最终采用三层桥接:
ArkTS
│ JSON string
▼
C++ N-API entry
│ const char*
▼
Kotlin/Native C ABI
│ HtmlExamples
▼
UTF-8 JSON + HtmlFree
4.2 方案对比
| 方案 | 优点 | 缺点 | 选用 |
|---|---|---|---|
| 直接导出 Kotlin 对象 | 代码少 | ABI、生命周期和类型不可控 | ❌ |
| 只导出 HTML 字符串 | 实现简单 | 页面难以获取目录和检查结果 | ❌ |
| C ABI + JSON | 边界清晰、易扩展、易调试 | 有一次序列化开销 | ✅ |
| 在 ArkTS 重写 HTML 生成 | 页面调用简单 | KMP 和 ArkTS 输出容易分叉 | ❌ |
4.3 Kotlin/Native 导出函数
@CName("HtmlRender")
public fun renderNative(): CPointer<ByteVar> = response {
HtmlExamples.render().toJson()
}
@CName("HtmlRunChecks")
public fun checksNative(): CPointer<ByteVar> = response {
val checks = HtmlExamples.runChecks()
"{\"passed\":true,\"checks\":[${checks.joinToString { quote(it) }}]}"
}
@CName("HtmlFree")
public fun freeNative(pointer: CPointer<ByteVar>?) {
if (pointer != null) nativeHeap.free(pointer.rawValue)
}
返回值是 Native heap 分配的 NUL 结尾 UTF-8 字符串,每一块缓冲区都由 HtmlFree 释放。
4.4 C++ N-API 方法分发
napi_property_descriptor methods[] = {
{"getCatalog", nullptr, Catalog, nullptr, nullptr, nullptr,
napi_default, nullptr},
{"getHtml", nullptr, GetHtml, nullptr, nullptr, nullptr,
napi_default, nullptr},
{"renderHtml", nullptr, RenderHtml, nullptr, nullptr, nullptr,
napi_default, nullptr},
{"runChecks", nullptr, RunChecks, nullptr, nullptr, nullptr,
napi_default, nullptr},
};
getHtml 会检查参数数量、有限数值、整数性和非负范围,再调用 HtmlGet(index)。所有方法都遵循“调用 Native、创建 ArkTS 字符串、释放 Native 缓冲区”的顺序。
CMake 将 Kotlin/Native 动态库作为 imported library:
add_library(kotlinx_html SHARED IMPORTED)
set_target_properties(kotlinx_html PROPERTIES
IMPORTED_LOCATION
"${CMAKE_CURRENT_SOURCE_DIR}/../../../libs/arm64-v8a/libkotlinx_html.so")
add_library(entry SHARED napi_init.cpp)
target_include_directories(entry PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include")
target_link_libraries(entry PRIVATE kotlinx_html libace_napi.z.so)
4.5 N-API 生命周期
ArkTS renderHtml()
│
▼
HtmlRender()
│
▼
napi_create_string_utf8(...)
│
▼
HtmlFree(nativeBuffer)
│
▼
return JavaScript string
C++ 负责跨语言字符串转换和 Native 释放,ArkTS 不需要知道 Kotlin/Native 的堆实现。
第 5 阶段:Web 页面封装
5.1 ArkTS 调用 N-API 客户端
HtmlClient.ets 对 N-API 返回的字符串做类型化解析:
import htmlNative from 'libentry.so';
export function renderHtml(): HtmlExample {
return JSON.parse(htmlNative.renderHtml()) as HtmlExample;
}
export function runChecks(): HtmlChecks {
return JSON.parse(htmlNative.runChecks()) as HtmlChecks;
}
页面因此只依赖 HtmlExample 和 HtmlChecks 接口,不直接处理指针或 N-API 对象。
5.2 ArkUI Web 生命周期
页面使用 WebviewController.loadData 将 Native 返回的 HTML 放进 ArkWeb:
Web({ src: this.previewSource, controller: this.webController })
.width('100%')
.height(230)
.onAppear(() => {
this.webReady = true;
this.loadPreview();
this.schedulePreviewLoad();
})
设备实测发现,onAppear 触发时 Web 渲染面可能仍在创建。如果立即调用 loadData,页面可能保持 about:blank。示例在下一次 UI turn 延迟 100ms 再加载:
private schedulePreviewLoad(): void {
setTimeout(() => {
if (this.showPreview) {
this.loadPreview();
}
}, 100);
}
切换到标签内容时将 webReady 清零;再次点击“显示网页”时先刷新文档,再安排延迟加载,避免出现“必须点击两次”才能看到网页的问题。
5.3 ArkUI 页面状态
Index.ets 保存 Native 文档、状态文本、错误文本和渲染次数:
@State private selected: HtmlExample = emptyHtml();
@State private status: string = '正在加载';
@State private errorText: string = '';
@State private renderCount: number = 0;
初始化流程为:
aboutToAppear
↓
renderHtml() + runChecks()
↓
selected / status / renderCount
↓
Web.onAppear + delayed loadData
5.4 页面交互预设
页面提供两个操作按钮:
- 恢复标签内容:显示 HTML 字符串,方便检查
<、&和属性编码; - 显示网页:将当前文档交给 ArkWeb 渲染,验证 Native 到 Web 的完整链路。
预览不访问网络和硬件,只消费本地 Native 生成的文档;因此即使设备没有额外系统能力,也可以完成 KMP/CMP 核心验收。
第 6 阶段:示例与验证
6.1 example 工程结构
example/
├── shared/
│ ├── src/commonMain/.../HtmlExamples.kt
│ └── src/commonTest/.../HtmlExamplesTest.kt
├── nativeApp/
│ ├── src/ohosArm64Main/.../NativeBridge.kt
│ └── src/ohosArm64Main/linker/shared-library.map
└── ohosApp/
├── AppScope/
├── entry/src/main/cpp/
│ ├── CMakeLists.txt
│ └── napi_init.cpp
└── entry/src/main/ets/
├── pages/Index.ets
└── html/HtmlClient.ets
shared 验证公共 HTML,nativeApp 产生 ARM64 动态库,ohosApp 负责 ArkUI 页面、CMake 和 N-API 模块。三者的边界清晰,任何一层失败都能单独定位。
6.2 原生模块注册
napi_init.cpp 通过 napi_module_register 注册 entry 模块,ArkTS 类型声明位于:
example/ohosApp/entry/src/main/cpp/types/libentry/index.d.ts
ArkTS 使用:
import htmlNative from 'libentry.so';
6.3 Native 动态库准备
执行:
export JAVA_HOME="/path/to/jdk-21"
./scripts/build-openharmony.sh
脚本依次执行根模块 jvmTest、示例共享测试、linkDebugSharedOhosArm64 和 prepareOhos。成功后生成:
example/ohosApp/entry/libs/arm64-v8a/libkotlinx_html.so
example/ohosApp/entry/src/main/cpp/include/libkotlinx_html_api.h
检查 ARM64 ELF 的强依赖:
python3 scripts/check-native-deps.py \
/path/to/openharmony/native-sdk \
example/ohosApp/entry/libs/arm64-v8a
6.4 构建、签名和安装
未配置签名时,可以在 DevEco Studio 构建未签名 HAP;真机安装需要签名 HAP。配置签名后执行:
./scripts/build-hap.sh "$PWD/example/ohosApp"
产物位于:
example/ohosApp/entry/build/default/outputs/default/entry-default-signed.hap
安装并启动:
hdc list targets -v
hdc -t <设备序列号> install -r \
example/ohosApp/entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <设备序列号> shell aa start \
-a EntryAbility -b org.jetbrains.kotlinx.html.sample
签名证书、profile、p12 和密码只保存在本机,不要提交到 AtomGit。
四、完整代码对照
4.1 整体架构
kotlinx.html createHTML
│ HtmlExample / HTML string
▼
Kotlin/Native HtmlRender / HtmlRunChecks
│ C ABI + UTF-8 JSON
▼
libentry.so C++ N-API
│ JavaScript string
▼
HtmlClient.ets JSON.parse
│ typed data
▼
ArkUI Index.ets → WebviewController.loadData
4.2 文件清单
| 文件 | 职责 |
|---|---|
src/commonMain/kotlin | HTML 标签、属性和流式渲染实现 |
example/shared/.../HtmlExamples.kt | 三个模板、render 和八项检查 |
example/shared/.../HtmlExamplesTest.kt | JVM/Native 共享门面测试 |
example/nativeApp/.../NativeBridge.kt | C ABI、JSON 返回和 Native 内存释放 |
example/ohosApp/.../HtmlClient.ets | N-API JSON 解析和类型接口 |
example/ohosApp/.../Index.ets | Web 预览、源码切换和状态展示 |
example/ohosApp/entry/src/main/cpp/napi_init.cpp | N-API 导出和参数检查 |
example/ohosApp/entry/src/main/cpp/CMakeLists.txt | imported .so 和 N-API 链接 |
scripts/build-openharmony.sh | 测试、Native 链接和产物复制 |
scripts/build-hap.sh | OHPM、Hvigor 和 HAP 构建 |
docs/openharmony/VALIDATION.md | 自动检查和真机验收说明 |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| Kotlin | createHTML | 创建 HTML 文档 |
| Kotlin | HtmlExamples.render | 生成当前网页预览 |
| Kotlin | HtmlExamples.runChecks | 执行八项共享检查 |
| Native | HtmlRender | 返回网页 JSON |
| Native | HtmlFree | 释放返回字符串 |
| N-API | renderHtml | 向 ArkTS 暴露网页生成方法 |
| N-API | runChecks | 向 ArkTS 暴露检查结果 |
| ArkTS | WebviewController.loadData | 在 ArkWeb 中渲染 HTML |
4.4 ArkTS 与 Kotlin 的边界
ArkTS 只负责读取 JSON 和 Web 生命周期:
const example: HtmlExample = renderHtml();
this.selected = example;
this.webController.loadData(example.html, 'text/html', 'UTF-8');
Kotlin 负责 HTML 语义和转义:
val rendered = HtmlExamples.render()
check(rendered.html.contains("<html>"))
两者之间只传输 JSON 字符串,不传输 Kotlin 对象、ArkTS class 实例或未校验的动态结构。
五、关键决策说明
决策 1:把 ohosArm64 加入公共构建约定
只有把原有 commonMain 代码真正链接为 ohosArm64 动态库,才能证明 HTML DSL 可以进入 OpenHarmony 运行时。单独在 ArkTS 页面写静态 HTML 不能替代这项验证。
决策 2:独立消费者必须通过构建产物消费
example 单独解析根工程,ohosApp 单独接收 .so 和头文件,避免根工程编译通过却无法在 DevEco 中打包。
决策 3:JSON 作为跨语言数据契约
JSON 让 ArkTS、C++ 和 Kotlin/Native 的边界清楚可调试,并允许后续增加文档描述或检查字段,而不暴露 Kotlin 对象布局。
决策 4:桥接层只开放五个 C ABI 入口
目录、按索引读取、渲染、自检和释放已经覆盖示例所需能力;减少 ABI 符号可以降低 Native 生命周期和兼容风险。
决策 5:源码状态和网页状态分开
“恢复标签内容”用于检查字符串和转义,“显示网页”用于检查 ArkWeb。两条路径共享同一份 HtmlExample,可以把 HTML 生成问题和 Web 挂载问题分开定位。
决策 6:把库验证和设备验证分开
JVM 测试验证 HTML 规则,Native 链接验证 ABI,Hvigor 验证 HAP,真机验证 WebviewController.loadData 和页面交互。每一层都有明确的失败边界。
六、测试与验证
6.1 测试环境
本次真机验证使用:
- macOS;
- Kotlin Multiplatform
2.2.21-1.0.0; - DevEco Studio 及 OpenHarmony ARM64 Native SDK;
- 已签名
entry-default-signed.hap; - USB 连接的 HarmonyOS ARM64 真机 Huawei Mate 60 Pro;
hdc设备序列号FMR0223825079397。
Gradle 命令要求 JDK 21;HAP 由 DevEco/Hvigor 工具链构建。
6.2 静态检查与单元测试
./gradlew jvmTest
(cd example && ./gradlew :shared:jvmTest)
测试覆盖三个模板、文档结构、文本和属性转义、嵌套标签、unsafe 输出、非法索引和八项公共自检。
6.3 原生桥接和 HAP 验证
./scripts/build-openharmony.sh
python3 scripts/check-native-deps.py \
/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native \
example/ohosApp/entry/libs/arm64-v8a
./scripts/build-hap.sh example/ohosApp
验证关注以下结果:
libkotlinx_html.so: 0 unresolved strong imports
Hvigor BUILD SUCCESSFUL
entry-default-signed.hap generated
6.4 功能验证用例
用例 1:默认页面和 Native 自检
启动应用后页面显示“kotlinx.html 工作台”和 8/8 公共检查通过。初始文档由 Native renderHtml() 生成。
用例 2:显示网页
点击“显示网页”,确认 ArkWeb 区域出现 HTML from Kotlin 和转义后的文本。首次加载等待 Web 渲染面完成,不需要重复点击。
用例 3:恢复标签内容
点击“恢复标签内容”,确认页面显示 HTML 字符串,并能看到 <html>、<hello> 和 & 等内容。
用例 4:切换网页和源码
重复执行“恢复标签内容 → 显示网页”,确认每次只点击一次“显示网页”即可恢复预览,状态渲染次数正常递增。
用例 5:N-API 参数边界
通过 getHtml(index) 读取有效索引,通过共享检查验证负索引会被拒绝;C++ 层同时拒绝非整数、无穷和超出 int32 范围的参数。
用例 6:设备和网络无关
断开网络后重新启动,页面仍能使用本地 Native HTML。该示例不依赖远程网页,便于区分 Web 组件问题和网络问题。
6.5 验证结论
Kotlin/Native ARM64 动态库、CMake/N-API 桥接、Hvigor HAP 构建、签名安装和真机页面渲染均已完成。真机效果图显示了 HTML 页面、操作按钮和 8/8 公共检查通过,说明从 kotlinx.html 到 ArkUI Web 的链路可用。
七、运行效果
7.1 真机截图

截图中可以看到:
- 页面标题为“kotlinx.html 工作台”;
- 副标题说明 Kotlin Multiplatform / OpenHarmony;
- 当前文档为
rendered; - Web 区域显示
HTML from Kotlin; - 文本中的
<hello> & goodbye已按 HTML 规则转义; - 页面提供“恢复标签内容”和“显示网页”两个操作;
- 状态栏显示
8/8 公共检查通过; - 底部说明数据经过 Kotlin/Native C ABI 和 N-API 提供给 ArkTS。
7.2 命令速查
# 根工程测试和 OpenHarmony Native 准备
export JAVA_HOME="/path/to/jdk-21"
./scripts/build-openharmony.sh
# 单独运行共享测试
(cd example && ./gradlew :shared:jvmTest)
# 设备依赖检查
python3 scripts/check-native-deps.py \
/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native \
example/ohosApp/entry/libs/arm64-v8a
# 在已配置签名的工程中构建 HAP
./scripts/build-hap.sh example/ohosApp
# 安装和启动
hdc -t <设备序列号> install -r \
example/ohosApp/entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <设备序列号> shell aa start \
-a EntryAbility -b org.jetbrains.kotlinx.html.sample
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配:必须把原有公共 HTML 代码真正编译成
ohosArm64动态库。 - N-API 不负责 HTML 业务判断:C++ 只做类型检查、字符串转换和释放,HTML 语义放在 Kotlin。
- Native 字符串必须显式释放:
napi_create_string_utf8完成复制后要立即调用HtmlFree。 - 权限不是 Web 能力:这个示例不需要网络权限,网页来自本地
loadData;Web 空白要先检查挂载时序。 - 首次
loadData可能过早:Web.onAppear不代表 ArkWeb 渲染面已经稳定,延迟加载是设备实测后的修复。
8.2 已知问题
libkotlinx_html.so当前只提供 ARM64(arm64-v8a)版本;- Gradle/Kotlin/Native 任务依赖 JDK 21 和匹配的 OpenHarmony SDK;
- HAP 签名配置只适用于本地开发机,不能直接复制到其他环境;
- Web 页面当前使用固定高度 230,复杂或很长的 HTML 需要业务侧增加滚动和尺寸策略;
- 示例主要用于验证 HTML 生成和桥接,不包含完整的多页面导航或资源服务器。
8.3 未来优化方向
- 增加 HTML 模板目录选择和
getHtml(index)的可视化预览; - 为 Web 组件增加页面开始、结束和错误事件状态;
- 支持更多 OpenHarmony ABI,并在 CI 中加入 Native 链接检查;
- 为 Compose Multiplatform 页面提供同一套
HtmlExample状态卡片; - 增加本地 HTML 资源、图片和 CSS 的加载示例;
- 把跨语言 JSON 契约抽成可复用的 KMP 示例模块。
九、总结
9.1 核心难点回顾
本次适配真正需要处理的不是一个 Web 组件,而是一条完整跨端链路:
kotlinx.html commonMain
→ Kotlin/Native ohosArm64
→ C ABI UTF-8 JSON
→ C++ N-API
→ ArkTS HtmlClient
→ ArkUI WebviewController
→ OpenHarmony 真机页面
9.2 封装层次
- KMP 层:定义 HTML 标签、属性、转义和流式输出;
- 示例层:提供
HtmlExamples、三模板和八项检查; - Native 层:生成 ARM64 动态库,并通过 C ABI 输出有限入口;
- N-API 层:完成参数检查、字符串转换和内存释放;
- ArkTS 层:管理 JSON、Web 生命周期、源码切换和错误文本;
- DevEco 层:完成 CMake、HAP、签名、安装和运行。
9.3 三条经验
- 先让公共 HTML 在 JVM 和 Native 通过,再接入 ArkUI;
- 用 JSON 和少量 C ABI 代替跨语言对象传递;
- 把自动测试、HAP 构建和真机 Web 渲染分别记录,避免把“编译成功”误认为“页面可用”。
9.4 适配成果
当前 kotlinx.html OpenHarmony 适配已完成:
ohosArm64KMP 目标和 ARM64 Native 动态库;HtmlExamples三模板和八项共享检查;- Kotlin/Native + C ABI + C++ N-API 桥接;
getCatalog、getHtml、renderHtml、runChecksArkTS API;- ArkUI
Web本地 HTML 预览和源码切换; - Web 首次加载空白及“需要点击两次”问题修复;
- 签名 HAP 构建、设备安装和效果图;
- AtomGit 项目文档和 OpenHarmony 验收记录。
参考文档
更多推荐




所有评论(0)