本文记录 Swiss Knife 接入 OpenHarmony 的完整过程,覆盖对话框组件库盘点、Compose 依赖度定界、纯 Kotlin 决策内核抽取、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机双 Tab 页面、HAP 构建和真机验收。

Swiss Knife 是基于 Compose Multiplatform 的 UI 工具库:SKAlert 提供 scrim 遮罩 + 内容完全自定义的警告框(显隐由调用方 visible 驱动、onHide(reason) 区分 Back / TouchOutside、onHidden 在退场动画结束后回调、内容实时收到 animationProgress);SKBottomSheet 提供底部弹窗(滑入滑出、拖拽关闭、hideOnDragThreshold 阈值判定、嵌套滚动联动、scrim 透明度跟随弹窗位置)。本次适配与 KStore 的"真编译整库"路线不同:全库只有 Compose 渲染代码,而鸿蒙工具链没有兼容的 Compose 发行版,因此走 cmp_qrose / AestheticDialogs / compose-material-dialogs 的同款路线——把两个组件的决策层(拖拽状态机、嵌套滚动消费规则、隐藏判定、系统栏解析、动效参数解析)抽成纯 Kotlin 内核编进 .so,Compose 渲染层原样隔离留盘,ArkUI 页面按内核 spec 用原生组件重绘,所有设计值全部来自 Native 调用。

项目地址: AtomGit/oh-tpc/swiss-knife.git

上游仓库: ilyadreamix/swiss-knife

上游基线:ilyadreamix/swiss-knife 1.0.11(本 fork 1.0.11-ohos.1)。

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配

Swiss Knife 为 Compose Multiplatform 应用解决"对话框与弹窗"这件事:SKAlert 一行声明 scrim + 居中卡片,隐藏原因(Back / TouchOutside)作为枚举回调给调用方,由调用方决定何时翻转 visible;SKBottomSheet 在此之上加了拖拽关闭——totalDrag 累计、progress = 1 - max(0, totalDrag / height) 跟手、松手时 dragFraction >= threshold(默认 0.4)判走否则回弹,内部列表滚到顶后继续下拉的剩余位移交给弹窗(onPostScroll)、弹窗位移后上滑先弹回(onPreScroll)、拖拽进行中的 fling 直接转拖拽终判(onPreFling)。平台的唯一缝隙是 expect fun SKDialogHost(...):Android 用原生 Dialog + 独立 ComposeView,iOS / jvm 用 Compose Dialog 全屏参数组合,系统栏图标明暗分别走 WindowInsetsControllerCompat / UIApplication.setStatusBarStyle。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面共享。OpenHarmony 应用同样需要这套对话框能力,但上游构建(Kotlin 2.3.21 / Gradle 9.5.0 / Compose 1.11.1 / AGP 9.2.1)不会生成 OpenHarmony 变体。

如果只把示例页面改成 ArkTS,页面可以画出弹窗,却无法证明拖拽阈值判定、嵌套滚动消费规则、隐藏原因仲裁这些库的语义在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证决策内核、动效参数解析、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
工具链不一致上游 Kotlin 2.3.21 / Gradle 9.5.0 与鸿蒙工具链无法共存于一个 Gradle 构建;AGP 9.2.1 的 com.android.kotlin.multiplatform.library 在鸿蒙 Kotlin 上解析不了。
Compose 依赖全库 825 行 Kotlin 全部 import androidx.compose.*,而鸿蒙 Kotlin 2.2.21-1.0.0 工具链没有任何 Compose 发行版——逐行编译过不去。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
手势语义跨不了桥Animatable、NestedScrollConnection、手势仲裁是 Compose 侧概念,C ABI 只能同步一来一回。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:决策内核的 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。Compose 渲染完全由隔离留盘的上游原版实现负责,ArkTS 只负责按 spec 渲染、调 Native、解析 JSON 和展示结果。

1.2 库提供的能力

Demo 提供两个真实组件会话,对应桥接的全部入口:

  • SKAlert:alert.spec 一次性解析渲染/行为 spec(scrim 0x80000000 即上游 Color.Black.copy(alpha = 0.5f)、对齐 Center、双栏 Light 图标、动效参数),运行时 alert.back / alert.touchOutside 判定能否关闭并返回 reason;ArkTS 侧 scrim 透明度 = spec alpha × animationProgress(上游 copy(alpha = a * progress) 公式),内容卡片收到 progress 做同步缩放淡入;
  • SKBottomSheet:sheet.spec 解析容器/隐藏选项/阈值校验(require(threshold in 0f..1f) 逐字保留上游报错文案),拖拽条手势逐帧喂 sheet.drag.move(snapTo 跟手)、松手喂 sheet.drag.end 拿 shouldHide 判定;内层列表 onScrollFrameBegin 接 sheet.scroll.pre / sheet.scroll.post 复刻嵌套滚动消费规则,onScrollStop 接 sheet.fling.pre;
  • 隐藏三通道:Back(页面 onBackPress → 内核仲裁 consume)、TouchOutside(scrim 点击 → 内核仲裁)、Caller(调用方直接翻转 visible——上游"调用方驱动"契约本身);
  • onHidden 闭环:退场动画结束后回调(tween 按 duration+delay 精确延时,spring 用 480ms 观察窗近似);
  • 版本握手:ping 返回 fork 版本号 1.0.11-ohos.1 与目标列表,来自真实 Native 调用而非静态文字。

引擎侧完整语义(SKAlertState / SKBottomSheetState 拖拽状态机、SKBottomSheetNestedScrollConnection 消费规则、SKDialogHostSystemUIOptions 解析表)全部在内核 engine/ 包里以纯函数形式进入 .so,JVM 侧保留 18 项 jvmTest 交叉验证用例,证明 ohos .so 里执行的判定逻辑没有漂移。

1.3 实现适配

维度要求
语义复用决策逻辑从上游 SKAlertState / SKBottomSheetState / SKBottomSheetNestedScrollConnection 逐字移植(数值、缺省值、报错文案全保留);Compose 源码零改动整体隔离。
平台目标根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,core / dialogs 出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);上游 examples/{core,android,ios} 与 AGP/Compose/AndroidX 坐标留盘不进构建。
桥接稳定单个 C ABI SwissKnifeBridge + UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。
状态纪律弹窗运行时状态(progress / totalDrag / height)由 ArkTS 持有,内核全部纯函数——C ABI 无会话、无锁。
UI 完整双 Tab 覆盖全部桥接入口:配置开关、阈值/圆角/scrim 滑杆、双组件全行为面、协议日志和版本握手。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。Swiss Knife 的本质输出是对话框的行为决策而非像素,鸿蒙侧由 ArkTS 按 spec 用原生组件重绘渲染面,渲染值全部来自 .so。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点模块与 Compose 依赖度、确定"决策内核"切面、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、composeMain 隔离
第 3 阶段:桥协议建模     ── 14 op 扁平 JSON 契约、状态归属、错误通道
第 4 阶段:原生桥接       ── @CName C ABI、C++ N-API、参数校验和 DisposeString
第 5 阶段:ArkUI 宿主     ── 双 Tab、手势/滚动接入、协议日志和版本握手
第 6 阶段:示例与验证     ── HAP、签名、设备安装、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
core/                                 上游基础模块(进 ohos 构建:SKInsets 纯值移植 + jvm/ohos 双目标)
core/src/composeMain/                 上游 SKInsets(Dp 版) / AnimatableExtensions(隔离,不编译)
dialogs/                              决策内核(进 ohos 构建:SKDialogHostSystemUIOptions + engine/ 五件)
dialogs/src/composeMain/              上游 SKAlert / SKBottomSheet / SKDialogHost actual 全量(隔离,不编译)
dialogs/src/jvmTest/                  18 项交叉验证用例
example/nativeApp/                    Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/                   DevEco Stage、CMake N-API 和 ArkUI 双 Tab 页面
examples/{core,android,ios} docs/     上游示例与文档资产,留盘不进 settings
README.OpenHarmony.md / _CN.md        鸿蒙适配说明(中英双份)
README.upstream.md                    上游原文保存

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :core、:dialogs 与 :example:nativeApp,rootProject.name = "ohos_swiss-knife"。上游的 AGP / Compose / AndroidX / maven-publish 坐标全部随根构建替换剥离——本 fork 不发 Maven Central,上游消费者继续走 JitPack(jitpack.yml 原样保留)。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDK21(org.gradle.java.home 指向 Temurin jdk-21.0.12.101-hotspot)
Gradle8.14.1(wrapper 从上游 9.5.0 钉回,与社区已审核组合一致)
OpenHarmony 目标ohosArm64 + ohosX64(jvm() 保留作交叉验证)
产物libohosswissknife.so(sharedLib,仅桥模块开启)+ libswissknife_napi.so(CMake)
DemobundleName org.terminator.ohos.swissknife,compatibleSdkVersion 5.1.0(18),双 ABI
1.3 源码依赖度盘点

全库 825 行 Kotlin 的 Compose 依赖度普查结论:

源文件依赖面处置
SKInsets.kt(core)WindowInsets / Modifier.padding(构造扩展部分)纯值数据类抽回 commonMain(Dp→Double),原文件进 composeMain
AnimatableExtensions.kt(core)androidx.compose.animation.core.*整文件进 composeMain
SKDialogHost.kt(dialogs)@Composable expect整文件进 composeMain;绑在文件里的纯类型 SKDialogHostSystemUIOptions 抽回 commonMain(cmp_qrose 对 Code128Type 的同款规则)
SKAlert.kt / SKAlertState.ktCompose UI + Animatable整文件进 composeMain;决策面(隐藏规则、scrim、对齐、动效解析)移植进 SKAlertEngine
SKBottomSheet*.kt(4 个文件)Compose UI + Animatable + NestedScrollConnection整文件进 composeMain;决策面(状态机、消费规则、校验)移植进 SKSheetEngine
android/ios/jvm 的 SKDialogHost actual平台 UI原样进 composeMain 留盘(给上游 PR 留完整上下文)

结论:库的本质输出是行为决策而非像素——SKAlert 的价值在"哪些手势能关、关的原因是什么、scrim 和动效长什么样",SKBottomSheet 的价值在"拖多远算关、列表的位移怎么分"。这些逻辑不依赖 Compose 运行时,抽成纯函数后天然可编进 KN。

1.4 页面边界

ArkUI 页面按"验收宿主"定位:双 Tab(SKAlert / SKBottomSheet),每 Tab 上半是配置区(开关/滑杆),下半是 bridge log 面板记录每次 JSON 进出;遮罩、卡片、弹窗面板全部按 spec 渲染,页面自身不定义任何设计值。

第 2 阶段:目标与依赖打通

2.1 制品线验证(本适配的成败手)

Swiss Knife 没有 Kotlin 依赖(连 kotlinx-serialization 都没有),制品线风险集中在一处:鸿蒙工具链上不存在 Compose。社区 Maven(maven.eazytec-cloud.com)能提供 HarmonyOS Kotlin 2.2.21-1.0.0(带 ohosArm64() / ohosX64() 工厂与 ohosMain 源集),但上游的 Compose 1.11.1 / AGP 9.2.1 坐标在这条工具链上一个都解析不了。因此:

  • gradle/libs.versions.toml 收缩到只剩一个插件 org.jetbrains.kotlin.multiplatform:2.2.21-1.0.0;
  • settings.gradle.kts 把 eazytec 双仓(maven-public 与 content/groups/harmonyos)排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  • 上游 :examples:core / :examples:android 不进 settings——它们需要 AGP 与 Compose 工具链,一旦 include 整个 sync 就挂。
2.2 工具链钉版(为什么是 2.2.21-1.0.0 + Gradle 8.14.1)
项上游本 fork原因
Kotlin2.3.212.2.21-1.0.0鸿蒙定制发行版,ohos 目标的唯一来源;与社区已审核批次(qrose / AestheticDialogs / CMD)完全一致
Gradle9.5.08.14.1社区已审核组合按工具链线区分:2.2.21 线降 8.14.1,不赌 9.x 兼容性
JDK未钉Temurin 21 钉死org.gradle.java.home 绝对路径,杜绝 daemon 用错 JDK
configuration-cache开启移除鸿蒙 Kotlin 插件在 8.14.1 上求稳
2.3 编译真实内核

core / dialogs 的构建脚本收缩为同一形状:

kotlin {
  applyDefaultHierarchyTemplate()
  jvmToolchain(21)
  jvm()          // 交叉验证目标
  ohosArm64()
  ohosX64()
}

内核五件(engine/ 包)全部纯 Kotlin、零第三方依赖:SKColors(ARGB parse/format/α 乘法)、SKMotion(spring/tween 描述 + ArkUI 参数解析,response = 2π/√stiffness)、SKSystemBars + SKAlignments(图标样式与对齐的解析表)、SKAlertEngine(spec 组装)、SKSheetEngine(拖拽状态机 + 嵌套滚动消费规则)。移植纪律:数值逐字保留——scrim 0x80000000、阈值缺省 0.4、require 报错文案 Parameter \"hideOnDragThreshold\" must be between 0f and 1f、progress 公式、pre/post 消费条件,全部与上游一致。

一个纯 KN 侧的坑:java.lang.Math.PI 在 ohos 目标上报 Unresolved reference 'Math'——commonMain 里必须用 kotlin.math.PI(JVM 编译过不代表 KN 编译过,这也是保留 jvm 目标之外还要真跑 ohos 编译的原因)。

2.4 测试依赖的处置

鸿蒙工具链没有 ohos 目标的测试 runner。处置:jvmTest 依赖 kotlin("test") 在 JVM 上跑(同一份 commonMain 代码,即编进 .so 的那份逻辑),根构建全局禁用名字含 Ohos + Test 的任务,上游零测试文件可改动(上游本来就没有测试——18 个用例全部为本 fork 新写的语义钉子)。

第 3 阶段:桥协议建模

3.1 JSON 契约
dialogs engine/**(纯 Kotlin 决策内核,原样编译)
        ↓ SKAlertSpec / SKSheetSpec / SKSheetDragState / 纯函数判定
Bridge.kt(@CName × 1,扁平 JSON 进出)
        ↓ UTF-8 JSON
N-API swissKnifeBridge()
        ↓
ArkUI Index.ets(双 Tab + bridge log)

单入口、14 op、协议全扁平(无嵌套对象、无数组——cubic-bezier 传 "x1,y1,x2,y2" 逗号串,insets 拆四个标量字段):

请求 : {"op":"alert.spec","hideOnTouchOutside":true,"hideOnBackPress":true,"scrimColor":"80000000","alignment":"center",...}
       {"op":"alert.back","hideOnBackPress":true}
       {"op":"sheet.drag.move","totalDrag":0,"amount":38,"height":420}
       {"op":"sheet.scroll.post","totalDrag":0,"delta":12,"height":420}
响应 : {"ok":true,"op":"...","consume":true,"reason":"back"}
       {"ok":true,"op":"sheet.drag.move","totalDrag":38,"progress":0.9095,"consumed":true}
       {"ok":false,"op":"...","error":"..."}

op 清单:ping;alert.spec / alert.back / alert.touchOutside;sheet.spec;sheet.drag.start / sheet.drag.move / sheet.drag.end / sheet.drag.cancel;sheet.scroll.pre / sheet.scroll.post / sheet.fling.pre;sheet.back / sheet.touchOutside。所有请求字段可省略,缺省值即上游默认值。

3.2 状态归属为什么放在 ArkTS 侧

上游的弹窗状态本就住在调用方可观察的 State / Animatable 里(rememberSKBottomSheetState 的 animationProgress、totalDragAmount 只是类的私有字段)。桥没有照搬这个类,而是把它拆成纯函数:状态(progress / totalDrag / height)由 ArkTS @State 持有,每帧手势事件把状态带进内核、内核把新状态还回来。好处是 C ABI 无会话、无锁、无生命周期管理——Kotlin/Native 侧没有 synchronized,无状态让并发问题不存在;代价是 ArkTS 侧多几个 @State 字段,而这正是 ArkUI 声明式渲染最自然的形态。动效执行同理:内核把 Compose AnimationSpec 解析成数(spring:response 秒 + dampingFraction;tween:duration/delay/曲线四元数),ArkUI animateTo 执行——spring 是连续物理动画,逐帧复刻无意义,参数化近似(response = 2π/√stiffness)是社区已认可的做法。

3.3 错误通道

入口整体 try/catch,Kotlin 异常永远不跨 FFI:

@CName("SwissKnifeBridge")
public fun swissKnifeBridge(request: String): String {
  var op = ""
  return try {
    val req = MiniJson.parse(request) as? J.Obj ?: throw JsonParseException("request must be an object")
    op = (req.values["op"] as? J.Str)?.v ?: throw JsonParseException("missing op")
    val response = Response(op)
    dispatch(op, req, response)
    val frame = response.done()
    MiniJson.parse(frame)   // 响应帧自检:写完必须能被重新 parse
    frame
  } catch (t: Throwable) {
    """{"ok":false,"op":${MiniJson.jsonString(op)},"error":${MiniJson.jsonString(t.message ?: t.toString())}}"""
  }
}

非法 JSON、缺 op、未知 op、阈值越界(返回上游同款 require 文案)、非法颜色/对齐/动效参数,全部走同一个 ok:false 通道,ArkTS 侧统一渲染到日志面板。响应帧写完必须过一遍 MiniJson 回读自检,保证桥上跑出去的永远是合法 JSON。

第 4 阶段:原生桥接(技术难点)

4.1 三层桥接
层文件职责
Kotlin/Nativeexample/nativeApp/src/ohosMain/.../Bridge.kt@CName("SwissKnifeBridge") 单入口、14 op 分发、MiniJson、Response 扁平帧
C++ N-APIharmonyApp/entry/src/main/cpp/napi_init.cpp参数检查、UTF-8 转换、调用 C 符号、DisposeString 归还 KN 字符串
类型声明cpp/types/libswissknife_napi/{index.d.ts, oh-package.json5}ArkTS 侧 import { swissKnifeBridge } from 'libswissknife_napi.so'

三处命名恒定一致:NAPI nm_modname = "swissknife_napi"、ArkTS import libswissknife_napi.so、声明 swissKnifeBridge——名字错一个就是运行期 does not provide an export named。

4.2 Kotlin/Native 导出函数
@file:OptIn(kotlin.experimental.ExperimentalNativeApi::class)

@CName("SwissKnifeBridge")
public fun swissKnifeBridge(request: String): String { /* §3.3 */ }

MiniJson 是手写递归下降解析器(J.Obj/Arr/Str/Num/Bool/Null 六型,~200 行)——kotlinx.serialization 在这条工具链上不可用,且协议全扁平,手写比引序列化运行时更小更可控(cmp_qrose MiniJson 同款理由)。两个纯 KN 细节:Kotlin 没有 \f 转义(Java 有),form-feed 用 Char(0x0C) 表达;响应数字输出走 num() 归一化,整值不带 .0 尾巴。

4.3 C++ N-API 方法分发

napi_init.cpp 固定四段:JsToUtf8(两段式 napi_get_value_string_utf8 + std::vector<char>,绕开 API 26 头文件把缓冲区标非 const 的问题)→ 调 SwissKnifeBridge(request.c_str()) → napi_create_string_utf8 回表 → DisposeKnString(通过 libohosswissknife_symbols()->DisposeString 归还 KN 堆字符串,绝不能 free())。模块注册走 napi_module 静态结构 + __attribute__((constructor))。

4.4 CMake 和类型声明
add_library(ohosswissknife SHARED IMPORTED)
set_target_properties(ohosswissknife PROPERTIES
    IMPORTED_LOCATION ${KN_SO_DIR}/libohosswissknife.so
    IMPORTED_NO_SONAME TRUE)

add_library(swissknife_napi SHARED napi_init.cpp)
target_link_libraries(swissknife_napi PUBLIC
    libace_napi.z.so libhilog_ndk.z.so ohosswissknife)
# $ORIGIN rpath + POST_BUILD 把 KN so 和 libc++_shared.so 拷到 NAPI so 旁

Gradle 侧 copyKnSo* 任务把 libohosswissknife.so 与 konan 工具链的 libc++_shared.so(triple aarch64-linux-ohos / x86_64-linux-ohos)拷进 entry/libs/<abi>/,挂为 link 任务 finalizedBy——link 完即拷,HAP 构建前 so 永远就位。实测产物:arm64 libohosswissknife.so 3.84MB、x86_64 3.53MB;llvm-nm -D --defined-only 确认 SwissKnifeBridge 在双 ABI 均为全局 T 符号。

第 5 阶段:ArkUI 宿主

5.1 双 Tab 与触发操作
Tab配置区验收面
SKAlerthideOnTouchOutside / hideOnBackPress 开关、scrim alpha 滑杆、motion 切换(default spring / tween 300ms)、系统栏图标样式 Selectscrim 淡入淡出、卡片 progress 同步缩放、点遮罩/返回键的 reason 仲裁、caller 直关
SKBottomSheet四开关(outside/drag/back/nestedScroll)、hideOnDragThreshold 滑杆、cornerRadius 滑杆拖拽跟手(snapTo)、阈值判定/回弹、列表嵌套滚动 pre/post、fling 拦截、三通道隐藏

页面根节点 Stack:常规内容 + 条件渲染的两层 overlay(alert / sheet),与上游"SKDialogHost 应包在 if 块里"的用法一一对应。页面级 onBackPress(): boolean 扮演 SKDialogHost 的 back 缝隙:先问内核(alert.back / sheet.back),consume 为真才拦截并触发隐藏——hideOnBackPress=false 时返回 false 让事件继续上传,与上游契约逐字一致。

5.2 页面交互

三条手势/滚动接入路径,对应上游三个决策入口:

  • 拖拽条 PanGesture(上游 detectVerticalDragGestures 挂在整块内容上,ArkTS 收敛到拖拽条):onActionStart → sheet.drag.start;onActionUpdate 累计增量喂 sheet.drag.move(PanGesture 的 offsetY 是累计值,需自行差分);onActionEnd → sheet.drag.end,shouldHide 走退场否则 sheet.drag.cancel 回弹;
  • 内层列表 onScrollFrameBegin(扮演 SKBottomSheetNestedScrollConnection):本 SDK 的回调签名是 (offset: number) => { offsetRemain: number }——dy<0 且弹窗位移中喂 sheet.scroll.pre(先弹回),dy>0 且列表在顶(scroller.currentOffset().yOffset <= 0)喂 sheet.scroll.post(剩余位移拖弹窗),消费则 offsetRemain 返回 0;
  • onScrollStop:接 sheet.fling.pre(上游 onPreFling 只在拖拽进行中拦截),拦截即转 sheet.drag.end 终判。

progress 更新分两种:拖拽/滚动路径直接赋值(上游 snapTo 语义,无动画);显隐路径包 animateTo 按 spec 动效执行(spring 用 curves.springMotion(response, dampingFraction),tween 用内置 Curve.FastOutSlowIn 预设或字符串曲线)。

5.3 对比度和错误边界

scrim 透明度 = skAlphaOf(spec.scrim) × progress(内核给基色,页面只做上游固定的 α×progress 公式);skWithAlpha 重建 AARRGGBB。所有桥调用包 try/catch,失败帧与 ok:false 统一进 bridge log 面板;每次协议往返(请求 + 响应)带时间戳入日志,最多保留 60 条——真机上出问题第一现场不丢。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohosswissknife)+ cxxShared + copyKnSo*
│   └── src/ohosMain/kotlin/example/nativeapp/
│       ├── Bridge.kt                         @CName × 1 + 14 op 分发 + Response 扁平帧
│       └── MiniJson.kt                       手写递归下降 JSON(协议全扁平,继承 cmp_qrose 审核模式)
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.swissknife、versionName 1.0.11-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       IMPORTED 引入 KN so、$ORIGIN rpath
        ├── src/main/cpp/napi_init.cpp        N-API 导出、参数检查、DisposeString
        ├── src/main/cpp/include/libohosswissknife_api.h
        ├── src/main/cpp/types/libswissknife_napi/{index.d.ts, oh-package.json5}
        ├── src/main/ets/model/BridgeTypes.ets 协议类型 + skWithAlpha/skCurve 工具
        └── src/main/ets/pages/Index.ets      ArkUI 双 Tab 验收页面
6.2 构建、签名和安装
./gradlew :dialogs:jvmTest                                            # 18 项交叉验证
./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 中勾选自动生成,证书写入本机 ~/.ohos/config,不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组,勾选自动签名后由 IDE 回填完整配置)。

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 org.terminator.ohos.swissknife -m entry
hdc shell hilog | grep SwissKnife

四、完整代码对照

4.1 整体架构

上游 composeMain(12 个 Compose 源文件,零改动隔离留盘)
        ↓ 决策面移植(数值逐字保留)
dialogs engine/**(纯 Kotlin 内核:颜色/动效/系统栏/Alert/Sheet 五件)
        ↓ jvm() 18 项用例交叉验证
Bridge.kt(@CName × 1,扁平 JSON 进出,异常不过界)
        ↓ UTF-8 JSON
N-API swissKnifeBridge()(DisposeString 纪律)
        ↓
ArkTS Index.ets(双 Tab:spec 渲染 + 手势/滚动接入 + 协议日志)
        ↓
HAP(双 ABI 三件套 so)→ 签名 → 真机

4.2 文件清单

动作文件说明
改写settings.gradle.kts / build.gradle.kts / gradle.properties / libs.versions.toml / wrappereazytec 前置、ohos_swiss-knife、钉 JDK21/Kotlin/Gradle、全局禁用 Ohos*Test
改写core/build.gradle.kts、dialogs/build.gradle.kts去 AGP/Compose/maven-publish,jvm + ohos 双目标,dialogs 依赖 core + kotlin-test
新写core/src/commonMain/.../SKInsets.ktDp→Double 纯值移植,同包同名(composeMain 原件隔离)
新写dialogs/src/commonMain/.../SKDialogHostSystemUIOptions.kt从 SKDialogHost.kt 抽回的纯类型(Light/Dark/Automatic)
新写dialogs/src/commonMain/.../engine/(5 文件)SKColors / SKMotion / SKAlertEngine(+SKAlignments) / SKSheetEngine
新写dialogs/src/jvmTest/...(2 文件 18 用例)语义钉子
移动`coredialogs/src/composeMain/**`
新增example/nativeApp/**KN 桥模块
新增example/harmonyApp/**HAP Demo
追加.gitignorebuild/ 去根锚定、example/harmonyApp/{oh_modules,.hvigor,entry/build,entry/libs,.cxx}、*.so
未动jitpack.yml、.github/、docs/、examples/**、dialogs/README.md上游资产原样保留

4.3 关键 API 对照

上游 Compose API桥 op / 内核语义保真点
SKAlert(visible, onHide, hideOptions, onHidden, systemUIOptions, insets, animationSpec, scrimColor, alignment)alert.spec + alert.back/alert.touchOutside + ArkTS 渲染层scrim 缺省 0x80000000;visible ↔ alertMounted;onHide(reason) 枚举一一对应
SKAlertHideOptions(hideOnTouchOutside, hideOnBackPress)alert.spec 同名字段缺省 true
SKDialogHostSystemUIOptions.SystemBarIconsStylestatusBarIconsStyle/navigationBarIconsStyle + isSystemDarkAutomatic = 深色系统给白图标
AnimationSpec<Float>(null / spring / tween)motionKind=default/spring/tween + 参数缺省 spring = damping 1.0 + stiffness 400(DampingRatioNoBouncy/StiffnessMediumLow);tween 缺省 FastOutSlowIn
SKBottomSheet(visible, onHide, container, hideOptions, behavior, ...)sheet.spec + sheet.drag.* + sheet.scroll.* + sheet.fling.pre阈值缺省 0.4;require 文案逐字保留;insets 外/内拆分保留
SKBottomSheetState.onDragStart/onDrag/onDragEndsheet.drag.start/move/endprogress = 1 - max(0, totalDrag/height);fraction >= threshold 才走;终判后 totalDrag 归零
SKBottomSheetNestedScrollConnection.onPreScroll/onPostScroll/onScroll/onPreFlingsheet.scroll.pre/post + sheet.fling.prepre 只吃 dy<0、post 只吃 dy>0、totalDrag+delta >= 0 才消费、fling 仅拖拽中拦截

4.4 ArkTS 与 Kotlin 的边界

  • Kotlin 侧(.so):隐藏规则仲裁、拖拽判定、滚动消费、阈值校验、颜色/对齐/动效/系统栏解析——一切"决策";
  • ArkTS 侧:@State 持有 progress/totalDrag/height、animateTo 执行动效、手势差分、scrim α×progress 上游固定公式、页面布局与日志——一切"渲染与交互";
  • 页面不写死任何设计值:换 scrim、换阈值、换动效、换图标样式,改的都是发给 .so 的请求参数。

五、关键决策说明

决策 1:抽决策内核,而不是整库编译或提取渲染层

Swiss Knife 与 KStore 相反:全库是 Compose UI,没有可整库编译的零依赖引擎;但它的本质输出是行为决策。所以切面选在"决策 vs 渲染"上:决策进 .so(可验证、可复用),渲染隔离留盘(给上游与未来 CMP 鸿蒙版留完整上下文)。这与 qrose(编码器 vs Painter)、AestheticDialogs(设计 token vs 组件)、CMD(对话框引擎 vs 组件)是同一条判据——切面由库的本质输出决定。

决策 2:工具链钉 Kotlin 2.2.21-1.0.0,Gradle 降 8.14.1

不追新:社区已审核批次在这套组合上跑通,偏离即踩未验证的坑。jvm() 保留不是摆设——18 个用例在 JVM 上验证的正是编进 ohos .so 的同一份 commonMain 代码。

决策 3:根构建替换(cmp_qrose 模式),上游 examples 留盘

上游 Android/iOS 消费者继续用 JitPack 的 1.0.11(jitpack.yml 未动);鸿蒙构建只 include :core :dialogs :example:nativeApp。composeMain 隔离区让"上游 PR 不带任何 UI diff"成为可核查的事实。

决策 4:无会话纯函数协议,状态由 ArkTS 持有

对比 CMD 的 25 op 有会话协议:swiss-knife 的状态只有三个标量,不值得上会话管理。纯函数协议让桥天然线程安全(Kotlin/Native 没有 synchronized)、让 C ABI 恒定、让 ArkTS 侧的 @State 成为唯一事实源。

决策 5:jvmTest 交叉验证 + 全局禁用 Ohos 测试

鸿蒙工具链没有 ohos 测试 runner;根构建统一禁用 *Ohos*Test* 任务,语义验证全部压到 JVM。18 个用例覆盖:缺省值逐字段、require 报错文案逐字、progress 公式与 clamp、pre/post 消费条件、fling 拦截条件、scrim α 乘法(含饱和/归零)、对齐映射与未知值报错、Automatic 按系统深色解析、动效解析(spring response 数学、tween 逐数透传)。

六、测试与验证

6.1 测试环境

项值
OSWindows 11 x64
JDKTemurin 21.0.12(org.gradle.java.home 钉死)
Gradle8.14.1
KotlinHarmonyOS 2.2.21-1.0.0
DevEco Studio5.1.0 Release(compatibleSdkVersion 5.1.0(18))
真机HarmonyOS ARM64(2in1/手机均可,Demo deviceTypes: ["phone"])

6.2 JVM 交叉验证

./gradlew :dialogs:jvmTest      # BUILD SUCCESSFUL,18/18 通过

两组用例:SKAlertEngineTest(7 项:缺省 spec、缺省动效数学、tween 透传、对齐映射、系统栏解析、scrim α 乘法与格式化回读、非法参数报错);SKSheetEngineTest(11 项:缺省 spec、阈值校验文案与边界、insets 保真、dragMove 累计/clamp/height=0 不消费、dragEnd 阈值判定与负值归零、scrollPre/scrollPost 消费条件、flingPre 拦截条件、dragStart 归零)。

6.3 HAP 验证

./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
ls -la example/harmonyApp/entry/libs/{arm64-v8a,x86_64}      # 各两件:KN so + libc++_shared.so
llvm-nm -D --defined-only .../arm64-v8a/libohosswissknife.so | grep SwissKnifeBridge   # T 符号

DevEco 打开 example/harmonyApp(不要打开仓库根目录),勾选自动签名,Run 到真机。

6.4 功能用例

SKAlert:

  1. Show → scrim 淡入 + 卡片缩放淡入(progress 0→1,bridge log 记录 alert.spec 请求);
  2. 点遮罩 → onHide(SKAlertHideReason.TouchOutside),退场动画结束后 onHidden fired;
  3. 关 hideOnTouchOutside 再点遮罩 → 日志 tap outside ignored,弹窗不关;
  4. 系统返回 → onHide(...Back);关 hideOnBackPress 再按返回 → back press propagates,事件上抛;
  5. scrim alpha 滑杆改 30% → 重开遮罩变淡;图标样式切 dark → 弹窗期间状态栏图标变黑;
  6. motion 切 tween → 300ms FastOutSlowIn 入场。

SKBottomSheet:

  1. Show → 底部滑入,scrim 跟随;
  2. 拖拽条下拉 → progress 实时跟手(无动画,snapTo),scrim 同步变淡;
  3. 下拉越过阈值(默认 0.4)松手 → onHide(...Drag),滑出后 onHidden;不足 → spring back 回弹;
  4. 列表滚到顶继续下拉 → 弹窗跟手下移(onPostScroll 路径);弹窗位移后上滑列表 → 先弹回升到顶(onPreScroll 路径)列表才滚动;
  5. 阈值滑杆调 0 → 轻碰即隐藏;调 1 → 拖满整高才隐藏;
  6. 关 nestedScroll → 列表滚动不再带动弹窗;
  7. 遮罩点击 / 返回键 / Close 按钮三条隐藏路径 reason 各自正确。

6.5 验证结论

编译层(:core:/:dialogs:compileKotlinJvm)、语义层(18/18 jvmTest)、链接层(双 ABI so + 自动拷贝)、符号层(SwissKnifeBridge 全局 T)全部通过;功能层与设备层经真机验收通过——首屏 ping 握手显示 swiss-knife 1.0.11-ohos.1 · protocol 1 · ohosArm64,ohosX64,SKAlert 全链路(spec → scrim → 隐藏仲裁 → onHidden)与 bridge log 逐条对得上,见 §7.1 截图。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/swiss-knife.git
cd swiss-knife
export JAVA_HOME="/path/to/jdk-21"
./gradlew :dialogs:jvmTest                                                      # 18 项交叉验证
./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 org.terminator.ohos.swissknife -m entry
hdc shell hilog | grep SwissKnife

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让决策内核真正编译成 ohosArm64 / ohosX64 动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. 上游模块(examples/core、examples/android)绝不能 include 进 settings——它们引用的 AGP/Compose 坐标在鸿蒙 Kotlin 工具链上一个都解析不了,include 即 sync 失败;
  4. Compose 源码不能留在 commonMain:"隔离目录 src/composeMain(任何目标都不编译)"是唯一既能留盘又不进构建的做法;被 UI 文件绑住的纯类型(SKDialogHostSystemUIOptions)要抽回 commonMain;
  5. commonMain 里禁用 java.lang.Math——Math.PI 在 JVM 编译通过、在 ohos 目标上报 Unresolved reference 'Math',必须用 kotlin.math.PI;同理 Kotlin 没有 \f 转义,form-feed 用 Char(0x0C);
  6. ArkTS 严格模式三连:对象字面量必须对应声明的 interface(arkts-no-untyped-obj-literals);PanGesture 是构造函数不是类型,手势链要内联不能当返回类型;onScrollFrameBegin 新签名是 (offset: number) => { offsetRemain: number },不是旧文档的 Offset 对象进出——SDK 报错信息里的类型名(OnScrollFrameBeginHandlerResult)才是权威;
  7. 弃用 API:curves.cubicBezier 用内置 Curve.FastOutSlowIn 预设或 animateTo 字符串曲线替代;getContext(this) 换 this.getUIContext().getHostContext();
  8. 根 .gitignore 的 /build/ 是根锚定模式,盖不住 example/nativeApp/build/——要改成不带锚的 build/ 并单独追加 harmonyApp 的 oh_modules/.hvigor/entry build/entry libs/.cxx/*.so;
  9. NAPI 三处命名(nm_modname / ArkTS import 名 / index.d.ts 导出名)必须恒定一致,错一个就是运行期 does not provide an export named;
  10. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  11. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每 ABI 两件套(KN so + libc++_shared.so)缺一不可;
  12. 全仓文档/代码/默认文案统一 AtomGit 链接,不出现第三方镜像站字样。

8.2 已知问题

  • Compose 渲染层不参与鸿蒙构建(src/composeMain 隔离),鸿蒙侧 UI 由 ArkTS 按 spec 重绘,逐帧数值与 Compose Animatable 不逐位相等;
  • 弹簧动效是参数化近似:response = 2π/√stiffness、dampingFraction = dampingRatio(缺省对应 springMotion(0.314, 1.0)),观感一致但非逐帧等价;
  • onHidden 对 spring 用 480ms 观察窗近似(tween 是精确 duration+delay);
  • ArkUI 拿不到子列表 fling 速度,fling 拦截改在 onScrollStop 触发——只在"拖拽进行中停手"这一上游真正拦截的场景生效;
  • 拖拽手势收敛到拖拽条(上游挂在整块内容上靠手势仲裁);列表区域的拖拽走嵌套滚动路径,两条路径覆盖的上游行为面相同;
  • Android/iOS 目标不在本 fork 构建里(上游仓库继续服务),鸿蒙消费者以 so 三件套集成;
  • HAP 安装必须使用与 bundle name org.terminator.ohos.swissknife 匹配的签名。

8.3 未来优化方向

  • 把拖拽手势扩到整块弹窗内容(PanGesture 与 List 的手势仲裁调参),进一步对齐上游手势面;
  • sheet.spec 增加 maxWidth / elevation 的 UI 控件(协议已支持,Demo 未放控件);
  • 增加 hilog 侧的结构化验收输出与 uitest 自动化截图脚本;
  • 评估 .har 形态发布内核类型包,让 ArkTS 工程直接依赖而不是走 so 三件套;
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建。

九、总结

9.1 核心难点回顾

上游 825 行 Compose UI(依赖度普查:决策面 vs 渲染面)
    → 决策内核抽取(SKAlertState / SKBottomSheetState / NestedScrollConnection 逐字移植)
    → composeMain 隔离(12 文件零改动留盘,纯类型抽回 commonMain)
    → 工具链对齐 Kotlin 2.2.21-1.0.0 + Gradle 8.14.1(eazytec 双仓前置)
    → ohosArm64 / ohosX64 KLIB + jvm 18 用例交叉验证
    → C ABI(@CName × 1,14 op 扁平 JSON,无会话纯函数)
    → C++ N-API(DisposeString 纪律)
    → ArkTS spec 渲染(手势/滚动/返回键三条接入路径)
    → ArkUI 真机双 Tab 验收(SKAlert / SKBottomSheet 全行为面 + bridge log)

9.2 封装层次

  • 决策层:内核五件(颜色、动效、系统栏、Alert spec、Sheet 状态机与消费规则),数值逐字保留上游语义;
  • 隔离层:composeMain 12 个上游文件零改动留盘,git rename 历史完整;
  • Native 层:双 ABI 动态库,单 C ABI 输出扁平 JSON;
  • N-API 层:参数检查、UTF-8 转换、KN 内存归还;
  • ArkTS 层:状态持有、spec 渲染、手势/滚动接入、协议日志与错误文本;
  • DevEco 层:CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 切面由库的本质输出决定:qrose 切在"编码器 vs Painter"、AestheticDialogs 切在"设计 token vs 组件"、CMD 切在"引擎 vs 组件"、swiss-knife 切在"行为决策 vs 渲染"——对话框库的价值是阈值判定、消费规则和隐藏仲裁,这些不依赖 Compose 运行时,抽出来就能编进 .so;
  2. 工具链组合宁可用社区已审核批次也不要自由发挥:Kotlin 2.2.21-1.0.0 + Gradle 8.14.1 + JDK 21 钉死,eazytec 双仓置前,任何"顺手升个级"都会把已验证的路走成未验证的坑;
  3. 交叉验证是"内核没漂"的唯一可信证明:require 文案逐字、progress 公式逐式、消费条件逐条钉进 jvmTest,Demo 的验收重点才落在"三条隐藏路径的 reason 对不对、列表滚到顶后弹窗跟不跟手"这些行为面上。

9.4 适配成果

当前适配已完成:

  • Swiss Knife 1.0.11(fork 版本 1.0.11-ohos.1)决策内核的双 ABI Kotlin/Native 编译(拖拽状态机、嵌套滚动消费规则、隐藏仲裁、系统栏与动效解析,数值逐字保留);
  • 上游 Compose 渲染层 12 文件零改动隔离留盘(composeMain,git rename 历史完整);
  • 1 个 C ABI 符号(SwissKnifeBridge,llvm-nm 验证双 ABI 全局 T)和 C++ N-API 包装层(双 ABI so 合计约 11MB 含 libc++_shared.so);
  • 14 op 全扁平 JSON 协议(无会话、纯函数、异常不过界)+ 手写 MiniJson;
  • DevEco Stage ArkUI 双 Tab 验收页面(SKAlert / SKBottomSheet 全行为面、bridge log、版本握手);
  • 18 项 JVM 交叉验证全绿 + HAP 构建、签名、设备安装和真机验证(效果图见 doc/img/);
  • 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。

参考文档

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐