开源鸿蒙平台 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>、&lt;hello&gt; &amp; goodbye 检查文本转义和完整文档结构
attributes href="https://kotlinlang.org"、data-label="a &amp; 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.html API 仍然保持平台无关。其他 KMP/CMP 应用可以复用同一套 HTML DSL,再自行决定使用 Compose Multiplatform、ArkUI 或其他 UI 展示层。

二、实现路线图

text 复制代码
第 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 示例层:

text 复制代码
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:

bash 复制代码
export JAVA_HOME="/path/to/jdk-21"
java -version

OpenHarmony 设备是否能运行 Web 组件还要单独验证。目标版本满足要求,只说明工程可以编译和安装,不能替代设备验收。

1.3 创建 OpenHarmony 示例目录

示例页面围绕"当前文档、HTML 内容、渲染状态和切换动作"组织:

text 复制代码
标题区          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 复制代码
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 复制代码
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 固定为:

text 复制代码
HtmlCatalog
HtmlGet
HtmlRender
HtmlRunChecks
HtmlFree

这样 ArkTS 只能通过明确的 ABI 获取文档和检查结果,Kotlin/Native 内部实现不会变成不受控的 ABI。

2.3 配置仓库和独立消费工程

example/settings.gradle.kts 使用本地根工程、Maven Central 和 OpenHarmony 社区 Maven:

kotlin 复制代码
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 链接成功后复制动态库和头文件:

kotlin 复制代码
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"))
}

产物位置固定为:

text 复制代码
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:

text 复制代码
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

公共示例门面定义一个稳定的数据结构:

kotlin 复制代码
public data class HtmlExample(
    val id: String,
    val html: String,
    val description: String,
)

HTML 文档由 kotlinx.html 生成:

kotlin 复制代码
private fun escapedDocument(): String = createHTML(prettyPrint = false).html {
    head { title { +"kotlinx.html" } }
    body {
        h1 { +"HTML from Kotlin" }
        p { +"<hello> & goodbye" }
    }
}

最终文本中的 <hello> & goodbye 会变成 &lt;hello&gt; &amp; goodbye,而不是由 ArkTS 重新实现替换规则。

3.3 属性、unsafe 和检查

属性模板验证链接和编码:

kotlin 复制代码
private fun attributeDocument(): String = createHTML(prettyPrint = false).html {
    head { title { +"Attributes" } }
    body {
        div {
            a("https://kotlinlang.org") {
                attributes["data-label"] = "a & b"
                +"Kotlin"
            }
        }
    }
}

原始内容必须显式调用 unsafe:

kotlin 复制代码
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 成功返回示例对象:

json 复制代码
{
  "id": "rendered",
  "html": "<html><head><title>kotlinx.html</title></head>...",
  "description": "A fresh document rendered by kotlinx.html"
}

异常会编码成 JSON 错误对象:

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 对象。最终采用三层桥接:

text 复制代码
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 导出函数
kotlin 复制代码
@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 方法分发
cpp 复制代码
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:

cmake 复制代码
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 生命周期
text 复制代码
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 返回的字符串做类型化解析:

typescript 复制代码
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:

typescript 复制代码
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 再加载:

typescript 复制代码
private schedulePreviewLoad(): void {
  setTimeout(() => {
    if (this.showPreview) {
      this.loadPreview();
    }
  }, 100);
}

切换到标签内容时将 webReady 清零;再次点击"显示网页"时先刷新文档,再安排延迟加载,避免出现"必须点击两次"才能看到网页的问题。

5.3 ArkUI 页面状态

Index.ets 保存 Native 文档、状态文本、错误文本和渲染次数:

typescript 复制代码
@State private selected: HtmlExample = emptyHtml();
@State private status: string = '正在加载';
@State private errorText: string = '';
@State private renderCount: number = 0;

初始化流程为:

text 复制代码
aboutToAppear
    ↓
renderHtml() + runChecks()
    ↓
selected / status / renderCount
    ↓
Web.onAppear + delayed loadData
5.4 页面交互预设

页面提供两个操作按钮:

  • 恢复标签内容 :显示 HTML 字符串,方便检查 &lt;、&amp; 和属性编码;
  • 显示网页:将当前文档交给 ArkWeb 渲染,验证 Native 到 Web 的完整链路。

预览不访问网络和硬件,只消费本地 Native 生成的文档;因此即使设备没有额外系统能力,也可以完成 KMP/CMP 核心验收。

第 6 阶段:示例与验证

6.1 example 工程结构
text 复制代码
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 类型声明位于:

text 复制代码
example/ohosApp/entry/src/main/cpp/types/libentry/index.d.ts

ArkTS 使用:

typescript 复制代码
import htmlNative from 'libentry.so';
6.3 Native 动态库准备

执行:

bash 复制代码
export JAVA_HOME="/path/to/jdk-21"
./scripts/build-openharmony.sh

脚本依次执行根模块 jvmTest、示例共享测试、linkDebugSharedOhosArm64 和 prepareOhos。成功后生成:

text 复制代码
example/ohosApp/entry/libs/arm64-v8a/libkotlinx_html.so
example/ohosApp/entry/src/main/cpp/include/libkotlinx_html_api.h

检查 ARM64 ELF 的强依赖:

bash 复制代码
python3 scripts/check-native-deps.py \
  /path/to/openharmony/native-sdk \
  example/ohosApp/entry/libs/arm64-v8a
6.4 构建、签名和安装

未配置签名时,可以在 DevEco Studio 构建未签名 HAP;真机安装需要签名 HAP。配置签名后执行:

bash 复制代码
./scripts/build-hap.sh "$PWD/example/ohosApp"

产物位于:

text 复制代码
example/ohosApp/entry/build/default/outputs/default/entry-default-signed.hap

安装并启动:

bash 复制代码
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 整体架构

text 复制代码
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 生命周期:

typescript 复制代码
const example: HtmlExample = renderHtml();
this.selected = example;
this.webController.loadData(example.html, 'text/html', 'UTF-8');

Kotlin 负责 HTML 语义和转义:

kotlin 复制代码
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 静态检查与单元测试

bash 复制代码
./gradlew jvmTest
(cd example && ./gradlew :shared:jvmTest)

测试覆盖三个模板、文档结构、文本和属性转义、嵌套标签、unsafe 输出、非法索引和八项公共自检。

6.3 原生桥接和 HAP 验证

bash 复制代码
./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

验证关注以下结果:

text 复制代码
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>、&lt;hello&gt; 和 &amp; 等内容。

用例 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 命令速查

bash 复制代码
# 根工程测试和 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 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配 :必须把原有公共 HTML 代码真正编译成 ohosArm64 动态库。
  2. N-API 不负责 HTML 业务判断:C++ 只做类型检查、字符串转换和释放,HTML 语义放在 Kotlin。
  3. Native 字符串必须显式释放 :napi_create_string_utf8 完成复制后要立即调用 HtmlFree。
  4. 权限不是 Web 能力 :这个示例不需要网络权限,网页来自本地 loadData;Web 空白要先检查挂载时序。
  5. 首次 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 组件,而是一条完整跨端链路:

text 复制代码
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 三条经验

  1. 先让公共 HTML 在 JVM 和 Native 通过,再接入 ArkUI;
  2. 用 JSON 和少量 C ABI 代替跨语言对象传递;
  3. 把自动测试、HAP 构建和真机 Web 渲染分别记录,避免把"编译成功"误认为"页面可用"。

9.4 适配成果

当前 kotlinx.html OpenHarmony 适配已完成:

  • ohosArm64 KMP 目标和 ARM64 Native 动态库;
  • HtmlExamples 三模板和八项共享检查;
  • Kotlin/Native + C ABI + C++ N-API 桥接;
  • getCatalog、getHtml、renderHtml、runChecks ArkTS API;
  • ArkUI Web 本地 HTML 预览和源码切换;
  • Web 首次加载空白及"需要点击两次"问题修复;
  • 签名 HAP 构建、设备安装和效果图;
  • AtomGit 项目文档和 OpenHarmony 验收记录。

参考文档

相关推荐
小小龙学IT1 小时前
TDengine 开源时序数据库深度解析:从超级表到工业数采落地
开源·时序数据库·tdengine
hasty1 小时前
HTML 已经转义,为何仍有 XSS?Sharp srcdoc 漏洞中的第二次解析
前端·html·xss
李游Leo1 小时前
HarmonyOS 7 + ArkTS + NAPI 学习笔记:Native 模块桥接、CMake 构建与跨语言调用实践【鸿蒙心迹】
笔记·学习·harmonyos
李游Leo2 小时前
《HarmonyOS 7 ArkGraphics 3D 空间设计开发实战》04:材质、纹理与灯光如何决定3D场景质感【鸿蒙心迹】
3d·harmonyos
IMPYLH2 小时前
HTML 的 <tfoot> 元素
前端·javascript·html
李游Leo2 小时前
《HarmonyOS 7 ArkGraphics 3D 空间设计开发实战》07:复杂3D场景的帧率、内存与资源性能优化【鸿蒙心迹】
android·3d·性能优化·harmonyos
分布式存储与RustFS2 小时前
GitLab 对接 RustFS 实战:OIDC 控制台 SSO + 制品存储落 S3 对象存储
运维·云原生·开源·对象存储·分布式存储·s3
SL-staff2 小时前
JVS私有化交付技术解析:如何通过全栈开源与引擎解耦实现真正可控的源码级交付
开源·私有化部署·springboot·信创适配·jvs·spi架构
miofly3 小时前
GitHub 日榜趋势速报 | 2026-09-29
开源·github