本文记录 ui-text-compose 接入 OpenHarmony 的完整过程,覆盖文本蓝图类库的选型定位、Compose 层停编决策、纯 Kotlin 算法内核移植、上游占位符规则的逐条对齐、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。

ui-text-compose 是一个 Compose Multiplatform 文本蓝图(text blueprint) 库:它解决的是 AndroidViewModel 反模式——ViewModel 里提前解析好的本地化字符串,在用户切换语言后全部过期。库的答案是让 ViewModel 持有未解析的蓝图(raw 纯文本 / res 资源模板 + 参数 / pluralRes 复数资源 + 数量 / 多蓝图 compound 拼接,参数本身还可以嵌套另一张蓝图),渲染时才在 Composable 语境下解析成 AnnotatedString,语言一变自动重新解析。本次适配与 KStore 的"真编译整库"路线有本质区别:@Composable、AnnotatedString、SpanStyle 全部长在 androidx.compose 里,社区鸿蒙工具链(CMP 1.9.2 / HarmonyOS Kotlin 2.2.21)编不了上游 CMP 1.8 / Kotlin 2.1.20 的源码集——因此走算法内核移植路线:上游 UITextUtil 的两条占位符正则、转义规则、注解栈语义被逐条移植成不依赖 Compose 的纯 Kotlin commonMain,上游 Compose 层四个文件 git mv 到 src/composeMain 停编保留;一个能正确处理两套占位符规则、嵌套蓝图、样式注解与链接的文本引擎运行在真机上,ArkUI 页面作为验收宿主。

项目地址: AtomGit/oh-tpc/ui-text-compose.git

上游仓库: radusalagean/ui-text-compose

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

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

ui-text-compose 为 Compose 应用提供"文本怎么描述、何时解析"这一件事的完整答案:UIText { raw() / res() / pluralRes() } DSL 一行构造蓝图,arg(...) 支持纯字符串与嵌套蓝图两种参数,+SpanStyle / +ParagraphStyle / +LinkAnnotation.Url 往蓝图上挂样式注解,渲染时 buildStringComposable() 出纯文本、buildAnnotatedStringComposable() 出带样式的 AnnotatedString。资源占位符有两套规则:Android String Resources 的 %1$s + %s(含 %%s 转义还原),以及 Compose Multiplatform String Resources 的仅 %1$s——两条正则与转义剥离逻辑全部在上游 UITextUtil 里。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面、Web 共享。OpenHarmony 应用同样需要这套"渲染期解析 + 语言热切换"的文本能力,但上游构建不会生成 OpenHarmony 变体。

如果只在 ArkTS 里手写一套"模板 + 参数 + 样式"拼串逻辑,页面可以显示文字,却等于把上游经过测试的算法翻译一遍——两条占位符规则的差异、%% 转义、嵌套参数的注解语义,每一处都是翻译走样的机会。适配需要同时验证算法内核、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
Compose 制品缺失AnnotatedString / SpanStyle / LinkAnnotation 在 androidx.compose.ui.text,@Composable 在 androidx.compose.runtime——社区鸿蒙栈没有能对上上游 CMP 1.8 / Kotlin 2.1.20 的制品线;强行对齐等于再打一套 Compose 发行版。
接口形状强绑定 Compose上游 UITextBase 的两个方法都是 @Composable,UITextAnnotation 三个 value class 直接包装 Compose 类型——不是加一个 actual 就能过的 expect/actual 形态。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
文本语义跨不了桥AnnotatedString 是 Compose 类型,C ABI 只能同步传数据——分段样式必须翻译成纯数据。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:占位符/注解算法的纯 Kotlin 移植与测试锁定、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。蓝图语义完全由移植自上游的算法负责,ArkTS 只负责提供已本地化模板、发起解析和渲染分段样式。

1.2 库提供的能力

Demo 提供一个真实的文本蓝图解析会话,对应桥接的全部入口:

  • 四种蓝图:raw(纯文本)、res(模板 + 参数)、plural(复数模板 + 数量,无显式参数时自动把数量注册为 %1$s——上游 pluralRes(resource, quantity) 的默认行为)、compound(多蓝图拼接);
  • 两套占位符规则:android 规则替换 %1$s 与 %s 并把 %%s / %%1$s 还原为 %s / %1$s;cmp 规则只替换 %1$s,%% 保持字面量——同一模板走两套规则的差异在 Demo 里并排展示;
  • 样式注解:span(颜色 / 背景 / 字号 / 字重 / 斜体 / 下划线 / 删除线)、paragraph(对齐 / 行高)、link(URL + 链接色 + 下划线),注解栈按字段合并、内层覆盖外层;
  • 嵌套参数两种语义:无自身注解的嵌套蓝图继承外层注解栈(上游 getString 返回 String、withStyle 重刷);带自身注解的嵌套蓝图保留自身样式不被重刷(上游 append AnnotatedString 的行为)——两种都与上游对齐;
  • 双输出:每次解析同时返回 plain(等价上游 buildString())与分段样式数组 segments(等价上游 buildAnnotatedString() 的 span 切片);
  • 版本握手:op:"spec" 返回 fork 版本、上游版本与能力清单,来自真实 Native 调用而非静态文字。

响应包含:

字段作用
ok解析是否成功,失败时 error 带原文。
op / locale回显本次操作与语言(信息性字段,模板进桥前已在 ArkTS 侧本地化)。
plain纯文本结果(上游 buildString() 的等价物)。
segments{text, span, link} 分段数组;相邻同样式分段自动合并,ArkTS 侧直接映射成 Span。
paragraph整段级 {align, lineHeight},无段落注解时为 null。

上游算法的全部语义(两条正则、编号/未编号映射、%% 转义剥离、注解栈合并、嵌套两种情形、越界抛错)由 34 项 JVM 测试锁死(21 项算法交叉验证 + 12 项桥协议端到端冒烟 + 1 项文档示例契约),证明 ohos .so 里执行的解析逻辑没有漂移。

1.3 实现适配

维度要求
代码复用算法语义逐条移植自上游 UITextUtil,不改行为(连 %%1$s 在 cmp 规则下拆出字面量 % 这种上游怪癖都原样保留);上游 Compose 层 4 文件 git mv 到 composeMain 停编保留,等制品对齐后接回。
平台目标根构建直接声明 ohosArm64 + ohosX64,保留 jvm 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);uitextcompose-multiplatform / uitextcompose-android / 两个 sample 留盘不进构建。
桥接稳定1 个 C ABI 入口(op = resolve / spec)和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。
依赖纪律移植内核零第三方依赖(连 JSON 解析都是手写递归下降),.so 里只有 Kotlin/Native 运行时。
UI 完整单页覆盖上游 sample 全部 9 个场景卡 + 占位符规则对照卡 + en/ro 语言切换 + quantity 切换。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。文本的"渲染"由 ArkTS Text + Span 完成,桥只负责把蓝图画成带样式的分段数据——这正是"文本引擎出数据、ArkUI 出像素"的分工。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点模块与依赖边界、确认 compose.ui 无 ohos 制品、确定交付物
第 2 阶段:源码集拆分     ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Compose 层 git mv 停编
第 3 阶段:算法内核移植   ── 蓝图树/注解模型/占位符扫描器纯 Kotlin 化、上游正则逐条对齐、JVM 测试锁定
第 4 阶段:桥协议建模     ── 蓝图 JSON 契约、resolve/spec 双 op、错误通道、MiniJson 递归解析器
第 5 阶段:原生桥接       ── @CName C ABI、C++ N-API、参数校验和内存释放
第 6 阶段:ArkUI 宿主     ── 九场景卡 + 规则对照卡、en/ro 与 quantity 热切换、HAP、签名、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
uitextcompose-core/                   上游核心库(进 ohos 构建,jvm + ohosArm64 + ohosX64)
uitextcompose-core/src/commonMain/    新增:纯 Kotlin 算法内核(5 个文件)
uitextcompose-core/src/composeMain/   上游 Compose 层 4 文件(git mv 停编,保留历史)
uitextcompose-multiplatform/          CMP 资源版 UIText,留盘不进 settings
uitextcompose-android/                Android R 资源版 UIText,留盘不进 settings
uitextcompose-multiplatform-sample/   上游 KMP 示例,留盘不进构建
uitextcompose-android-sample/         上游 Android 示例,留盘不进构建
example/nativeApp/                    Kotlin/Native 动态库(Dispatch + MiniJson + Bridge)
example/harmonyApp/                   DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md        鸿蒙适配说明(中英双份)
README.upstream.md                    上游原文保存

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :uitextcompose-core 与 :example:nativeApp,rootProject.name = "ohos_ui_text_compose"。上游的 binary-compatibility-validator / vanniktech 发布插件 / AGP classpath / compose 插件全部随根构建替换剥离——本 fork 不发 Maven Central。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDK21(Temurin,org.gradle.java.home 钉 C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot)
OpenHarmony 目标ohosArm64 + ohosX64(jvm 保留作交叉验证)
Gradle8.14.1(wrapper 钉版;2.2.21 线的社区标准)
依赖零第三方依赖——算法内核与桥都不引 kotlinx 库
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.uitextcompose
export JAVA_HOME="/c/Program Files/Eclipse Adoptium/jdk-21.0.12.101-hotspot"
java -version    # openjdk 21

注意 Windows 上 gradlew.bat 的启动器本身要求 JAVA_HOME——gradle.properties 里的 org.gradle.java.home 只作用于 daemon,启动器层面照样报 JAVA_HOME is not set,所以命令行构建必须先 export。

1.3 源码依赖度盘点
模块commonMain 文件依赖形态处置
uitextcompose-core5UITextBase / UITextBuilderBase / UITextAnnotation / UITextUtil 强依赖 compose.runtime + compose.ui.text;InternalApi 纯 Kotlin4 个 git mv 到 composeMain 停编;算法移植为 Ui* 纯 Kotlin 5 文件,InternalApi 原样留在 commonMain
uitextcompose-multiplatform2CMP 资源系统(getString / rememberResourceEnvironment)+ compose.runtime留盘不进构建
uitextcompose-android2Android Context.getString + LocalConfiguration + compose留盘不进构建
两个 sample—AGP / compose app 工程留盘不进构建

值得单独说明的是上游库没有 expect/actual——全部平台差异被 CMP 资源系统吸收,这本来是 KMP 的优雅形态,但恰恰意味着没有一个"补 actual 就能编"的缝:@Composable 与 AnnotatedString 长在方法签名和 value class 内部,停编隔离是唯一出路。

1.4 页面边界
标题区          UI-TEXT-COMPOSE · OPENHARMONY / 版本握手行(CMP 文本蓝图库 · Kotlin/Native ohosArm64/ohosX64)
语言切换        English / Română 双按钮(切换后全部蓝图即时重解析)
数量切换        1 / 2 / 5 / 30 / 35 五档(复数分类联动)
场景卡 ×9       raw / res / pluralRes / res·annotated / pluralRes·annotated / compound 1-3 / ToS&Privacy
规则对照卡      同一模板走 android / cmp 两套规则的输出并排展示
状态行          最近一次解析结果(如"已解析 · locale=ro · quantity=30")

页面启动时先发 op:"spec" 握手,随后把全部蓝图画出来。资源表镜像上游 sample 的 strings.xml(en/ro 双语),切语言只是换一张模板表重新 resolve——这正是上游"蓝图在渲染期解析、语言变了自动重刷"的鸿蒙等价物。

第 2 阶段:源码集拆分与目标打通

2.1 为什么停编而不是硬接

社区此前审核通过的钉法是 HarmonyOS Kotlin 2.2.21-1.0.0(qrose / segmented-control / combo-breaker 用的),配套 CMP 1.9.2。而上游 ui-text-compose 构建在 CMP 1.8 / Kotlin 2.1.20 / AGP 8.10——两套 Compose 发行版无法共存于一个 Gradle 构建。社区里唯一"用上 ohos Compose 制品"的先例 molecule 也只用了 compose.runtime(1.12.0-0.1.0-11 线),compose.ui.text 这一级没有任何 ohos 变体。所以上游 Compose 层 4 个文件 git mv 进 src/composeMain:不挂任何 target、不注册 sourceSet,Kotlin 编译器看不到它们;保留在树里一是对照上游 API,二是等制品对齐后接回。

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_ui_text_compose"

include(":uitextcompose-core")
include(":example:nativeApp")

gradle/libs.versions.toml 收敛到只剩一行插件:

[versions]
kotlin = "2.2.21-1.0.0"

[plugins]
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
2.2 编译纯 Kotlin 内核

uitextcompose-core/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"))
        }
    }
}

实测:

./gradlew.bat :uitextcompose-core:compileKotlinJvm :uitextcompose-core:compileKotlinOhosArm64 :uitextcompose-core:compileKotlinOhosX64   # BUILD SUCCESSFUL

第 3 阶段:算法内核移植(技术难点)

3.1 上游算法的形状

上游 UITextUtil 的全部逻辑建立在 androidx.compose.ui.text.buildAnnotatedString 之上,核心是三条正则:

上游正则语义
(?<!%)%(\d+\$)?[s]android 规则:编号 %1$s + 未编号 %s,负向单字符 lookbehind 拒绝 % 前缀
%(\d+\$)[s]cmp 规则:仅编号形式
%%([0-9]+\$)?s转义占位符,仅 android 规则对字面分段剥掉一个 %

外加一套注解栈语义:handleUITextAnnotations 把 SpanStyle / ParagraphStyle / LinkAnnotation 列表按嵌套 withStyle / withLink 包住文本;编号占位符映射 n - 1、未编号按出现顺序、%5$s 越界直接抛 IndexOutOfBoundsException。

3.2 移植的关键决策:手写扫描器替代 lookbehind

Kotlin/Native 对 regex lookbehind 的支持在各目标上没有保证——第一条正则的 (?<!%) 不能照抄。移植时手写了单字符 lookbehind 的扫描器 scanTemplate / matchPlaceholder:逐字符扫描,% 起头时手工解析 (\d+\$)?s,android 规则下检查前一个字符是否为 %,cmp 规则下要求必须有编号形式。接受集与上游正则完全一致,且规避了 Native 正则引擎差异。

private fun matchPlaceholder(text: String, start: Int): Pair<Int, Int?>? {
    var i = start + 1
    var number: Int? = null
    val digitStart = i
    while (i < text.length && text[i] in '0'..'9') i++
    if (i > digitStart) {
        if (i >= text.length || text[i] != '$') return null
        number = text.substring(digitStart, i).toIntOrNull() ?: return null
        i++
    }
    if (i < text.length && text[i] == 's') return (i + 1) to number
    return null
}

扫描器输出(字面分段 + 每个占位符的 arg 下标)与上游 baseString.split(regex) + findAll 的结构一一对应。甚至上游的一个怪癖也被忠实复现:%%1$s 在 cmp 规则下,正则无 lookbehind 会匹配内部的 %1$s,输出是字面量 % + 替换结果(如 "%X")——本 fork 原样保留、不修,因为鸿蒙侧要的是与上游可对照的行为,不是"更合理"的行为。

3.3 新 commonMain 的五个文件
InternalApi.kt       上游 opt-in 注解,原样保留(唯一没动内容的文件)
UiTextAnnotation.kt  Span/Paragraph/Link 纯数据注解(颜色 packed Long ARGB、字重 100..900)
UiTextValue.kt       蓝图树 Raw/Res/PluralRes/Compound + UiTextArg + PlaceholderRule 枚举
UiResolvedText.kt    UiSpanSpec/UiParagraphSpec/UiSegment/UiResolvedText(解析结果模型)
UiTextResolver.kt    算法移植:扫描器 + 注解栈合并 + 嵌套分流 + 相邻同样式段合并

AnnotatedString 的语义翻译成两条纯数据规则:注解栈按字段合并、内层覆盖外层(mergeSpan);嵌套参数按"解析结果的分段是否全无样式"分流——全无样式等价上游的纯 String(getString 返回值),外层 withStyle/withLink 会重刷;带样式等价上游的 AnnotatedString,append 时保留自身样式不被重刷。

3.4 JVM 冒烟抓出的三个保真度 bug

算法移植最容易翻车的地方不是编译,而是语义走样。本次把桥的协议逻辑放进 example/nativeApp 的 commonMain(@CName 只是 ohosMain 里的薄包装),给 nativeApp 加 jvm() 目标跑端到端 JSON 冒烟——ArkTS 将要走的完整链路(请求 JSON 进、响应 JSON 出)在 JVM 上先跑一遍。这套冒烟真实抓出了三个 bug:

  1. plural 自动参数缺失:上游 pluralRes(resource, quantity) 省略 builder 块时默认 arg(quantity.toString())——桥最初没移植这个默认行为,无显式参数的 plural 蓝图直接越界报错;
  2. 基础链接不覆盖字面分段:上游 handleUITextAnnotations(baseAnnotations) 包住整个 builder,withLink 对字面量同样生效——桥最初只在参数分支查链接,“Proceed to checkout” 整段模板的链接注解失效;
  3. 无注解嵌套不吃外层注解:上游 getString 返回 String 时 withStyle 会重刷——桥最初一刀切地让所有嵌套保持自身样式,legal 卡的链接注解失效。

三个 bug 全部修复并由测试锁定。这正是"JVM 交叉验证是内核没漂的唯一可信证明"的现场版:不用等真机,协议层走样在桌面就被拦下了。

3.5 测试矩阵
./gradlew.bat :uitextcompose-core:jvmTest :example:nativeApp:jvmTest   # 34 项全绿
测试类用例数钉住什么
UiTextResolverTest(core commonTest)21两条占位符规则、%%s/%%1$s 转义矩阵(含 cmp 怪癖)、编号重排、注解栈合并、嵌套两种语义、基础/参数链接、段落、compound、越界抛错
BridgeProtocolTest(nativeApp jvmTest)12spec 握手、raw/res/plural/compound 端到端、嵌套样式、链接段、段落上报、规则对照、Unicode 往返、错误兜底(坏 JSON / 未知 op / 越界全部 ok:false)
DocExampleTest(nativeApp jvmTest)1与本仓 README.OpenHarmony_CN.md 第 5.10 节完整示例绑定的契约测试——代码与文档必须同步改

第 4 阶段:桥协议建模

4.1 JSON 契约
com.radusalagean.uitextcompose.core/**(纯 Kotlin 算法内核)
        ↓ UiTextValue 蓝图树 / UiTextResolver
Dispatch.kt + MiniJson(commonMain,JVM 可测)
        ↓ UTF-8 JSON
N-API uiTextDispatch()
        ↓
ArkUI Index.ets(Text + Span 渲染)

请求与响应:

请求 : {"op":"resolve","locale":"ro","texts":[ blueprint, ... ]}   // 1 条单蓝图,>1 条自动 compound
       blueprint = {"kind":"raw","text":"..."}
                  | {"kind":"res","template":"Salut, %1$s!","rule":"cmp",
                     "args":[{"value":"Radu","annotations":[ann]}],"annotations":[ann]}
                  | {"kind":"plural","quantity":30,"template":"<已选中的复数形式>",...}
       ann = {"type":"span","color":"#FF2563EB","fontWeight":700,...}
           | {"type":"paragraph","align":"center","lineHeight":30}
           | {"type":"link","url":"https://...","color":"#FF2563EB","underline":true}
响应 : {"ok":true,"op":"resolve","locale":"ro","plain":"Salut, Radu!",
        "segmentCount":2,"segments":[{"text":"Salut, ","span":null,"link":null},
                                      {"text":"Radu","span":{...},"link":null}],"paragraph":null}
       {"ok":false,"op":"resolve","error":"..."}

三个设计决策:其一,模板字符串由 ArkTS 侧提供——鸿蒙资源系统(ResourceManager)在 ArkTS 侧,Kotlin 侧拿不到资源 id,而占位符/注解语义与资源来源无关;其二,复数形式的选择放在 ArkTS 侧(one/few/other 分类),桥只收已选中的模板——与上游"plural 选择委托平台 getQuantityString/getPluralString"的分工一致;其三,手写 MiniJson 递归下降解析器——蓝图树嵌套(blueprint → args → annotations)必须递归解析,qrose 时代的扁平扫描器不够用,而 kotlinx.serialization 不引入,.so 保持零第三方依赖,C ABI 恒为 String → String。

4.2 错误通道

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

@CName("UiTextDispatch")
fun uiTextDispatch(request: String): String {
    val op = try { /* 解析 op */ } catch (t: Throwable) { return err("resolve", ...) }
    return try {
        when (op) { "spec" -> specJson(); "resolve" -> resolveJson(...); else -> err(...) }
    } catch (t: Throwable) {
        err(op, t.message ?: t.toString())   // {"ok":false,"op":"...","error":"..."}
    }
}

非法 JSON、未知 op/kind/type、缺 template/url/texts、%5$s 越界(IndexOutOfBoundsException),全部走同一个 ok:false 通道,ArkTS 侧统一渲染成 解析失败: <error 原文>。

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

5.1 三层桥接
ArkTS
  │ JSON string
  ▼
C++ N-API entry(libuitextcompose_napi.so)
  │ const char*
  ▼
Kotlin/Native C ABI(libohosuitextcompose.so)
  │ UiTextDispatch
  ▼
UTF-8 JSON + DisposeString
5.2 Kotlin/Native 导出函数
// example/nativeApp/src/ohosMain/kotlin/Bridge.kt —— @CName 只是薄包装
@CName("UiTextDispatch")
fun uiTextDispatch(request: String): String {
    return dispatch(request)   // 协议逻辑在 commonMain/Dispatch.kt,JVM 测试覆盖
}
C ABI 符号ArkTS 方法覆盖内核
UiTextDispatchuiTextDispatch(req)resolve(四种蓝图 + 两套规则 + 注解栈)/ spec(版本与能力握手)一口进
5.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
    {"uiTextDispatch", nullptr, NapiUiTextDispatch, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};

回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohosuitextcompose_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohosuitextcompose_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与业务符号),不拖整份生成头。

5.4 CMake 和类型声明
add_library(ohosuitextcompose SHARED IMPORTED)
set_target_properties(ohosuitextcompose PROPERTIES
  IMPORTED_LOCATION ${KN_SO}
  IMPORTED_NO_SONAME TRUE)

add_library(uitextcompose_napi SHARED napi_init.cpp)
target_link_libraries(uitextcompose_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohosuitextcompose)

OHOS_ARCH 为空或 libohosuitextcompose.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。三个必须一致的名字:nm_modname = "uitextcompose_napi" ↔ ArkTS import 'libuitextcompose_napi.so' ↔ index.d.ts 导出 uiTextDispatch。

export const uiTextDispatch: (request: string) => string;

第 6 阶段:ArkUI 宿主

6.1 九场景卡与规则对照

Demo 复刻上游 multiplatform sample 的全部场景(MainViewModel 的 9 个 ExampleEntryModel),外加一张规则对照卡:

卡片蓝图验收点
raw{"kind":"raw","text":"Radu"}单段无样式
resgreeting 模板 + arg("Radu")编号占位符替换
pluralResquantity + 已选复数形式en 单复数 / ro 单-少-多三分类
res · annotatedcartStatus + 嵌套 plural + 嵌套 res「30 products」保留自身样式(带注解嵌套);「coșul…」吃 arg 级红色(无注解嵌套继承外层)
pluralRes · annotated数量参数绿色 + 基础加粗注解栈字段级合并、内层覆盖
compound 1/2/3多蓝图拼接 + 链接 + 段落链接可点(toast 显示 URL)、整段居中行高 30
ToS & Privacylegal 模板 + 两个链接嵌套参数蓝色下划线链接段、最近点击行
规则对照同一模板 Save %%s now: hello %s! 双规则并排android:Save %s now: hello world!;cmp:Save %%s now: hello %s!
6.2 语言与数量热切换
private rebuild(): void {
  // 换 locale 状态 → 换一张模板表(镜像上游 strings.xml en/ro)→ 全部蓝图重新 resolve
  this.resText = this.resolve([resBp]);
  this.pluralText = this.resolve([this.pluralBlueprint()]);
  // ... 12 张卡片全部重发
  this.status = `已解析 · locale=${this.locale} · quantity=${this.quantity}`;
}

罗马尼亚语复数按 CLDR 规则在 ArkTS 侧分类(1 → one;0 与 2…19 → few;其余 → other),quantity 五档切换直接演示 1 produs / 2 produse / 30 de produse 三分类。每张卡片下方还有灰色的 plain: 行——buildString() 与 buildAnnotatedString() 双输出同屏对照。

6.3 渲染与交互边界

分段样式映射成 ArkTS Text + Span:span.color → fontColor、fontWeight(100…900 整数)→ fontWeight、underline/lineThrough → decoration、italic → fontStyle;链接段单独走可点击的 Text 分块(ArkTS 的 Span 不支持点击事件),this.getUIContext().getPromptAction().showToast 弹出目标 URL。段落注解作用于 Text 的 textAlign 与 lineHeight。

四、完整代码对照

4.1 整体架构

com.radusalagean.uitextcompose.core/**(移植自上游 UITextUtil 的纯 Kotlin 内核)
    │ UiTextValue 蓝图树 / UiTextAnnotation 注解模型 / UiTextResolver 占位符算法
example/nativeApp commonMain(Dispatch.kt + MiniJson.kt,JVM 可测)
    │ UiTextDispatch 协议逻辑
example/nativeApp ohosMain(Bridge.kt)
    │ @CName 薄包装
libuitextcompose_napi.so
    │ uiTextDispatch
Index.ets
    │ JSON parse + Text/Span 渲染
    ▼
ArkUI 真机页面(单页滚动 · 12 张卡片)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_ui_text_compose、上游 4 个 Compose 依赖模块移出
gradle/libs.versions.tomlKotlin 2.2.21-1.0.0 + kotlin-multiplatform 插件别名(仅此一条)
build.gradle.kts(根)版本 1.0.0-ohos.1、全局禁用 Ohos*Test* 任务
uitextcompose-core/build.gradle.kts纯 KMP:jvm + ohosArm64 + ohosX64,composeMain 不挂 target
uitextcompose-core/src/commonMain/**(5 文件)纯 Kotlin 算法内核
uitextcompose-core/src/commonTest/**21 项算法交叉验证
uitextcompose-core/src/composeMain/**(4 文件)上游 Compose 层,git mv 停编
example/nativeApp/build.gradle.kts双 ABI sharedLib(baseName=ohosuitextcompose)、jvm() 测试目标、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../commonMain/Dispatch.kt协议逻辑(resolve/spec 分发、蓝图解析、响应序列化)
nativeApp/.../commonMain/MiniJson.kt递归下降 JSON 解析器(JVal 树、\uXXXX、转义)
nativeApp/.../ohosMain/Bridge.kt@CName("UiTextDispatch") 薄包装
nativeApp/.../jvmTest/**12 项桥协议冒烟 + 1 项文档契约
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 验收页面(九场景 + 规则对照 + 双语热切换)
README.OpenHarmony_CN.md鸿蒙适配主文档(16 节,含桥协议全字段表)

4.3 关键 API 对照

层次上游 API桥协议等价物
蓝图UIText { raw("...") }kind:"raw" + text
蓝图res(Res.string.x) { arg(...) }kind:"res" + template + args
蓝图pluralRes(Res.plurals.x, n)kind:"plural" + quantity + 已选模板(无 args 时自动注册数量为 %1$s)
蓝图compound(多条目拼接)texts 数组多条目(>1 自动 Compound)
参数arg("plain") / arg(UIText { ... })args[i].value / args[i].nested(完整蓝图)
注解+SpanStyle / +ParagraphStyle / +LinkAnnotation.Urltype:"span" / "paragraph" / "link"
规则android 模块 / multiplatform 模块rule:"android" / rule:"cmp"
输出buildString() / buildAnnotatedString()响应 plain / segments + paragraph
刷新rememberResourceEnvironment() 语言变化自动重组ArkTS locale 状态变化 → 重新发起 resolve

4.4 ArkTS 与 Kotlin 的边界

const raw = uiTextDispatch(JSON.stringify({
  op: 'resolve', locale: 'ro',
  texts: [{ kind: 'res', template: 'Salut, %1$s!', rule: 'cmp',
            args: [{ value: 'Radu', annotations: [{ type: 'span', color: '#FF2563EB', fontWeight: 700 }] }] }]
}));
const res = JSON.parse(raw);   // res.segments = [{text:'Salut, ',span:null}, {text:'Radu',span:{...}}]
val value = UiTextValue.Res(
    template = "Salut, %1\$s!",
    args = listOf(UiTextArg("Radu", listOf(UiTextAnnotation.Span(color = 0xFF2563EB, fontWeight = 700)))),
    rule = PlaceholderRule.CMP,
)
val resolved = UiTextResolver.resolve(value)   // plain + segments,与桥输出逐字段一致

两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象、AnnotatedString 或未校验的动态结构。

五、关键决策说明

决策 1:停编 composeMain 而不是硬接或重写

compose.ui.text 没有 ohos 制品(molecule 先例也只覆盖 compose.runtime),硬接等于再打一套 Compose 发行版;而在 ArkTS 里重写一套拼串框架则丢掉上游算法的全部测试资产。停编 + 算法移植让两条占位符规则、转义矩阵、注解栈语义以纯 Kotlin 形态继续被 JVM 测试锁定,git mv 保留接回路径。

决策 2:桥出纯数据分段而不是渲染结果

文本库的产物是 AnnotatedString,它的纯数据等价物就是"分段 + 扁平样式 + 链接"列表。相比位图方案,分段数据让 ArkTS Text+Span 原生渲染——清晰、可选中、随系统字号缩放,且与上游 buildString()/buildAnnotatedString() 双输出天然对齐。

决策 3:协议逻辑放 commonMain,JVM 冒烟先行

@CName 是 Native-only,但协议逻辑(JSON 解析、蓝图建模、响应序列化)与平台无关。把它拆进 commonMain、给 nativeApp 加 jvm() 目标,ArkTS 将要走的完整 JSON 链路就能在桌面先跑——本次的三个保真度 bug(plural 自动参数、基础链接覆盖字面段、无注解嵌套继承)全部是这样在真机之前被拦下的。

决策 4:模板与复数选择留在 ArkTS 侧

鸿蒙资源系统在 ArkTS 侧,Kotlin 拿不到资源 id;上游本来就把 plural 分类委托给平台(getQuantityString / getPluralString)。桥只收已本地化、已分类的模板字符串,专注占位符与注解规则——语义边界与上游完全一致。

决策 5:手写 MiniJson,.so 零第三方依赖

蓝图树嵌套(blueprint → args → annotations)需要递归 JSON 解析,扁平扫描器不够;但引入 kotlinx.serialization 会让 .so 多出一串传递依赖。手写约两百行递归下降解析器(JVal 树、字符串转义、\uXXXX),C ABI 恒为 String → String,产物里只有 Kotlin/Native 运行时。

六、测试与验证

6.1 测试环境

本次验证使用 Windows 11(Git Bash)、JDK 21(Temurin jdk-21.0.12.101-hotspot)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。

6.2 JVM 交叉验证

export JAVA_HOME="/c/Program Files/Eclipse Adoptium/jdk-21.0.12.101-hotspot"
./gradlew.bat :uitextcompose-core:jvmTest :example:nativeApp:jvmTest

34 项用例全部通过(构成见第 3.5 节表格)。其中 DocExampleTest 是文档契约测试:它把本仓 README 第 5.10 节的完整示例 JSON 逐段断言(plain、7 个分段、每段的样式字段、合并行为),代码与文档谁漂了都过不了这一关。

6.3 HAP 验证

:uitextcompose-core:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: UiTextDispatch / libohosuitextcompose_symbols 均为全局 T 符号
BuildNativeWithCmake: finished(双 ABI 编出 libuitextcompose_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully

KN so(debugShared 未 strip)arm64 约 3.9MB、x86_64 约 3.6MB,另带匹配 triple 的 libc++_shared.so(约 1.8MB),由 copyKnSoArm64 / copyKnSoX64 任务自动回填 entry/libs/{arm64-v8a,x86_64}。

6.4 功能用例

  1. 启动后版本握手行显示 1.0.0-ohos.1 · upstream 1.0.0 · kotlin 2.2.21-1.0.0(来自真实 Native 调用);
  2. en 下 res 卡显示 Hi, Radu!,plain: 行与分段渲染一致;
  3. quantity 切到 1:pluralRes 卡变 1 product;ro 下切 2 / 30:分别变 2 produse(few)与 30 de produse(other)——罗马尼亚语三分类生效;
  4. res · annotated 卡:Ai 30 de produse în coșul tău de cumpăraturi.——「30 de produse」加粗+绿色(带注解嵌套保留自身样式),「coșul tău de cumpăraturi」红色(无注解嵌套继承 arg 级注解);
  5. compound 2 的 Finalizează comanda 为蓝色下划线链接段,点击弹 toast 显示 https://example.com;
  6. compound 3 整段居中、行高 30(paragraph 注解作用于 Text 层);
  7. ToS 卡两个链接段分别可点,「最近点击」行更新目标 URL;
  8. 规则对照卡:android 行 Save %s now: hello world!、cmp 行 Save %%s now: hello %s!——%s 替换与 %%s 还原的差异并排可见;
  9. 点 English / Română,全部 12 张卡片(含 plain 行)即时变化,状态行同步 已解析 · locale=xx · quantity=xx。

6.5 验证结论

算法内核、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的蓝图解析会话:版本握手、四种蓝图、两套占位符规则、嵌套参数两种语义、注解栈合并、链接与段落样式、双语热切换(见下节效果图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/ui-text-compose.git
cd ui-text-compose
export JAVA_HOME="/c/Program Files/Eclipse Adoptium/jdk-21.0.12.101-hotspot"
./gradlew.bat :uitextcompose-core:jvmTest :example:nativeApp:jvmTest             # 34 项交叉验证
./gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
# 用 DevEco Studio 打开 example/harmonyApp(不要打开仓库根目录),配置签名后 Build HAP(s)
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.uitextcompose -m entry
hdc shell hilog | grep UiText

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算适配,必须让算法内核真正编译成 ohosArm64 / ohosX64 产物——但反过来,只编译不测试也不算:三个保真度 bug 全部是 JVM 冒烟抓出来的,没有一个等到真机;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. Windows 上 gradlew.bat 启动器自身要求 JAVA_HOME——gradle.properties 的 org.gradle.java.home 盖不住启动阶段,报错只有一行 JAVA_HOME is not set;
  4. Kotlin 正则的 lookbehind((?<!%))在 Native 目标上没有支持保证,改手写单字符 lookbehind 扫描器,接受集与上游逐一致;
  5. String.format 是 JVM-only API——Native 侧的 \u 转义要手写 hex 查表;Kotlin 也没有 \f 字面量转义,JSON 解析器里的 form-feed 用字面字符;
  6. 上游嵌套语义有两种情形,不是一条规则:无注解嵌套(getString 返回 String)吃外层 withStyle/withLink,带注解嵌套(AnnotatedString)不被重刷——按"解析结果分段是否全无样式"分流;
  7. 上游 pluralRes 省略 builder 块时自动注册数量参数——桥不移植这个默认行为,无参 plural 蓝图直接越界;
  8. ArkTS 的 TextAlign 枚举成员是 Start / Center / End,没有 Right——.textAlign(... TextAlign.End) 才能编过(编译器会提示 Did you mean ‘RIGHT’,那是字符串近似匹配的误导);
  9. promptAction.showToast 已废弃,换 this.getUIContext().getPromptAction().showToast(...);ArkTS 的 Span 不支持点击事件,链接段要用独立 Text 分块 + FlexWrap 布局;
  10. ArkTS 严格模式要求对象字面量有显式类型标注——蓝图/参数/注解全部先声明 interface Blueprint / Arg / Ann,嵌套字面量靠上下文类型推导;
  11. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套(libuitextcompose_napi.so / libohosuitextcompose.so / libc++_shared.so)缺一不可;
  12. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();nm_modname / import 名 / index.d.ts 三处名字必须一致。

8.2 已知问题

  • 鸿蒙侧无 AnnotatedString / @Composable,桥输出纯数据分段由 ArkTS 渲染——不是在鸿蒙上跑 Compose 文本组件;
  • SpanStyle 大部分字段(fontFamily、letterSpacing、textBaseline、shadow、textGeometricTransform、textIndent、locale 等)未镜像,协议字段可平滑扩展;
  • 段落样式是整段级近似(一个 paragraph 作用于整次解析结果),上游 ParagraphStyle 的块级嵌套语义未在协议里展开;
  • 上游无注解路径走平台 String.format(%% 通用反转义、%d 等非 s 占位符也替换),桥统一走 annotated 算法(只处理 s 占位符、只还原 %%s 形态)——差异已记录在 README 已知限制;
  • uitextcompose-multiplatform / uitextcompose-android / 两个 sample 留盘不进构建(源文件留在树里),本 fork 不发 Maven Central;
  • composeMain 四文件等社区 compose.ui / text 的 ohos 制品与 CMP 1.9.2+ 对齐后接回,桥协议保持不变、ArkTS 侧无感。

8.3 未来优化方向

  • 扩展 span 注解镜像字段(fontFamily / letterSpacing / textGeometricTransform);
  • 把 StyledString(ArkUI 富文本模型)作为可选渲染路径,替代手排 Span 列表;
  • 评估 .har 形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套;
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
  • 补充真机(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本。

九、总结

9.1 核心难点回顾

上游 uitextcompose-core(Compose 层 + 算法混编)
    → 确认 compose.ui/text 无 ohos 制品 → Compose 层 git mv 停编
    → 算法内核纯 Kotlin 移植(两条正则 + 注解栈 + 嵌套语义)
    → 手写 lookbehind 扫描器(规避 Native 正则差异)
    → 根构建替换(cmp_qrose 模式,Kotlin 2.2.21-1.0.0 / Gradle 8.14.1)
    → ohosArm64 / ohosX64 KLIB + JVM 交叉验证(34 项)
    → C ABI(@CName UiTextDispatch)+ 蓝图 JSON 协议 + MiniJson
    → C++ N-API(DisposeString 纪律)
    → ArkTS Text/Span 渲染
    → ArkUI 真机单页验收(九场景 + 规则对照 + 双语热切换)

9.2 封装层次

  • 内核层:移植自上游的蓝图树、占位符扫描器、注解栈合并与嵌套分流(纯 Kotlin,零依赖);
  • 桥层:协议逻辑(commonMain、JVM 可测)+ @CName 薄包装(ohosMain),双 ABI 动态库;
  • N-API 层:参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:资源模板表、蓝图构造、解析分发、Text+Span 渲染与链接交互;
  • DevEco 层:CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 停编是手段,语义不漂才是目的:UI 类库适配的验收物不是"页面能显示",而是"同一输入产出与上游一致的解析结果"——两条占位符规则、转义矩阵、嵌套两种情形都要有测试逐条锁死,连上游怪癖(%%1$s 在 cmp 规则下拆字面量)都原样保留;
  2. 把协议逻辑放进 commonMain,让 JVM 冒烟跑在真机前面:@CName 只是 Native 的壳,JSON 进出的协议本体与平台无关——三个保真度 bug 若等到真机才发现,排查成本至少翻一个数量级;
  3. 桥的形态跟着产物走:条码库出 PNG、图表库出坐标、文本库出分段样式——桥只传数据不传框架,AnnotatedString 的等价物就是 {text, span, link} 列表,ArkTS 侧原生渲染反而保留了可选中与系统字号缩放。

9.4 适配成果

当前适配已完成:

  • ui-text-compose 1.0.0(Apache-2.0)算法内核的双 ABI Kotlin/Native 编译(上游 Compose 层 git mv 停编保留,接回路径清晰);
  • 1 个 C ABI 符号(UiTextDispatch,resolve/spec 双 op)和 C++ N-API 包装层(llvm-nm 验证全局 T 符号);
  • 34 项 JVM 交叉验证全绿(21 算法 + 12 协议 + 1 文档契约)+ HAP 构建(双 ABI 六件套 so)、签名、设备安装和真机验证;
  • DevEco Stage ArkUI 单页验收页面(四种蓝图、两套规则、嵌套语义、链接与段落样式、en/ro 双语热切换、quantity 五档联动);
  • 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。

参考文档

这里写自定义目录标题

欢迎使用Markdown编辑器

你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdown的基本语法知识。

新的改变

我们对Markdown编辑器进行了一些功能拓展与语法支持,除了标准的Markdown编辑器功能,我们增加了如下几点新功能,帮助你用它写博客:

  1. 全新的界面设计 ,将会带来全新的写作体验;
  2. 在创作中心设置你喜爱的代码高亮样式,Markdown 将代码片显示选择的高亮样式 进行展示;
  3. 增加了 图片拖拽 功能,你可以将本地的图片直接拖拽到编辑区域直接展示;
  4. 全新的 KaTeX数学公式 语法;
  5. 增加了支持甘特图的mermaid语法1 功能;
  6. 增加了 多屏幕编辑 Markdown文章功能;
  7. 增加了 焦点写作模式、预览模式、简洁写作模式、左右区域同步滚轮设置 等功能,功能按钮位于编辑区域与预览区域中间;
  8. 增加了 检查列表 功能。

功能快捷键

撤销:Ctrl/Command + Z
重做:Ctrl/Command + Y
加粗:Ctrl/Command + B
斜体:Ctrl/Command + I
标题:Ctrl/Command + Shift + H
无序列表:Ctrl/Command + Shift + U
有序列表:Ctrl/Command + Shift + O
检查列表:Ctrl/Command + Shift + C
插入代码:Ctrl/Command + Shift + K
插入链接:Ctrl/Command + Shift + L
插入图片:Ctrl/Command + Shift + G
查找:Ctrl/Command + F
替换:Ctrl/Command + G

合理的创建标题,有助于目录的生成

直接输入1次#,并按下space后,将生成1级标题。
输入2次#,并按下space后,将生成2级标题。
以此类推,我们支持6级标题。有助于使用TOC语法后生成一个完美的目录。

如何改变文本的样式

强调文本 强调文本

加粗文本 加粗文本

标记文本

删除文本

引用文本

H2O is是液体。

210 运算结果是 1024.

插入链接与图片

链接: link.

图片: Alt

带尺寸的图片: Alt

居中的图片: Alt

居中并且带尺寸的图片: Alt

当然,我们为了让用户更加便捷,我们增加了图片拖拽功能。

如何插入一段漂亮的代码片

去博客设置页面,选择一款你喜欢的代码片高亮样式,下面展示同样高亮的 代码片.

// An highlighted block
var foo = 'bar';

生成一个适合你的列表

  • 项目
    • 项目
      • 项目
  1. 项目1
  2. 项目2
  3. 项目3
  • 计划任务
  • 完成任务

创建一个表格

一个简单的表格是这么创建的:

项目Value
电脑$1600
手机$12
导管$1

设定内容居中、居左、居右

使用:---------:居中
使用:----------居左
使用----------:居右

第一列第二列第三列
第一列文本居中第二列文本居右第三列文本居左

SmartyPants

SmartyPants 是一个文本转换工具,主要功能是将普通的 ASCII 标点符号自动转换为更美观的印刷体标点符号。例如:

原始符号转换后说明
"引号"“引号”直引号变弯引号
'单引号'‘单引号’直单引号变弯单引号
--–两个连字符变短破折号
---—三个连字符变长破折号
...…三个点变省略号

创建一个自定义列表

Markdown
Text-to- HTML conversion tool
Authors
John
Luke

如何创建一个注脚

一个具有注脚的文本。2

注释也是必不可少的

Markdown将文本转换为 HTML。

KaTeX数学公式

您可以使用渲染LaTeX数学表达式 KaTeX:

Gamma公式展示 Γ ( n ) = ( n − 1 ) ! ∀ n ∈ N \Gamma(n) = (n-1)!\quad\forall n\in\mathbb N Γ(n)=(n−1)!∀n∈N 是通过欧拉积分

Γ ( z ) = ∫ 0 ∞ t z − 1 e − t d t   . \Gamma(z) = \int_0^\infty t^{z-1}e^{-t}dt\,. Γ(z)=∫0∞​tz−1e−tdt.

你可以找到更多关于的信息 LaTeX 数学表达式here.

新的甘特图功能,丰富你的文章

2014-01-07 2014-01-09 2014-01-11 2014-01-13 2014-01-15 2014-01-17 2014-01-19 2014-01-21 已完成 进行中 计划一 计划二 现有任务 Adding GANTT diagram functionality to mermaid
  • 关于 甘特图 语法,参考 这儿,

UML图表

可以使用UML图表进行渲染,例如下面产生的一个序列图:

王五 李四 张三 王五 李四 张三 李四想了很长时间, 文字太长了 不适合放在一行. 你好!李四, 最近怎么样? 你最近怎么样,王五? 我很好,谢谢! 我很好,谢谢! 打量着王五... 很好... 王五, 你怎么样?
  • 关于 UML图表 语法,参考 这儿,

流程图

链接

长方形

圆

圆角长方形

菱形

  • 关于 Mermaid 语法,参考 这儿,

FLowchart流程图

我们依旧会支持flowchart.js的流程图语法:

Created with Raphaël 2.3.0 开始 我的操作 确认? 结束 yes no
  • 关于 Flowchart流程图 语法,参考 这儿.

导出与导入

导出

如果你想尝试使用此编辑器, 你可以在此篇文章任意编辑。当你完成了一篇文章的写作, 在上方工具栏找到 文章导出 ,生成一个.md文件或者.html文件进行本地保存。

导入

如果你想加载一篇你写过的.md文件,在上方工具栏可以选择导入功能进行对应扩展名的文件导入,
继续你的创作。


  1. mermaid语法说明 ↩︎

  2. 注脚的解释 ↩︎

Logo

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

更多推荐