开源鸿蒙平台 CMP 三方库「jujubaSVG」适配全流程.md
本文记录
jujubaSVG接入 OpenHarmony 的完整过程,覆盖 WebView 渲染型库的适配路线选型、Compose 源码集隔离、零依赖纯 Kotlin 内核、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI Web 宿主真机页面、HAP 构建和真机验收。
jujubaSVG是 CodandoTV 出品的 Kotlin Multiplatform(+ Flutter)SVG 操作库:把 SVG 文本装进 WebView 渲染,按元素 id 对任意节点做细粒度操作——改背景色、改描边颜色/宽度、删节点、往 SVG 里塞圆形图片、执行任意 JS,同时回调"用户点了哪个节点"及其元素/光标坐标。本次适配与 KStore 的"真编译整库"路线有本质区别:jujubaSVG 的commonMain直接焊在compose-webview-multiplatform、ComposeColor、coroutinesFlow与 kotlinx-serialization 上,渲染引擎本身就是一份 HTML 模板加纯 JS 的 DOM 操作层——因此走的是 cmp_qrose 式"逻辑复用、UI 隔离":命令翻译器、HTML 拼装器与数据模型被改造成零依赖纯 Kotlin 原样编译进.so,Compose UI 归档到不编译的composeMain,鸿蒙侧用 ArkUIWeb组件当新宿主,渲染引擎base_js.js一个字节不改、三处拷贝字节一致,上游七种命令与点击回调语义在真机上完整复刻。
项目地址: AtomGit/oh-tpc/jujubaSVG.git
上游仓库: CodandoTV/jujubaSVG
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
jujubaSVG 为跨平台应用提供"SVG ↔ 交互"这一件事的完整答案:JujubaSVG composable 一行装载,JujubaCommander.execute(Command.UpdateBackgroundColor("BRAC", color)) 一行改色,onElementClick 回调 NodeInfo(id, elementPosition, cursorPosition) 一行拿点击。它的架构分层非常特别:Kotlin 侧真正的"库逻辑"只有命令翻译(Command 密封类 → updateBackgroundColor('BRAC','#ff0000ff'); 这类 JS 片段)和 HTML 拼装(模板占位符替换),而渲染引擎是 base_js.js —— 一份跑在 WebView 里的纯 JS DOM 操作函数集,由宿主平台负责装进 WebView。
OpenHarmony 应用同样需要这套能力,但上游构建(Kotlin 2.4.0 / Gradle 9.3.1 / AGP 9.1.0 / Compose 1.11.1)不会生成 OpenHarmony 变体,且鸿蒙社区 CMP 栈还在 1.9.2。如果只把示例页面改成 ArkTS 而不动 Kotlin 内核,页面能显示,却无法证明命令翻译器在 OpenHarmony ARM64 设备上完成编译、链接和执行,更无法保证它与上游输出逐字节一致。适配需要同时验证命令翻译内核、HTML 拼装器、Native 动态库、C ABI、N-API、ArkUI Web 宿主和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| WebView 宿主不可移植 | commonMain 直接依赖 io.github.kevinnzou:compose-webview-multiplatform:2.0.3(WebView composable、WebViewJsBridge、evaluateJavaScript),该库没有 ohos 目标;JujubaSVG composable 整体焊死在这套 API 上。 |
| 命令模型带 Compose 类型 | Command.UpdateBackgroundColor(id, color: Color) 用的是 androidx.compose.ui.graphics.Color;NodeInfo / NodeCoordinate 挂着 @Immutable @Stable;只要 commonMain 还碰 Compose,ohos 编译就需要整套 Compose ohos 工具链。 |
| 协程与序列化没有 ohos 制品 | 上游 JujubaCommander 用 Channel + Flow(coroutines),点击消息解析用 kotlinx-serialization;官方 kotlinx 系列不发布 linux_ohos 变体。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:命令翻译内核与 HTML 拼装器的 ohos 编译(零第三方依赖)、根构建替换(cmp_qrose 模式)、C ABI/N-API 桥接层和 ArkUI Web 宿主。命令语义完全由上游算法负责——convertToJSCode 的每一行 when 分支都被原样保留,JVM 侧测试用上游同名用例的期望字符串逐字节锁死;ArkTS 只负责 marshal 与展示。
1.2 库提供的能力
Demo 提供一个真实的 SVG 操作会话,对应桥接的全部入口:
- HTML 组装(build):
JujubaBuildHtml把jujuba.html模板、base_js.js、宿主点击 JS 与 Brazil 地图 SVG 拼成完整页面——拼装算法逐行移植自上游 composable 内的LaunchedEffect,连"行拼接不加分隔符"的上游怪癖都原样保留; - 命令执行(execute):
JujubaExecuteCommand接收命令数组,经上游JujubaCommander翻译成 JS 片段返回,ArkTS 拿到后runJavaScript注入 WebView——七种命令全覆盖(节点背景色 / 描边色 / 描边宽度 / 删节点 / 根背景色 / 圆形图片 / 自定义 JS); - 点击解析(parse):
JujubaParseNodeClick把 WebView 回传的点击 JSON 解析成上游NodeInfo结构(节点 id + 元素坐标 + 光标坐标); - 首帧行为:页面加载完成后自动执行一次根背景色命令——与上游 composable 的行为一致。
响应包含:
| 字段 | 作用 |
|---|---|
ok | 操作是否成功,失败时 error 带原文(异常永远不跨 FFI)。 |
html | build 返回组装完成的完整 HTML 文档。 |
js | execute 返回翻译后的 JS 片段,与上游输出逐字节一致。 |
node | parse 返回 NodeInfo 结构(id / elementPosition / cursorPosition)。 |
引擎侧完整能力(七种命令、多命令 \n 连接、CSS #rrggbbaa 颜色输出、addRoundedImage 的 clipPath 圆图、模板占位符替换)全部在 commonMain 里随编译进入 .so,JVM 侧保留 16 项交叉验证用例——其中 8 项的期望字符串逐字取自上游测试文件,证明 ohos .so 里执行的翻译逻辑没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | 命令翻译 when 分支、颜色位运算、HTML 拼装算法、模型字段全部来自上游原文;因 Compose/coroutines 依赖被隔离,仅类型层做了等价替换(Color → JujubaColor、Flow 版 execute → 同步版),新增 HtmlBuilder、JujubaColor 两个纯 Kotlin 文件与桥模块两个文件。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);上游 sampleApp / androidSampleApp / iosSampleApp / build-logic 留盘不进构建。 |
| 桥接稳定 | 使用 3 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 依赖纪律 | commonMain 零第三方依赖——不引 Compose、不引 coroutines、不引 serialization,桥上 JSON 用手写 MiniJson;因此不需要 kotlinx ohos 制品线,工具链用社区已批准的 2.2.21 旧线即可。 |
| UI 完整 | 单页覆盖全部桥接入口与全部七种命令模式、彩虹调色板、描边宽度四档、点击信息回显与页面重置。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit;base_js.js 三处拷贝字节一致并由 CI 强制。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。jujubaSVG 的渲染引擎本来就是跑在 WebView 里的 JS,鸿蒙宿主换成 ArkUI
Web组件后,引擎与资产(base_js.js/jujuba.html/brazil.svg)原样复用——这正是"换宿主不换引擎"的适配面。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点源码依赖边界(Compose 耦合面)、确定"逻辑复用、UI 隔离"路线、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Gradle 降 8.14.1
第 3 阶段:源码集手术 ── composeMain 归档、commonMain 零依赖化(JujubaColor / 同步 Commander / HtmlBuilder)
第 4 阶段:原生桥接 ── @CName × 3 C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主 ── Web 组件加载策略(file://)、javaScriptProxy 点击回传、七模式命令分发
第 6 阶段:示例与验证 ── HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
kotlin/ Gradle 根(rootProject = ohos_jujubaSVG)
kotlin/jujubasvg/src/commonMain/ 零依赖纯 Kotlin 内核(Command / JujubaCommander / HtmlBuilder / JujubaColor / 模型)
kotlin/jujubasvg/src/composeMain/ 归档:上游 Compose API(JujubaSVG composable、rememberJujubaCommander、
OnClickedJSMessageHandler、NodeClicked、Color.toHex),不注册源集 = 不编译
kotlin/jujubasvg/src/androidMain/ 上游 Android @RawRes 重载,随 android target 一并归档
kotlin/jujubasvg/src/commonTest/ 交叉验证测试(16 项,期望值锚定上游)
example/nativeApp/ Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 页面
flutter/ 上游 Flutter 包,不在本次适配范围,留盘不动
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):kotlin/settings.gradle.kts 只 include :jujubasvg 与 :example:nativeApp(后者物理位置在仓库根 example/ 下,用 projectDir 重定向),rootProject.name = "ohos_jujubaSVG"。上游的 compose / vanniktech 发布 / serialization 插件、AGP classpath、build-logic included build 全部随根构建替换剥离——本 fork 不发 Maven Central,Android/iOS 工件继续由上游官方坐标提供。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| 上游版本 | CodandoTV/jujubaSVG 2.0.0(GitHub 上游 tag) |
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven,社区已批准旧线) |
| JDK | 21(Adoptium 21.0.12,JAVA_HOME 注入,不硬编码进 gradle.properties) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm() 保留作交叉验证) |
| Gradle | 8.14.1(wrapper 重钉;2.2.21 旧线必须降 8.x,上游 9.3.1 不参与本 fork 构建) |
| 依赖 | commonMain 零第三方依赖;测试仅 kotlin("test") |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | com.github.codandotv.jujubasvg.ohos |
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
| 文件(commonMain 原状) | 依赖形态 | 处置 |
|---|---|---|
core/JujubaSVG.kt(composable 主体) | kevinnzou WebView + Compose runtime + kermit + coroutines | 归档 → composeMain |
core/Commander.kt(rememberJujubaCommander) | Compose runtime | 归档 → composeMain |
core/bridge/OnClickedJSMessageHandler.kt | kevinnzou IJsMessageHandler + serialization | 归档 → composeMain |
core/bridge/NodeClicked.kt | serialization @Serializable | 归档 → composeMain |
core/ext/CommandExt.kt(Color.toHex()) | Compose Color | 归档 → composeMain(等价逻辑并入 JujubaColor.toCssHex()) |
core/commander/Command.kt | Compose Color(仅类型) | 保留,Color → JujubaColor,文档注释原样 |
core/commander/JujubaCommander.kt | coroutines Channel/Flow(仅包装层) | 保留,when 翻译分支一行不动,外壳改同步返回 |
core/Const.kt / core/ext/InputStreamExt.kt | 纯字符串 / 纯 Kotlin | 原样保留 |
model/NodeInfo.kt / NodeCoordinate.kt | @Immutable @Stable 注解 | 保留,剥掉两行注解 |
composeResources/files/{jujuba.html, base_js.js} | 静态资产 | 原样保留,并字节级拷贝进 HAP rawfile |
全库没有任何 expect/actual——平台差异完全被 kevinnzou WebView 库吸收,这反而成了鸿蒙侧的最大障碍(不可吸收时无接缝可换)。手术的净结果:commonMain 只剩 8 个纯 Kotlin 文件,其中 2 个是新增(JujubaColor、HtmlBuilder),其余 6 个与上游的差异加起来不到 30 行。
1.4 页面边界
标题区 jujubaSVG · OpenHARMONY
Web 区 Brazil 地图(Web 组件,约 330 高)
信息行 节点 ID / 元素坐标 / 光标坐标(JujubaParseNodeClick 的回显)
模式区 七个模式 Chip:节点背景色 / 节点描边色 / 描边宽度 / 删除节点 / 根背景色 / 圆形图片 / 自定义JS
色盘区 七色彩虹圆点(上游 sample 同款调色板,ARGB 十六进制)
宽度区 1/3/5/10px 四档(仅描边宽度模式显示)
操作区 重置页面按钮
状态行 组装/加载/执行全链路状态(分段诊断的关键,见 8.1 第 10 条)
页面启动时先读三个 rawfile 资产、经 JujubaBuildHtml 组装、写入沙箱后以 file:// 加载;加载完成后自动执行一次根背景色命令(对齐上游首帧行为)。
第 2 阶段:目标与依赖打通
2.1 为什么是 2.2.21 旧线而不是 kotlinx 制品线
KStore 适配时为了真编译 coroutines/serialization 引擎被迫升到 Kotlin 2.4.20-0.1.0-07。jujubaSVG 的内核经过源码集手术后零第三方依赖——不需要 kotlinx ohos 制品线,工具链回到社区已批准的 2.2.21-1.0.0 旧线即可,Gradle 相应降 8.14.1(2.2.21 线不支持 Gradle 9.x)。上游源码是 Kotlin 2.4.0 编的,但保留下来的内核只用基础语言特性(sealed class、data class、字符串模板),2.2.21 源码级兼容零成本。
settings.gradle.kts 把 eazytec 仓库写在最前:
pluginManagement {
repositories {
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
gradlePluginPortal()
google()
mavenCentral()
}
}
rootProject.name = "ohos_jujubaSVG"
include(":jujubasvg")
include(":example:nativeApp")
project(":example:nativeApp").projectDir = file("../example/nativeApp")
2.2 编译零依赖内核
jujubasvg/build.gradle.kts 收敛为纯 KMP 模块:
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证用
ohosArm64()
ohosX64()
sourceSets {
commonMain.dependencies {} // 零第三方依赖是硬性纪律
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}
根构建全局禁用 ohos 测试任务(ohos 目标没有测试运行器),版本由 gradle.properties 的 version=2.0.0-ohos.1 驱动。实测:
./gradlew :jujubasvg:compileKotlinOhosArm64 :jujubasvg:compileKotlinOhosX64 # BUILD SUCCESSFUL
gradle.properties 里三个 kotlin.native.* 开关(ignoreDisabledTargets / distribution.downloadFromMaven / apple.deprecated.allowUsingOpenSourceCompiler)缺一不可,第二个让 KN 工具链从 Maven 下载,CI 不依赖 GitHub 发行包。
第 3 阶段:源码集手术与桥协议建模
3.1 手术三处替换
| 上游 | fork | 语义保证 |
|---|---|---|
Command.* 里的 Color | JujubaColor(argb: Int) | 按 Color.toArgb() 同款位打包(alpha 24…31 / red 16…23 / green 8…15 / blue 0…7) |
Color.toHex() | JujubaColor.toCssHex() | 位运算逐行照抄(顺手纠正了上游标反的注释),输出同款 CSS #rrggbbaa |
suspend execute + Flow<String> 收集 | execute(vararg): String 同步返回 | when 翻译分支与 map + reduce 的 \n 连接一字不动;Flow 语义属于 Compose 宿主集成层,随 composable 归档 |
HtmlBuilder.assemble(htmlTemplate, baseJs, svgText, clickJs = Const.JS_CLICK_EVENT) 是从 composable 的 LaunchedEffect 里抽出的纯函数:逐行扫描模板, <!-- svg here --> 行替换为 SVG 文本、 // baseJS here 行替换为 base_js.js + 宿主点击 JS。两个上游怪癖刻意保留:行拼接不加分隔符(整个 HTML 除 JS 自带换行外是一行),以及 clickJs 默认取上游 Const.JS_CLICK_EVENT(window.kmpJsBridge.callNative 协议)——鸿蒙宿主传入自己的 java.jujubaBridge 版本即可,base_js.js 本体不含任何宿主协议,三处拷贝因此可以字节一致。
3.2 JSON 契约
com.github.codandotv.jujubasvg/**(命令翻译 + HtmlBuilder + 模型,零依赖编译)
↓ Command × 7 / #rrggbbaa / 占位符替换
Bridge.kt(@CName × 3,扁平 JSON 进出)
↓ UTF-8 JSON
N-API jujubaBuildHtml() / jujubaExecuteCommand() / jujubaParseNodeClick()
↓
ArkUI Index.ets(Web 宿主 + 七模式分发)
请求与响应:
build : {"htmlTemplate":…,"baseJs":…,"svgText":…,"clickJs":…} → {"ok":true,"html":"<!DOCTYPE html>…"}
exec : {"commands":[{"type":"updateBackgroundColor","id":"BRSP","color":"FFFF0000"},…]}
→ {"ok":true,"js":"updateBackgroundColor('BRSP','#ff0000ff');"}
parse : {"id":"BRSP","elementX":275.0,"elementY":356.7,"cursorX":283.5,"cursorY":362.1}
→ {"ok":true,"node":{"id":"BRSP","elementPosition":{…},"cursorPosition":{…}}}
错误 : {"ok":false,"error":"…"} ← 非法 JSON / 未知 type / 缺字段,全部走同一通道
颜色在桥上用 8 位 ARGB 十六进制字符串("FFFF0000",可带 #),桥内 toLong(16).toInt() 装进 JujubaColor;"FFFF0000" → #ff0000ff 的整条链路与上游 Color(0xFFFF0000).toHex() 输出一致,由 JVM 测试双向锚定。
3.3 错误通道
每个导出整体 try/catch,Kotlin 异常永远不跨 FFI:
@CName("JujubaBuildHtml")
fun jujubaBuildHtml(request: String): String = try {
/* 模板拼装 */
} catch (t: Throwable) {
"""{"ok":false,"error":${MiniJson.writeString(t.message ?: t.toString())}}"""
}
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libjujubasvg_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohosjujubasvg.so)
│ JujubaBuildHtml / JujubaExecuteCommand / JujubaParseNodeClick
▼
UTF-8 JSON + DisposeString
4.2 Kotlin/Native 导出函数
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
JujubaBuildHtml | jujubaBuildHtml(req) | HtmlBuilder.assemble 模板拼装 |
JujubaExecuteCommand | jujubaExecuteCommand(req) | JujubaCommander.execute 七种命令翻译(多命令 \n 连接) |
JujubaParseNodeClick | jujubaParseNodeClick(msg) | 点击 JSON → NodeInfo(id, elementPosition, cursorPosition) |
桥上的 JSON 解析用手写 MiniJson(约 200 行递归下降:对象/数组/字符串转义/数字/字面量),不引 kotlinx-serialization——与 KStore 用制品线真编译 serialization 的选择不同,根源在于本库内核根本不需要 JSON 引擎,桥协议又是扁平结构,手写解析器的体积收益大于维护成本。命令映射按 type 分发到 Command 子类后复用上游翻译器,ArkTS 侧拿到的 js 字段就是上游会在 WebView 里执行的同一段字符串。
4.3 C++ N-API 方法分发
三个导出共用一个 WrapKnCall 包装——参数校验(必须是 string)、OH_LOG_INFO 打字节数、调 KN 函数、napi_create_string_utf8、释放:
static napi_value WrapKnCall(napi_env env, napi_callback_info info,
const char *name, const char *(*fn)(const char *)) {
/* … */
const char *raw = fn(request.c_str());
napi_value out = nullptr;
napi_status st = napi_create_string_utf8(env, raw, NAPI_AUTO_LENGTH, &out);
DisposeKnString(raw); // KN 分配的字符串必须经 CAdapter 归还,绝不 free()
return out;
}
头文件 libohosjujubasvg_api.h 直接采用 CAdapter 生成物(linkDebugSharedOhosArm64 产物目录里自带,含 libohosjujubasvg_symbols 与三个业务符号声明),比手写最小头更不容易与 baseName 漂移。
4.4 CMake 和类型声明
set(KN_SO_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../libs/${OHOS_ARCH})
add_library(ohosjujubasvg SHARED IMPORTED)
set_target_properties(ohosjujubasvg PROPERTIES IMPORTED_LOCATION ${KN_SO} IMPORTED_NO_SONAME TRUE)
add_library(jujubasvg_napi SHARED napi_init.cpp)
target_link_libraries(jujubasvg_napi PUBLIC libace_napi.z.so libhilog_ndk.z.so ohosjujubasvg)
OHOS_ARCH 为空或 KN so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。Gradle 侧 linkDebugSharedOhos* 任务 finalizedBy 两个 Copy 任务,把 libohosjujubasvg.so 与配套 libc++_shared.so(从 ~/.konan/dependencies/*/lib/{aarch64,x86_64}-linux-ohos/ 定位)自动回填 entry/libs/<abi>/。
export const jujubaBuildHtml: (request: string) => string;
export const jujubaExecuteCommand: (request: string) => string;
export const jujubaParseNodeClick: (request: string) => string;
第 5 阶段:ArkUI 宿主(本适配最大的实战课)
5.1 加载策略:三次白屏换来的 file:// 正解
上游 Android/iOS 由 WebView 库把 HTML 字符串灌进 WebView,鸿蒙侧的等价操作踩了三层坑(详见 8.1):
尝试 1 Web({src:'about:blank'}) + onControllerAttached 里 loadData() → 白屏(时序竞争)
尝试 2 Web({src: this.html})(HTML 字符串当 src)+ loadData 兜底 → 白屏(被当 URL 导航出错误页)
尝试 3 about:blank 跳板 + onPageEnd 后 loadData(让内容成为最后一次导航)→ 状态停在"页面开始加载…"(265KB 的 loadData 挂起)
正解 组装结果写入 cacheDir/jujuba.html,Web({src: 'file://…'}) 直接加载 → 一次成功
const file = fileIo.openSync(cachePath,
fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE | fileIo.OpenMode.TRUNC);
fileIo.writeSync(file.fd, this.html);
fileIo.closeSync(file);
// …
Web({ src: `file://${cachePath}`, controller: this.controller })
.fileAccess(true)
.onPageEnd(() => this.runCommand({ type: 'updateRootBackgroundColor', color: 'FFFFB700' }))
状态行在整个排查过程中充当了分段诊断器:HTML 已组装 (N KB) → 页面开始加载… → 命令已执行 分别锚定桥、导航、执行三段,每轮真机截图直接读出卡点。
5.2 点击回传与命令分发
.javaScriptProxy({
object: this.proxy, // class JujubaJsProxy { onElementClicked(msg) {…} }
name: 'jujubaBridge',
methodList: ['onElementClicked'],
controller: this.controller
})
注入的 clickJs 与上游 Const.JS_CLICK_EVENT 同构,回调目标换成 window.jujubaBridge.onElementClicked(JSON.stringify({id, elementX, elementY, cursorX, cursorY}))。收到消息后先过 JujubaParseNodeClick 回显三行坐标,再按当前模式构造命令:
private handleElementClicked(msg: string): void {
const node = (JSON.parse(jujubaParseNodeClick(msg)) as BridgeResponse).node;
this.lastNodeId = node.id; // 例如 BRSP(圣保罗州)
switch (this.mode) {
case MODE_BG:
this.runCommand({ type: 'updateBackgroundColor', id: node.id, color: COLORS[this.colorIndex] });
break;
/* …其余六种模式… */
}
}
private runCommand(command: CommandRequest): void {
const res = JSON.parse(jujubaExecuteCommand(JSON.stringify({ commands: [command] }))) as BridgeResponse;
if (res.ok) { this.controller.runJavaScript(res.js, () => { this.status = '命令已执行'; }); }
}
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/ (Gradle 模块,projectDir 重定向到仓库根)
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohosjujubasvg)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/
│ ├── Bridge.kt @CName × 3 + 命令映射
│ └── MiniJson.kt 手写 JSON 解析/转义
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName com.github.codandotv.jujubasvg.ohos、versionName 2.0.0-ohos.1
└── entry/
├── build-profile.json5 externalNativeOptions + abiFilters [arm64-v8a, x86_64]
├── libs/{arm64-v8a,x86_64}/ linkDebugShared* 经 copyKnSo* 回填的三件套(gitignore)
├── src/main/cpp/ CMakeLists.txt、napi_init.cpp、include/、types/libjujubasvg_napi/
├── src/main/ets/pages/Index.ets ArkUI 验收页面
└── src/main/resources/rawfile/ base_js.js / jujuba.html / brazil.svg(与 Kotlin 资产字节一致)
6.2 构建、签名和安装
./gradlew :jujubasvg:jvmTest # 16 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书材料不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组)。
hdc list targets -v
hdc -t <设备序列号> install -r \
entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <设备序列号> shell aa start \
-a EntryAbility -b com.github.codandotv.jujubasvg.ohos -m entry
四、完整代码对照
4.1 整体架构
com.github.codandotv.jujubasvg/**(上游内核,零依赖改造后原样编译)
│ Command × 7 / JujubaCommander 翻译 / JujubaColor(#rrggbbaa) / HtmlBuilder / NodeInfo
Bridge.kt + MiniJson(example/nativeApp)
│ JujubaBuildHtml / JujubaExecuteCommand / JujubaParseNodeClick
libjujubasvg_napi.so
│ jujubaBuildHtml / jujubaExecuteCommand / jujubaParseNodeClick
Index.ets
│ rawfile 资产 + file:// 加载 + javaScriptProxy + runJavaScript
▼
ArkUI Web 宿主(base_js.js 引擎原样运行,七命令 × Brazil 地图)
4.2 文件清单
| 文件 | 职责 |
|---|---|
kotlin/settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_jujubaSVG、nativeApp projectDir 重定向 |
kotlin/gradle/libs.versions.toml | 只剩 Kotlin 2.2.21-1.0.0 + multiplatform 插件一个别名 |
kotlin/build.gradle.kts(根) | 版本 2.0.0-ohos.1、全局禁用 Ohos*Test* 任务 |
jujubasvg/build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64,commonMain 零依赖 |
jujubasvg/.../model/JujubaColor.kt | 新增:ARGB 数据类 + toCssHex()(上游 toHex 位运算照抄) |
jujubasvg/.../core/HtmlBuilder.kt | 新增:模板占位符拼装(自 composable 抽出,怪癖保留) |
jujubasvg/src/composeMain/** | 归档的上游 Compose API(不编译) |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt / MiniJson.kt | 3 个 C ABI、命令映射、手写 JSON |
harmonyApp/.../cpp/napi_init.cpp | N-API 导出(WrapKnCall 共用包装)、DisposeString |
harmonyApp/.../cpp/CMakeLists.txt | IMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套 |
harmonyApp/.../ets/pages/Index.ets | ArkUI 验收页面(file:// 加载、七模式分发) |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(24 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 内核 | JujubaCommander.execute(vararg Command): String | 命令 → JS 片段,多命令 \n 连接(上游 when 原文) |
| 内核 | JujubaColor(argb).toCssHex() | ARGB → CSS #rrggbbaa(与上游 Color.toHex 逐字节一致) |
| 内核 | HtmlBuilder.assemble(…) | 模板 + baseJs + SVG + 宿主点击 JS → 完整 HTML |
| 内核 | NodeInfo / NodeCoordinate | 点击回调载荷模型 |
| Native | JujubaBuildHtml / JujubaExecuteCommand / JujubaParseNodeClick | C ABI JSON 导出 × 3 |
| N-API | jujubaBuildHtml(req) 等 | 向 ArkTS 暴露能力 |
| ArkTS | runCommand(cmd) | marshal → 桥 → runJavaScript 注入 |
4.4 ArkTS 与 Kotlin 的边界
const res = JSON.parse(jujubaExecuteCommand(JSON.stringify({
commands: [{ type: 'updateBackgroundColor', id: 'BRSP', color: 'FFFF0000' }]
}))) as BridgeResponse;
this.controller.runJavaScript(res.js, () => {});
// res.js === "updateBackgroundColor('BRSP','#ff0000ff');" ← 与上游输出逐字节一致
// Bridge.kt 内部:复用上游翻译器,桥不自己拼 JS
"updateBackgroundColor" -> Command.UpdateBackgroundColor(obj.string("id"), obj.color("color"))
val js = JujubaCommander().execute(*commands.toTypedArray())
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:逻辑复用而不是整库真编译
KStore 真编译整库的前提是"引擎纯 Kotlin 且依赖有 ohos 制品"。jujubaSVG 的公共 API 面一半是 Compose composable、渲染宿主焊在 kevinnzou WebView 上、引擎本体是 JS——任何"整库真编译"都要先解决 Compose ohos 工具链 1.9.2 与上游 1.11.1 的代差。而它的可复用内核天然存在且极小(命令翻译 + 拼装 + 模型),渲染引擎又是平台无关的 JS。抽内核不是妥协,是这座库的正确切面;Compose API 归档在 composeMain,等社区 CMP 对齐后可以原样接回。
决策 2:工具链回到 2.2.21 旧线,Gradle 降 8.14.1
内核零依赖后不需要 kotlinx 制品线,工具链不必跟着依赖走,直接采用社区已批准的 2.2.21-1.0.0(qrose / haze 同款)。代价是 Gradle 从上游 9.3.1 降到 8.14.1——2.2.21 线不支持 Gradle 9.x,两条工具链线的经验不能互相套用。JDK 21 用 JAVA_HOME 注入而不是硬编码 org.gradle.java.home,CI 与本机都免路径踩坑。
决策 3:commonMain 零依赖纪律
不引 coroutines(execute 同步化)、不引 serialization(MiniJson 手写)、不引 Compose(JujubaColor 替代 Color)。收益有三:ohos 编译零制品风险、.so 体积最小、上游翻译算法的每一行都能原样保留。桥协议是扁平 JSON,手写解析器的复杂度可控(200 行,含完整转义处理)。
决策 4:三入口桥 + 输出逐字节一致
桥不自己拼接任何 JS:execute 入口把 JSON 映射成 Command 对象后调用上游 JujubaCommander,js 字段就是上游会在 Android WebView 里执行的同一字符串。JVM 测试用上游同名用例的期望字符串逐字锚定(8 条命令翻译 + 4 个颜色锚点 + 1 个 sample 底色 0xffffb700 → #ffb700ff),任何"顺手优化"导致的输出漂移都会被测试拦下。
决策 5:base_js.js 三拷贝不变量
上游 CI 原本 diff 两份 base_js.js(Flutter / Kotlin)。本 fork 增加 HAP rawfile 第三份,CI 升级为三方 diff——引擎只有一份,宿主协议(kmpJsBridge / JujubaSVGChannel / jujubaBridge)全部放在各宿主自己的 clickJs 参数里,从结构上保证"换宿主不换引擎"。
六、测试与验证
6.1 测试环境
本次验证使用 Windows(x86_64)、JDK 21(Adoptium 21.0.12)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。
6.2 JVM 交叉验证
./gradlew :jujubasvg:jvmTest
16 项用例全部通过:
| 测试类 | 用例数 | 钉住什么 |
|---|---|---|
JujubaCommanderTest | 8 | 七种命令的期望 JS 逐字取自上游测试(updateBackgroundColor('n1','#ffffffff'); 等),外加多命令 \n 连接 |
JujubaColorTest | 5 | #ffffffff / #000000ff / #ffffff00 / 上游 sample 底色 #ffb700ff / of() 打包 |
HtmlBuilderTest | 3 | 合成小模板的精确输出(锁死"无分隔符拼接"怪癖)+ 真实资产结构校验 + 默认 clickJs 取上游常量 |
与 KStore"上游测试文件零改动"不同,本库因 API 外壳同步化(Flow → 同步返回)重写了测试外壳,但全部期望字符串逐字保留——锚定的是输出而不是测试代码本身。
6.3 HAP 验证
:jujubasvg:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL(so 自动回填 entry/libs/)
llvm-nm -D: JujubaBuildHtml / JujubaExecuteCommand / JujubaParseNodeClick / libohosjujubasvg_symbols 均为全局 T 符号
BuildNativeWithCmake: finished(双 ABI 编出 libjujubasvg_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
KN so arm64 约 3.7MB、x86_64 约 3.4MB(debugShared,含 Kotlin/Native 运行时);每个 ABI 目录三件套(libjujubasvg_napi.so / libohosjujubasvg.so / libc++_shared.so)齐全。
6.4 功能用例
- 启动后状态行走完
HTML 已组装 (N KB)→页面开始加载…→命令已执行,Web 区显示琥珀色根背景 + 绿色 Brazil 地图(首帧根背景命令生效,证明 HTML 组装、file:// 加载、命令翻译、runJavaScript 全链路贯通); - 点击圣保罗州(
BRSP),信息行回显节点 ID BRSP、元素坐标 (275.0, 356.7)、光标坐标 (283.5, 362.1)——javaScriptProxy→JujubaParseNodeClick→NodeInfo回传链路成立; - 默认"节点背景色"模式 + 红色色盘,点击后该州即被染红——
updateBackgroundColor('BRSP','#ff0000ff');在 ArkWeb 里执行成功; - 切换"节点描边色 / 描边宽度(1/3/5/10px)/ 删除节点",逐一点击验证;
- "根背景色"模式点击地图任意处,整页底色变为所选色;
- "圆形图片"模式点击某州,在其坐标处插入 100×100 的 imgur 圆图(clipPath 裁剪,
base_js.js原生行为); - "自定义JS"模式验证
CustomCommand透传;"重置页面"重新加载并重放首帧根背景色; - 三个 rawfile 资产与 Kotlin 侧源头资产
diff为空(字节一致)。
6.5 验证结论
命令翻译内核、颜色协议、HTML 拼装器、Native 动态库、C ABI、N-API、ArkUI Web 宿主和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的 SVG 操作会话:地图渲染、点击解析、七种命令与坐标回显闭环(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/jujubaSVG.git
cd jujubaSVG/kotlin
export JAVA_HOME="/path/to/jdk-21"
./gradlew :jujubasvg:jvmTest # 16 项交叉验证
./gradlew :jujubasvg:compileKotlinOhosArm64 :jujubasvg:compileKotlinOhosX64 # klib
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd ../example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b com.github.codandotv.jujubasvg.ohos -m entry
hdc shell hilog | grep Jujuba
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:jujubasvg真正编译成ohosArm64/ohosX64动态库——但"整库真编译"也不是唯一答案,渲染宿主不可移植时,正确切面是"内核复用、宿主隔离"; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- eazytec Kotlin 2.2.21 线对
@JvmInline的处理两端不一致:JVM 端强制要求(Value classes without '@JvmInline' annotation are not yet supported)、ohos 端又Unresolved reference 'JvmInline'——common 代码里的 value class 直接退回普通 data class; when(c)里写c.code == 0x0C ->会报Logical expression in when-with-subject,改无 subject 的when { … }即可;- ArkWeb 的 HTML 字符串 src 陷阱:把组装好的 HTML 直接当
Web({src}),部分版本会把它当 URL 去导航,得到一个白屏错误页——onPageEnd照常触发、runJavaScript照常"成功",极具迷惑性;判别方法是首帧命令若把 body 染色,白屏即说明当前页不是你的内容; - ArkWeb 的 loadData 大 payload 陷阱:约 265KB 的 HTML 用
loadData加载,onPageBegin触发后onPageEnd永不到来,页面永久白屏——正解是写入cacheDir后以file://URL 加载(真实 URL、单次导航、无时序竞争); about:blank跳板 +onControllerAttached里loadData的时序在部分设备上会被初始导航覆盖——让内容成为"最后一次导航"(跳板onPageEnd后再 loadData)可解,但大 payload 下仍受第 6 条制约;- HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++_shared.so)缺一不可; - KN 返回字符串必须经 CAdapter 的
DisposeString归还运行时,N-API 层不能free(); - 白屏类问题的最快诊断法:给状态行做分段状态机(组装→导航→执行各一个状态文本),一轮真机截图就能锁定断点,比反复接 hilog 快得多;
example/放在仓库根而 Gradle 根在kotlin/子目录时,用project(":example:nativeApp").projectDir = file("../example/nativeApp")重定向即可,桥模块构建脚本里引用 HAP 工程目录要记得../前缀;- 上游模块(sampleApp / androidSampleApp / build-logic)引用 AGP 9.1 与 Compose 插件,留在
settings.gradle.kts里 include 会在配置期爆炸——留盘不 include 是唯一稳妥处置。
8.2 已知问题
- 本 fork 的 Kotlin 构建不再产出 Android/iOS 工件(上游模块归档不编译);Maven Central 坐标
io.github.codandotv:jujubaSVG仍指上游官方发布; - Compose API(
JujubaSVGcomposable、rememberJujubaCommander、Flow 版 commander)在本 fork 中不编译,仅源码存档于composeMain,等社区 CMP-ohos 与 Kotlin 2.4 / Compose 1.11 对齐后接回; JujubaColor为普通 data class(@JvmInline两端不一致所迫),有轻微装箱开销,桥场景可忽略;- Demo 的圆形图片走 https(imgur),需
ohos.permission.INTERNET,离线环境该模式无图; - Flutter 包(
flutter/jujuba_svg)未适配 ohos,保持上游 Android/iOS 形态; - HAP 安装必须使用与 bundle name
com.github.codandotv.jujubasvg.ohos匹配的签名。
8.3 未来优化方向
- 评估
.har形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套; - 社区 CMP-ohos 工具链对齐后把
composeMain接回,恢复 Compose 直用形态; - 在 CI 中加入 ohos 编译(已上线)、ELF 符号检查与 HAP 构建;
- 补充真机(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本;
- 桥增加批量会话入口(一次请求携带 SVG + 多命令,减少 FFI 往返)。
九、总结
9.1 核心难点回顾
上游 jujubaSVG commonMain(Compose WebView 宿主 + Color + Flow + serialization)
→ 源码依赖度盘点,切出"命令翻译 + HTML 拼装 + 模型"零依赖内核
→ 工具链钉 Kotlin 2.2.21-1.0.0(Gradle 降 8.14.1,eazytec 双仓置前)
→ 根构建替换(cmp_qrose 模式,上游 Compose/AGP 模块留盘)
→ composeMain 归档 / JujubaColor / 同步 Commander / HtmlBuilder
→ ohosArm64 / ohosX64 KLIB + 双 ABI 动态库
→ C ABI(@CName × 3)+ 手写 MiniJson
→ C++ N-API(WrapKnCall 共用包装 + DisposeString 纪律)
→ ArkTS file:// 加载(三次白屏排查)+ javaScriptProxy 点击回传
→ ArkUI Web 真机验收(七命令 × Brazil 地图 × 坐标回显)
9.2 封装层次
- 内核层:上游原版命令翻译
when分支、#rrggbbaa颜色位运算、模板拼装算法与模型; - Native 层:生成双 ABI 动态库并通过 3 个 C ABI 输出 JSON;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:rawfile 资产注入、file:// 页面加载、点击回传解析、七模式命令分发;
- 引擎层:
base_js.js原样运行于 ArkWeb,宿主协议收敛在clickJs参数; - DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 适配路线由库的架构决定,而不是由社区先例决定:KStore 的引擎是纯 Kotlin 且依赖有制品线,正确答案是真编译整库;jujubaSVG 的引擎是 JS、宿主焊在 Compose 上,正确答案是内核复用加宿主替换。同一个 cmp_qrose 模式,落在两座库上是两种完全不同的手术面;
- 逐字节交叉验证是移植正确性的唯一锚点:把上游测试的期望字符串原封不动搬进 fork 的测试,让"翻译器没有漂移"成为可执行断言而非口头承诺——颜色格式、多命令连接符、无分隔符拼接这些"怪癖"全部被测试锁死;
- 给真机问题装分段诊断:ArkWeb 白屏三连(src 时序 / URL 误判 / loadData 挂起)的排查之所以两轮收敛,靠的是状态行的分段状态机——组装、导航、执行各有独立状态文本,一张截图就能定位断点。
9.4 适配成果
当前适配已完成:
- jujubaSVG Kotlin
2.0.0命令翻译内核的双 ABI Kotlin/Native 编译(commonMain零第三方依赖,上游when翻译分支逐行保留); - 3 个 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证全局 T 符号,CAdapter 生成头直接复用); - DevEco Stage ArkUI Web 验收页面(file:// 加载、javaScriptProxy 点击回传、七种命令模式、彩虹调色板、坐标回显、页面重置);
- 16 项 JVM 交叉验证全绿(期望字符串逐字取自上游)+ HAP 构建(双 ABI 六件套 so)、签名、设备安装和真机验证;
base_js.js三拷贝不变量与 CI 三方 diff、AtomGit 项目文档、OpenHarmony README(中英双份)和真机效果图。
适配过程 FAQ
以下问题来自
ohosArm64目标接入、NAPI 桥接、HAP 构建与真机验收的实际流程,按出现频率排序。
Q1:DevEco Studio Sync 失败或 ohpm 依赖下载超时,怎么处理?
- 确认 DevEco Studio 版本与工程
build-profile.json5的compatibleSdkVersion一致; - 在
File > Settings > Build, Execution, Deployment > Ohpm配置可用的 ohpm 仓库镜像或代理; - 删除
oh_modules、oh-package-lock.json5与.hvigor缓存后重新 Sync; - 公司网络需要代理时,同时给 ohpm 与 hvigor 配置
http.proxy。
Q2:CMake 报 Missing .../libxxx.so 或找不到 Kotlin/Native 动态库?
- 先在
example目录执行./gradlew :nativeApp:linkDebugSharedOhosArm64(模拟器用linkDebugSharedOhosX64); - 确认
libxxx.so与libc++_shared.so已复制到ohos/entry/libs/<abi>/; - 检查
entry/build-profile.json5的abiFilters与设备 ABI 是否一致(真机arm64-v8a,模拟器x86_64); - 清理
entry/.cxx与entry/build后重试。
Q3:ArkTS 报 Cannot find module 'libxxx_napi.so' 或调用返回 undefined?
- 确认 CMake 的
add_library(<target> SHARED ...)名称与import ... from 'libxxx_napi.so'完全一致; - 解包 HAP,确认
libs/<abi>/libxxx_napi.so已打包; - 删除
entry/build、entry/.cxx与oh_modules后重新构建; - 检查
module.json5的srcEntry与页面路径是否正确。
Q4:真机安装报 signature verification failed 或 install failed?
- 在 DevEco Studio 的
Signing Configs配置证书、Profile 与密码; bundleName必须与签名 profile 中的 bundle name 一致;- 设备需加入 profile 的
deviceIds,调试证书有效期需覆盖当前时间; - 用
hdc list targets确认设备在线,再执行hdc install -r xxx-signed.hap。
Q5:应用启动白屏、闪退或 NAPI 调用崩溃?
- 用
hdc hilog | grep -i "napi\|kotlin"查看原生日志; - 确认
libc++_shared.so与 Kotlin/Native 工具链版本匹配,且 ABI 与设备一致; - 检查 C ABI 返回的字符串是否由 Native 侧分配、ArkTS 侧复制后是否调用
XxxFree释放; - 在 DevEco 的 Native 调试配置中附加
lldb,定位空指针与越界访问。
Q6:Gradle 报 ohosArm64 目标不存在或 Kotlin/Native 编译失败?
- 使用鸿蒙发行版 Kotlin(如
2.2.21-1.0.0),并在settings.gradle.kts配置 eazytec 仓库; - Gradle 建议 8.x、JDK 21,版本不匹配时先统一工具链;
- 确认
kotlin { ohosArm64() }已应用,且local.properties的 SDK 路径有效; - 清理
~/.gradle/caches中对应版本的 Kotlin/Native 缓存后重试。
Q7:HAP 构建报 SDK 版本不匹配或 compatibleSdkVersion 错误?
- 对齐 DevEco Studio、HarmonyOS SDK 与
build-profile.json5中的compatibleSdkVersion; - 升级 DevEco Studio 后同步升级 hvigor 与 SDK,避免
modelVersion与 IDE 不匹配; - 命令行构建时设置
DEVECO_SDK_HOME指向 DevEco 的sdk目录。
Q8:命令行 hvigor 构建失败,但 DevEco 里能构建?
- 使用 DevEco 自带的 node 与 hvigor:
"$DEVECO/Contents/tools/node/bin/node" "$DEVECO/Contents/tools/hvigor/bin/hvigorw.js" ...; - 设置
JAVA_HOME、NODE_HOME、DEVECO_SDK_HOME; - 加上
--no-daemon排除守护进程缓存影响,并清理entry/build与.hvigor。
参考文档
更多推荐


所有评论(0)