本文记录
clikt接入 OpenHarmony 的完整过程,覆盖 KMP 工程盘点、ohosArm64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、命令行解析示例、
HAP 构建、签名和真机验收。
Clikt 本身是 Kotlin Multiplatform 命令行解析库,不是 Compose Multiplatform UI
组件库。因此本次适配复用 KMP 侧的真实解析器和测试模型,使用 ArkUI 作为
OpenHarmony 宿主页面,验证同一份 Kotlin 代码可以从 JVM 进入 OpenHarmony ARM64
运行时,而不是在 ArkTS 页面中重新实现一套解析规则。
项目地址: AtomGit/oh-tpc/clikt
上游仓库: ajalt/clikt
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做开源鸿蒙平台 KMP/CMP 适配
Clikt 的核心能力是把命令行参数解析为类型安全的 Kotlin 值,并统一处理默认值、
选项、flag、校验和错误退出码。原仓库面向 JVM、JS、Wasm、Apple、Linux 和 Windows
等平台,OpenHarmony 侧需要增加真正的 Kotlin/Native 目标,才能证明解析器进入了
OpenHarmony 运行时。
如果只把页面重写成 ArkTS,再在页面里手工判断 --name、--count 和 --verbose,
页面虽然可以显示几个字符串,却无法证明共享 Kotlin 解析器、Native 动态库和 N-API
桥接已经连通。本次适配需要解决以下问题:
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | KMP 模块默认没有 OpenHarmony 目标,必须加入 ohosArm64() 才能生成 KLIB 和动态库。 |
| 工具链不一致 | Kotlin/Native、Gradle、OpenHarmony LLVM、Native SDK 和 DevEco Hvigor 必须使用匹配版本。 |
| 平台边界 | ArkTS 不能直接持有 Kotlin data class,需要使用 C ABI、C++ N-API 和 JSON。 |
| 参数生命周期 | 输入框内容、快捷预设和解析结果必须保持一致,避免页面展示的参数与实际解析参数不同。 |
| 错误可见性 | Clikt 的校验错误、退出码和 Native 异常都要返回页面,不能只在 Native 日志里失败。 |
| 内存释放 | Kotlin/Native 返回的 C 字符串必须由明确的 CliktFree 入口释放。 |
| 交付链路复杂 | Native 动态库、CMake、HAP、签名、设备安装和 ARM64 依赖需要分别验证。 |
因此,本项目把适配边界放在三个地方:Kotlin/Native 目标配置、C ABI/N-API 桥接层和
ArkUI 示例页面。参数解析和错误语义仍由 Clikt 维护,ArkTS 只负责输入、按钮交互、
结果展示和页面状态。
1.2 Clikt 示例提供的能力
example/shared 中的公共示例使用 Clikt 核心 API 定义了一个简单命令:
--name/-n:字符串选项,默认值为OpenHarmony;--count/-c:整数选项,默认值为1,有效范围为1..5;--verbose/-v:flag 选项,用于切换详细输出;CommandLineParser.tokenize:把输入框中的命令行文本拆成参数列表;CliktError:将参数错误转换为可展示的错误文本和退出码;runChecks:在 JVM 示例和 OpenHarmony Native 页面中复用相同的验收检查。
解析结果使用以下结构:
| 字段 | 含义 |
|---|---|
id |
当前结果的来源标识,例如 command 或 preset-1 |
argv |
实际参与解析的参数文本 |
output |
成功时的命令输出,失败时的 Clikt 错误信息 |
success |
是否解析和执行成功 |
exitCode |
成功为 0,参数错误通常为 1 |
页面中的三个按钮职责不同:
| 按钮 | 行为 | 示例 |
|---|---|---|
| 默认参数 | 写入预设并立即解析 | --name OpenHarmony --count 1 |
| 详细模式 | 写入带 flag 的预设并立即解析 | --name Clikt --verbose |
| 解析命令 | 只解析输入框当前内容 | 用户可以自行编辑任意参数 |
1.3 实现适配矩阵
| 维度 | 适配方式 |
|---|---|
| 代码复用 | 公共命令、Clikt 参数声明、tokenize、错误处理和检查逻辑由 Kotlin 共享。 |
| 平台目标 | 为核心 clikt 和示例工程加入 ohosArm64,生成 libclikt.so。 |
| 桥接稳定 | 使用少量 C ABI 函数和 UTF-8 JSON,避免把 Kotlin 对象地址交给 ArkTS。 |
| UI 完整 | 页面支持输入参数、默认预设、详细预设、手动解析、成功结果和错误结果。 |
| 可测试 | JVM 测试、Native 链接、ELF 依赖、HAP 构建、设备安装分别验收。 |
| 签名安全 | 源码只保留签名入口,证书、profile、p12 和密码由开发者本机配置。 |
| 仓库规范 | 项目、文章、截图和示例链接统一使用 AtomGit。 |
本项目没有把 Compose Multiplatform UI 强行移植到 ArkUI。KMP 负责共享 Clikt
解析能力,ArkUI 负责 OpenHarmony 宿主页面;这种边界可以让其他 KMP/CMP 应用复用
Clikt 核心 API,并自行决定页面实现方式。
二、实现路线图
text
第 1 阶段:项目初始化 ── 盘点 KMP 模块、解析示例和 OpenHarmony 工程边界
第 2 阶段:目标与依赖打通 ── 加入 ohosArm64、独立 example 工程和 Native 构建任务
第 3 阶段:解析与序列化 ── 建立 CliktExample、错误状态和稳定 JSON 契约
第 4 阶段:原生桥接 ── Kotlin/Native C ABI、C++ N-API、类型检查和内存释放
第 5 阶段:ArkUI 页面 ── 输入框、预设按钮、手动解析、状态提示和错误展示
第 6 阶段:示例与验证 ── HAP 构建、签名、设备安装、按钮交互和效果图
每个阶段都使用真实产物作为下一阶段输入:先用 JVM 验证 Clikt 解析,再把同一份
commonMain 代码链接为 ARM64 动态库,随后由 CMake 和 N-API 加载到 Stage 工程,
最后安装签名 HAP 观察设备页面上的解析结果。
三、逐步实现过程
第 1 阶段:项目初始化
1.1 盘点公共 API 和工程边界
项目采用清晰的 KMP、Native 和 DevEco 分层:
text
clikt/ Clikt 核心多平台库和 ohosArm64 目标
clikt-mordant/ Mordant 输出扩展
clikt-mordant-markdown/ Markdown 输出扩展
example/shared/ 公共命令示例和 JVM 验收测试
example/nativeApp/ ohosArm64 Kotlin/Native 动态库
example/ohosApp/ DevEco Stage 工程、C++ N-API 和 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 |
| Gradle | 8.14.3 |
根工程和 example 工程构建 |
| JDK | 21 |
Gradle、Kotlin 编译和 Native 任务 |
| OpenHarmony 目标 | ohosArm64 |
ARM64 真机动态库 |
| DevEco product | 6.0.0(20) |
工程兼容和目标版本 |
| DevEco Studio | 6.0.0 Release |
IDE 版本,负责工程同步、HAP 构建与签名 |
| ABI | arm64-v8a |
HAP 原生库架构 |
执行 Gradle 脚本前先选择 JDK 21:
bash
export JAVA_HOME="/path/to/jdk-21"
java -version
OpenHarmony SDK 版本满足编译条件,只说明工程可以生成 HAP;应用是否能安装和运行,
还要由 DevEco 和 ARM64 设备单独验证。
1.3 创建 OpenHarmony 示例目录
示例页面围绕"当前输入、解析结果、退出码、检查状态和最近一次解析"组织:
text
标题区 Clikt 工作台 / KMP NATIVE
输入区 命令行输入框、解析次数和快捷示例
操作区 默认参数 / 详细模式 / 解析命令
结果区 SUCCESS 或 EXIT、输出文本和实际 argv
检查区 公共检查通过数量和 Native 错误文本
快捷按钮用于快速准备可重复的输入,解析按钮用于验证任意手动编辑的命令行。每次
解析都会更新状态文本和刷新次数,避免"参数没有变化所以看起来没有响应"。
第 2 阶段:目标与依赖打通
2.1 在核心模块加入 ohosArm64
clikt/build.gradle.kts 保留所有原有目标,并增加 OpenHarmony ARM64:
kotlin
kotlin {
jvm()
js { nodejs() }
wasmJs { nodejs() }
wasmWasi { nodejs() }
linuxX64()
linuxArm64()
ohosArm64()
mingwX64()
macosX64()
macosArm64()
iosArm64()
iosX64()
iosSimulatorArm64()
}
适配的关键是让公共 commonMain 代码在 ohosArm64 上可编译,而不是复制一份只
能在 ArkTS 中运行的解析实现。Mordant 模块仍然保持原有目标矩阵,因为公开 Mordant
制品没有对应的 OpenHarmony 变体。
2.2 独立 example 工程消费 Clikt
example/shared 使用平台无关的公共源码,并通过项目属性消费本地 Clikt 制品:
kotlin
kotlin {
explicitApi()
jvm()
ohosArm64()
sourceSets {
commonMain.dependencies {
api("com.github.ajalt.clikt:clikt-core:${property("cliktVersion")}")
}
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}
根脚本先发布适配后的本地 Maven 制品,示例工程再使用相同版本执行 JVM 测试和
Native 链接。这样可以确认"被消费的 artifact"与刚刚修改的源码一致。
2.3 构建 Native 动态库
example/nativeApp 生成一个受 linker map 约束的 shared library:
kotlin
kotlin {
ohosArm64 {
binaries.sharedLib {
baseName = "clikt"
linkerOpts("--entry=0", "--version-script=${project.file(
"src/ohosArm64Main/linker/shared-library.map"
)}")
linkerOpts("-lace_napi.z", "-luv", "-lhilog_ndk.z")
}
}
}
prepareOhos 在链接成功后复制两个构建产物:
text
entry/libs/arm64-v8a/libclikt.so
entry/src/main/cpp/include/libclikt_api.h
动态库、生成头文件和 HAP 属于构建产物,仓库会忽略它们;每台开发机都可以从源码
重新生成与本机 SDK 匹配的文件。
第 3 阶段:解析与序列化
3.1 为什么需要统一 JSON 契约
ArkTS、C++ 和 Kotlin/Native 不能直接共享 Kotlin 对象,因此边界使用 UTF-8 JSON:
text
TextInput command line
↓ string
CliktClient.ets
↓ N-API run(commandLine)
C++ N-API entry
↓ C string
Kotlin/Native CliktRun
↓ CliktExamples.execute
CliktExample JSON
↓
ArkUI result card
页面只消费 JSON,不复制 option().default()、.int()、.flag() 和 .check() 的
业务规则。这样 JVM 测试和设备页面都使用同一份 Clikt 解析逻辑。
3.2 公共 DemoCommand
示例命令位于 example/shared/src/commonMain:
kotlin
private class DemoCommand : CoreCliktCommand(name = "clikt-demo") {
private val name by option("--name", "-n").default("OpenHarmony")
private val count by option("--count", "-c")
.int()
.default(1)
.check("count must be between 1 and 5") { it in 1..5 }
private val verbose by option("--verbose", "-v").flag()
var output: String = ""
private set
override fun run() {
val greeting = List(count) { "Hello, $name!" }.joinToString(" ")
output = if (verbose) "[verbose] $greeting" else greeting
}
}
CliktExamples.execute 先调用 CommandLineParser.tokenize,再把参数列表交给
command.parse(argv)。成功结果带 exitCode = 0;CliktError 会被转换为失败结果,
保留错误消息和状态码。
3.3 结果 JSON
正常解析结果保持稳定字段:
json
{
"id": "command",
"argv": "--name HarmonyOS --count 2",
"output": "Hello, HarmonyOS! Hello, HarmonyOS!",
"success": true,
"exitCode": 0
}
Native 异常则返回:
json
{
"error": "Native Clikt error"
}
ArkTS 侧会把缺少正常字段的响应转换为结构完整的失败结果,避免页面渲染时访问
undefined 字段。
3.4 公共检查
CliktExamples.runChecks() 覆盖七项检查:
- 默认选项可以解析;
- 自定义名称和次数可以解析;
- 整数选项控制问候语重复次数;
verboseflag 会改变输出前缀;count = 0会被校验拒绝;- 公共 API 不依赖平台类型;
- OpenHarmony 桥接只传递不可变 JSON 值。
页面将结果展示为 7/7 公共检查通过,这个数字来自 Native 返回,而不是页面写死。
第 4 阶段:原生桥接(技术难点)
4.1 Kotlin/Native 对象不能直接交给 ArkTS
Kotlin/Native 的 CliktExample 属于 Kotlin 运行时对象。ArkTS 只能接收 JavaScript
值,C++ 不能把 Kotlin 对象地址直接当成 JavaScript 对象。
最终采用三层桥接:
text
ArkTS
│ JSON string
▼
C++ N-API entry
│ const char*
▼
Kotlin/Native C ABI
│ CliktExamples
▼
UTF-8 JSON + explicit free
4.2 方案对比
| 方案 | 优点 | 缺点 | 选用 |
|---|---|---|---|
| 直接导出 Kotlin 对象 | 代码少 | ABI、生命周期和类型不可控 | ❌ |
| 只导出解析后的字符串 | 实现简单 | 页面无法展示参数、错误和退出码 | ❌ |
| C ABI + JSON | 边界清晰、易扩展、易调试 | 有一次序列化开销 | ✅ |
| 在 ArkTS 重写完整解析器 | 页面调用简单 | KMP 和 ArkTS 逻辑容易分叉 | ❌ |
4.3 Kotlin/Native 导出函数
NativeBridge.kt 对外导出四个 C ABI 符号:
kotlin
@CName("CliktCatalog")
public fun catalogNative(): CPointer<ByteVar> = response { ... }
@CName("CliktRun")
public fun runNative(commandLine: CPointer<ByteVar>?): CPointer<ByteVar> = response {
CliktExamples.execute(commandLine?.toKString().orEmpty()).toJson()
}
@CName("CliktRunChecks")
public fun checksNative(): CPointer<ByteVar> = response { ... }
@CName("CliktFree")
public fun freeNative(pointer: CPointer<ByteVar>?) {
if (pointer != null) nativeHeap.free(pointer.rawValue)
}
返回值使用 Native heap 分配的、以 0 结尾的 C 字符串。每一块返回缓冲区都由
CliktFree 释放。
4.4 C++ N-API 方法分发
C++ 注册三个 ArkTS 方法:
cpp
napi_property_descriptor methods[] = {
{"getCatalog", nullptr, Catalog, nullptr, nullptr, nullptr,
napi_default, nullptr},
{"run", nullptr, Run, nullptr, nullptr, nullptr,
napi_default, nullptr},
{"runChecks", nullptr, RunChecks, nullptr, nullptr, nullptr,
napi_default, nullptr},
};
run 先读取 ArkTS 字符串长度,再写入可变 std::string 缓冲区,最后调用
CliktRun。ReadNative 创建 ArkTS 字符串后立即调用 CliktFree,保证成功和异常
路径都不泄漏 Native 缓冲区。
CMake 将 Kotlin/Native 动态库作为 imported library:
cmake
add_library(clikt SHARED IMPORTED)
set_target_properties(clikt PROPERTIES
IMPORTED_LOCATION
"${CMAKE_CURRENT_SOURCE_DIR}/../../../libs/arm64-v8a/libclikt.so")
add_library(entry SHARED napi_init.cpp)
target_include_directories(entry PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include")
target_link_libraries(entry PRIVATE clikt libace_napi.z.so)
4.5 N-API 生命周期
text
ArkTS run(commandLine)
│
▼
napi_get_value_string_utf8
│
▼
CliktRun(commandLine)
│
▼
napi_create_string_utf8
│
▼
CliktFree(nativeBuffer)
│
▼
return JS string
C++ 负责完成跨语言字符串转换和 Native 释放,ArkTS 页面不需要知道 Kotlin/Native
的堆实现。
第 5 阶段:ArkUI 页面
5.1 页面状态
Index.ets 保存输入参数、解析结果、检查状态、错误文本和解析次数:
typescript
@State private commandLine: string = '--name OpenHarmony --count 1';
@State private selected: CliktExample = emptyExample();
@State private status: string = '正在连接 Native Clikt';
@State private errorText: string = '';
@State private parseCount: number = 0;
@State private lastAction: string = '等待操作';
页面加载时会自动执行一次 refresh()。输入框的 onChange 只更新命令行文本,真正
点击"解析命令"时才调用 Native run,避免每次键入字符都触发完整解析。
5.2 三个按钮的实现
typescript
private usePreset(commandLine: string): void {
this.refresh(commandLine);
}
Button('默认参数')
.onClick(() => this.usePreset('--name OpenHarmony --count 1'))
Button('详细模式')
.onClick(() => this.usePreset('--name Clikt --verbose'))
Button('解析命令')
.onClick(() => this.refresh(this.commandLine))
预设按钮将参数直接传给 refresh,不会在状态尚未刷新时读取旧的输入框值。手动解析
按钮始终使用当前输入内容,并在结果卡片中显示"解析完成 · 已刷新第 N 次"。
5.3 成功与失败展示
成功时页面显示 SUCCESS、Clikt 输出和实际 argv;失败时显示 EXIT 1、错误文本
和公共检查状态。Native 返回格式错误或 JSON 解析异常也会被 CliktClient.ets
转换为失败结果,不会让页面因为字段缺失直接崩溃。
第 6 阶段:示例与验证
6.1 example 工程结构
text
example/
├── shared/
│ ├── src/commonMain/.../CliktExamples.kt
│ └── src/commonTest/.../CliktExamplesTest.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
└── clikt/CliktClient.ets
shared 验证公共 Clikt 模型,nativeApp 产生 Native 动态库,ohosApp 负责 ArkUI
页面和 N-API 加载。三者边界清晰,任何一层失败都能单独定位。
6.2 原生模块注册
napi_init.cpp 通过 napi_module_register 注册 entry 模块,类型声明位于:
text
example/ohosApp/entry/src/main/cpp/types/libentry/index.d.ts
ArkTS 使用:
typescript
import cliktNative from 'libentry.so';
6.3 Native 动态库准备
执行:
bash
export JAVA_HOME="/path/to/jdk-21"
./scripts/build-openharmony.sh
脚本依次执行根模块测试、示例测试、linkDebugSharedOhosArm64 和 prepareOhos。
成功后生成:
text
example/ohosApp/entry/libs/arm64-v8a/libclikt.so
example/ohosApp/entry/src/main/cpp/include/libclikt_api.h
还可以检查 ARM64 ELF 的强依赖:
bash
python3 scripts/check-native-deps.py \
/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native \
example/ohosApp/entry/libs/arm64-v8a
6.4 构建、签名和安装
未配置签名时,可以在 DevEco Studio 构建本地调试 HAP;真机安装需要有效签名。配置
签名后执行:
bash
python3 scripts/prepare-signing-project.py /absolute/path/clikt-signing
DEVECO_HOME=/Applications/DevEco-Studio.app/Contents \
scripts/build-hap.sh /absolute/path/clikt-signing
产物位于:
text
entry/build/default/outputs/default/entry-default-signed.hap
安装并启动:
bash
HDCP=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/hdc
"$HDCP" install -r \
/absolute/path/clikt-signing/entry/build/default/outputs/default/entry-default-signed.hap
"$HDCP" shell aa start \
-a EntryAbility -b com.github.ajalt.clikt.sample -m entry
签名证书、profile、p12 和密码只保存在本机,不要提交到 AtomGit。
四、完整代码对照
4.1 整体架构
text
TextInput / preset button
│ command-line string
▼
CliktClient.ets
│ libentry.so
▼
C++ N-API
│ C ABI
▼
libclikt.so
│ Kotlin/Native
▼
CliktExamples.execute
│ CoreCliktCommand.parse
▼
CliktExample -> JSON -> ArkUI result card
4.2 文件清单
| 文件 | 职责 |
|---|---|
clikt/src/commonMain/... |
Clikt 核心解析 API |
example/shared/.../CliktExamples.kt |
公共命令示例、JSON 前的结果模型和自检 |
example/shared/.../CliktExamplesTest.kt |
JVM 解析和错误验收 |
example/nativeApp/.../NativeBridge.kt |
C ABI、JSON 返回和内存释放 |
example/ohosApp/.../CliktClient.ets |
N-API 调用、JSON 解析和结果兜底 |
example/ohosApp/.../Index.ets |
输入、预设、解析按钮和结果页面 |
example/ohosApp/.../napi_init.cpp |
N-API 导出、参数读取和 C 字符串转换 |
scripts/build-openharmony.sh |
测试、Native 链接和产物复制 |
scripts/build-hap.sh |
已配置签名工程的 HAP 构建 |
docs/openharmony/VALIDATION.md |
自动检查和真机验收记录 |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| Kotlin | CommandLineParser.tokenize |
将命令行文本拆成参数列表 |
| Kotlin | CoreCliktCommand.parse |
使用 Clikt 解析选项和 flag |
| Kotlin | CliktError |
返回错误文本和退出码 |
| Native | CliktRun |
返回一条命令的 JSON 结果 |
| Native | CliktRunChecks |
返回公共检查 JSON |
| N-API | run |
向 ArkTS 暴露解析方法 |
| ArkTS | refresh |
更新页面输入、结果和状态 |
4.4 ArkTS 与 Kotlin 的边界
ArkTS 只负责输入和界面生命周期:
typescript
const result: CliktExample = run(this.commandLine);
this.selected = result;
Kotlin 负责参数语义、默认值、类型转换、范围校验和输出:
kotlin
val command = DemoCommand()
command.parse(argv)
CliktExample("command", argv.joinToString(" "), command.output, true, 0)
两者之间只传输字符串和 JSON,不传输 Kotlin 对象、ArkTS class 实例或未校验的动态
结构。
五、关键决策说明
决策 1:把 ohosArm64 加入核心构建约定
只有真正链接 ohosArm64 动态库,才能证明共享 Clikt 代码可以进入 OpenHarmony
运行时。只添加一个 ArkTS 页面不能代表 KMP 库已经完成适配。
决策 2:独立消费者必须通过构建产物消费
example 单独解析 Clikt,ohosApp 单独接收 .so 和头文件,避免根工程编译通过
却无法在 DevEco 中打包。
决策 3:JSON 作为跨语言数据契约
JSON 让 ArkTS、C++ 和 Kotlin/Native 的边界清楚可调试,并允许后续增加可信字段而
不暴露内部对象布局。
决策 4:桥接层只开放四个 C ABI 入口
目录、解析、自检和释放已经覆盖示例所需能力;减少 ABI 符号可以降低 Native 生命周期
和兼容风险。
决策 5:快捷预设和手动解析分开
预设按钮用于快速验证固定参数,解析按钮用于验证用户输入。两者都调用同一个
refresh,不会出现页面逻辑和库逻辑各自维护一套解析器的情况。
决策 6:把库验证和设备验证分开
JVM 测试验证 Clikt 规则,Native 链接验证 ABI,Hvigor 验证 HAP,真机验证安装和
ArkUI 页面。每一层都有明确的失败边界。
六、测试与验证
6.1 测试环境
本次真机验证使用:
- macOS;
- JDK 21;
- Kotlin Multiplatform
2.2.21-1.0.0; - DevEco Studio 及 OpenHarmony ARM64 Native SDK;
- DevEco 自带 Node.js 和 Hvigor;
- 已签名
entry-default-signed.hap; - USB 连接的 HarmonyOS ARM64 真机;
hdc设备序列号FMR0223825079397。
6.2 静态检查与单元测试
bash
./gradlew :clikt:jvmTest :test:jvmTest
(cd example && ./gradlew :shared:jvmTest)
测试覆盖默认选项、自定义名称、重复次数、flag、整数范围错误和公共检查列表。
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
本地 DevEco 构建命令:
bash
PATH=/Applications/DevEco-Studio.app/Contents/tools/node/bin:$PATH \
/Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/hvigorw \
assembleApp --no-daemon
验证结果:
text
Hvigor BUILD SUCCESSFUL
entry-default-signed.hap generated
ARM64 native library packaged
6.4 功能验证用例
用例 1:默认页面和自检
启动应用后页面显示默认命令 --name OpenHarmony --count 1、SUCCESS 和公共检查
通过数量。初始结果由 Native run 生成。
用例 2:默认参数按钮
点击"默认参数",输入框恢复默认命令并重新解析,结果为:
text
Hello, OpenHarmony!
用例 3:详细模式按钮
点击"详细模式",结果为:
text
[verbose] Hello, Clikt!
这个用例同时验证 flag 参数已经由 Clikt 处理,而不是页面手工拼接前缀。
用例 4:自定义参数和解析按钮
输入:
text
--name OpenHarmony --count 2
点击"解析命令"后,页面显示两次问候语、实际 argv 和"解析完成 · 已刷新第 N 次"。
用例 5:无效参数
输入 --count 0,页面显示 EXIT 1 和 count must be between 1 and 5。Native
错误不会导致 ArkUI 页面崩溃。
用例 6:重复解析
连续点击"解析命令",结果内容不变但刷新次数递增,证明按钮事件被执行且页面有明确
反馈。这个场景解决了初始默认参数与再次解析结果相同、用户误以为按钮无响应的问题。
6.5 验证结论
公共 JVM 测试、Kotlin/Native ARM64 链接、ELF 依赖检查、Hvigor HAP 构建、签名安装
和真机页面交互均已完成。真机页面可以执行默认、详细和手动参数解析,说明从 Clikt
核心到 ArkTS 展示的链路可用。
七、运行效果
7.1 真机截图

截图中可以看到:
- 页面标题为"Clikt 工作台";
- 副标题说明这是命令行解析示例;
- 顶部标识显示
KMP NATIVE; - 输入框当前为
--name OpenHarmony --count 2; - 快捷按钮提供"默认参数"和"详细模式";
- 主按钮为"解析命令";
- 结果卡片显示两次
Hello, OpenHarmony!; - 状态为
SUCCESS,公共检查为7/7 公共检查通过; - 解析结果下方显示实际参与解析的参数。
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 /absolute/path/clikt-signing
# 安装和启动
hdc install -r \
/absolute/path/clikt-signing/entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b com.github.ajalt.clikt.sample -m entry
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配 :必须把共享 Clikt 代码真正编译成
ohosArm64
动态库。 - N-API 不负责业务判断 :C++ 只做参数检查、字符串转换和释放,参数语义放在
Kotlin Clikt 命令中。 - Native 字符串必须及时释放 :每次
CliktRun、CliktCatalog和
CliktRunChecks返回后都要配对调用CliktFree。 - 解析按钮需要可见反馈 :默认参数已经在页面启动时解析,再次点击若只显示相同
文本,用户会误以为没有响应,因此页面增加了刷新次数和状态提示。 - JDK、Node 和 DevEco 工具链要分开确认 :Gradle 使用 JDK 21,Hvigor 使用
DevEco 自带 Node,避免系统 Node 版本导致 Native 构建异常。 - 签名材料不能进入源码:证书、profile、p12 和密码必须放在独立的本机工程中。
8.2 已知问题
- 当前示例只构建
ohosArm64和arm64-v8a,不覆盖 OpenHarmony x86 或其他 ABI; - Clikt 是命令行解析库,ArkUI 页面是验收宿主,不提供 Compose Multiplatform UI 组件;
- Native 和 HAP 构建依赖本机 OpenHarmony SDK、DevEco 工具链和签名配置;
- 页面采用同步 Native 调用,复杂或长时间运行的命令应在业务层增加异步调度;
- 文章中的签名配置只适用于本地开发机,不能直接复制到其他环境。
8.3 未来优化方向
- 增加
Flow<CliktExample>或异步执行接口,支持更复杂的命令任务; - 为 Compose Multiplatform 宿主补充共享状态卡片示例,保持 ArkUI 与 CMP 的模型一致;
- 增加命令行引号、转义和多行输入的交互测试;
- 增加设备能力、签名状态和 Native 库版本的诊断页面;
- 在持续集成中加入
ohosArm64链接、ELF 依赖检查和 HAP 未签名构建任务; - 将 AtomGit 文档、示例截图和版本发布说明纳入同一套发布流程。
九、总结
9.1 核心难点回顾
本次适配真正需要处理的不是一个按钮,而是一条完整跨端链路:
text
命令行文本
→ ArkUI 输入和页面状态
→ C++ N-API
→ Kotlin/Native C ABI
→ KMP Clikt 核心解析器
→ JSON
→ ArkUI 结果页面
9.2 封装层次
- KMP 层:定义选项、默认值、类型转换、flag、范围检查和错误语义;
- Native 层:生成 ARM64 动态库,并通过 C ABI 输出有限入口;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理输入框、快捷预设、解析动作、结果状态和错误提示;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 先让共享 Clikt 模型在 JVM 和 Native 通过,再接入 ArkUI;
- 用 JSON 和少量 C ABI 代替跨语言对象传递;
- 把自动测试、HAP 构建和真机交互分别记录,避免把"编译成功"误认为"设备运行链路可用"。
9.4 适配成果
当前 Clikt OpenHarmony 适配已完成:
clikt核心模块新增ohosArm64目标;example/shared复用真实 Clikt parser、默认选项、flag 和范围校验;example/nativeApp生成 ARM64libclikt.so和 C ABI 头文件;- Kotlin/Native + C ABI + C++ N-API JSON 桥接;
- ArkUI 页面支持输入、默认参数、详细模式和手动解析;
- 解析成功、退出码、错误文本和公共检查状态可视化;
- HAP 构建、签名安装和 Mate 60 Pro 真机验证;
- AtomGit 项目文档、OpenHarmony 验收记录和真机效果图;
- 仓库中的代码托管地址、项目文档和发布链接已统一使用 AtomGit。
适配过程 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。
参考文档
- AtomGit/oh-tpc/clikt
- Clikt 官方文档
- Clikt Multiplatform 说明
- OpenHarmony N-API
- Kotlin Multiplatform
- DevEco Studio
这里写自定义目录标题
-
- 一、背景
-
- [1.1 为什么做开源鸿蒙平台 KMP/CMP 适配](#1.1 为什么做开源鸿蒙平台 KMP/CMP 适配)
- [1.2 Clikt 示例提供的能力](#1.2 Clikt 示例提供的能力)
- [1.3 实现适配矩阵](#1.3 实现适配矩阵)
- 二、实现路线图
- 三、逐步实现过程
-
- [第 1 阶段:项目初始化](#第 1 阶段:项目初始化)
-
- [1.1 盘点公共 API 和工程边界](#1.1 盘点公共 API 和工程边界)
- [1.2 固定工具链和版本矩阵](#1.2 固定工具链和版本矩阵)
- [1.3 创建 OpenHarmony 示例目录](#1.3 创建 OpenHarmony 示例目录)
- [第 2 阶段:目标与依赖打通](#第 2 阶段:目标与依赖打通)
-
- [2.1 在核心模块加入 `ohosArm64`](#2.1 在核心模块加入
ohosArm64) - [2.2 独立 example 工程消费 Clikt](#2.2 独立 example 工程消费 Clikt)
- [2.3 构建 Native 动态库](#2.3 构建 Native 动态库)
- [2.1 在核心模块加入 `ohosArm64`](#2.1 在核心模块加入
- [第 3 阶段:解析与序列化](#第 3 阶段:解析与序列化)
-
- [3.1 为什么需要统一 JSON 契约](#3.1 为什么需要统一 JSON 契约)
- [3.2 公共 DemoCommand](#3.2 公共 DemoCommand)
- [3.3 结果 JSON](#3.3 结果 JSON)
- [3.4 公共检查](#3.4 公共检查)
- [第 4 阶段:原生桥接(技术难点)](#第 4 阶段:原生桥接(技术难点))
-
- [4.1 Kotlin/Native 对象不能直接交给 ArkTS](#4.1 Kotlin/Native 对象不能直接交给 ArkTS)
- [4.2 方案对比](#4.2 方案对比)
- [4.3 Kotlin/Native 导出函数](#4.3 Kotlin/Native 导出函数)
- [4.4 C++ N-API 方法分发](#4.4 C++ N-API 方法分发)
- [4.5 N-API 生命周期](#4.5 N-API 生命周期)
- [第 5 阶段:ArkUI 页面](#第 5 阶段:ArkUI 页面)
-
- [5.1 页面状态](#5.1 页面状态)
- [5.2 三个按钮的实现](#5.2 三个按钮的实现)
- [5.3 成功与失败展示](#5.3 成功与失败展示)
- [第 6 阶段:示例与验证](#第 6 阶段:示例与验证)
-
- [6.1 example 工程结构](#6.1 example 工程结构)
- [6.2 原生模块注册](#6.2 原生模块注册)
- [6.3 Native 动态库准备](#6.3 Native 动态库准备)
- [6.4 构建、签名和安装](#6.4 构建、签名和安装)
- 四、完整代码对照
-
- [4.1 整体架构](#4.1 整体架构)
- [4.2 文件清单](#4.2 文件清单)
- [4.3 关键 API 对照](#4.3 关键 API 对照)
- [4.4 ArkTS 与 Kotlin 的边界](#4.4 ArkTS 与 Kotlin 的边界)
- 五、关键决策说明
-
- [决策 1:把 `ohosArm64` 加入核心构建约定](#决策 1:把
ohosArm64加入核心构建约定) - [决策 2:独立消费者必须通过构建产物消费](#决策 2:独立消费者必须通过构建产物消费)
- [决策 3:JSON 作为跨语言数据契约](#决策 3:JSON 作为跨语言数据契约)
- [决策 4:桥接层只开放四个 C ABI 入口](#决策 4:桥接层只开放四个 C ABI 入口)
- [决策 5:快捷预设和手动解析分开](#决策 5:快捷预设和手动解析分开)
- [决策 6:把库验证和设备验证分开](#决策 6:把库验证和设备验证分开)
- [决策 1:把 `ohosArm64` 加入核心构建约定](#决策 1:把
- 六、测试与验证
-
- [6.1 测试环境](#6.1 测试环境)
- [6.2 静态检查与单元测试](#6.2 静态检查与单元测试)
- [6.3 原生桥接和 HAP 验证](#6.3 原生桥接和 HAP 验证)
- [6.4 功能验证用例](#6.4 功能验证用例)
-
- [用例 1:默认页面和自检](#用例 1:默认页面和自检)
- [用例 2:默认参数按钮](#用例 2:默认参数按钮)
- [用例 3:详细模式按钮](#用例 3:详细模式按钮)
- [用例 4:自定义参数和解析按钮](#用例 4:自定义参数和解析按钮)
- [用例 5:无效参数](#用例 5:无效参数)
- [用例 6:重复解析](#用例 6:重复解析)
- [6.5 验证结论](#6.5 验证结论)
- 七、运行效果
-
- [7.1 真机截图](#7.1 真机截图)
- [7.2 命令速查](#7.2 命令速查)
- 八、遗留问题与改进方向
-
- [8.1 踩坑复盘](#8.1 踩坑复盘)
- [8.2 已知问题](#8.2 已知问题)
- [8.3 未来优化方向](#8.3 未来优化方向)
- 九、总结
-
- [9.1 核心难点回顾](#9.1 核心难点回顾)
- [9.2 封装层次](#9.2 封装层次)
- [9.3 三条经验](#9.3 三条经验)
- [9.4 适配成果](#9.4 适配成果)
- [适配过程 FAQ](#适配过程 FAQ)
-
- [Q1:DevEco Studio Sync 失败或 ohpm 依赖下载超时,怎么处理?](#Q1:DevEco Studio Sync 失败或 ohpm 依赖下载超时,怎么处理?)
- [Q2:CMake 报 `Missing .../libxxx.so` 或找不到 Kotlin/Native 动态库?](#Q2:CMake 报
Missing .../libxxx.so或找不到 Kotlin/Native 动态库?) - [Q3:ArkTS 报 `Cannot find module 'libxxx_napi.so'` 或调用返回 `undefined`?](#Q3:ArkTS 报
Cannot find module 'libxxx_napi.so'或调用返回undefined?) - [Q4:真机安装报 `signature verification failed` 或 `install failed`?](#Q4:真机安装报
signature verification failed或install failed?) - [Q5:应用启动白屏、闪退或 NAPI 调用崩溃?](#Q5:应用启动白屏、闪退或 NAPI 调用崩溃?)
- [Q6:Gradle 报 `ohosArm64` 目标不存在或 Kotlin/Native 编译失败?](#Q6:Gradle 报
ohosArm64目标不存在或 Kotlin/Native 编译失败?) - [Q7:HAP 构建报 SDK 版本不匹配或 `compatibleSdkVersion` 错误?](#Q7:HAP 构建报 SDK 版本不匹配或
compatibleSdkVersion错误?) - [Q8:命令行 hvigor 构建失败,但 DevEco 里能构建?](#Q8:命令行 hvigor 构建失败,但 DevEco 里能构建?)
- 参考文档
- 欢迎使用Markdown编辑器
欢迎使用Markdown编辑器
你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdown的基本语法知识。
新的改变
我们对Markdown编辑器进行了一些功能拓展与语法支持,除了标准的Markdown编辑器功能,我们增加了如下几点新功能,帮助你用它写博客:
- 全新的界面设计 ,将会带来全新的写作体验;
- 在创作中心设置你喜爱的代码高亮样式,Markdown 将代码片显示选择的高亮样式 进行展示;
- 增加了 图片拖拽 功能,你可以将本地的图片直接拖拽到编辑区域直接展示;
- 全新的 KaTeX数学公式 语法;
- 增加了支持甘特图的mermaid语法[1](#甘特图的mermaid语法1) 功能;
- 增加了 多屏幕编辑 Markdown文章功能;
- 增加了 焦点写作模式、预览模式、简洁写作模式、左右区域同步滚轮设置 等功能,功能按钮位于编辑区域与预览区域中间;
- 增加了 检查列表 功能。
功能快捷键
撤销: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.
图片:
带尺寸的图片:
居中的图片:
居中并且带尺寸的图片:
当然,我们为了让用户更加便捷,我们增加了图片拖拽功能。
如何插入一段漂亮的代码片
去博客设置页面,选择一款你喜欢的代码片高亮样式,下面展示同样高亮的 代码片.
javascript
// An highlighted block
var foo = 'bar';
生成一个适合你的列表
- 项目
- 项目
- 项目
- 项目
- 项目1
- 项目2
- 项目3
- 计划任务
- 完成任务
创建一个表格
一个简单的表格是这么创建的:
| 项目 | Value |
|---|---|
| 电脑 | $1600 |
| 手机 | $12 |
| 导管 | $1 |
设定内容居中、居左、居右
使用:---------:居中
使用:----------居左
使用----------:居右
| 第一列 | 第二列 | 第三列 |
|---|---|---|
| 第一列文本居中 | 第二列文本居右 | 第三列文本居左 |
SmartyPants
SmartyPants 是一个文本转换工具,主要功能是将普通的 ASCII 标点符号自动转换为更美观的印刷体标点符号。例如:
| 原始符号 | 转换后 | 说明 |
|---|---|---|
"引号" |
"引号" | 直引号变弯引号 |
'单引号' |
'单引号' | 直单引号变弯单引号 |
-- |
-- | 两个连字符变短破折号 |
--- |
--- | 三个连字符变长破折号 |
... |
... | 三个点变省略号 |
创建一个自定义列表
:
Text-to- conversion tool
: John
: Luke
如何创建一个注脚
一个具有注脚的文本。[2](#2)
注释也是必不可少的
Markdown将文本转换为 。
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.
新的甘特图功能,丰富你的文章
#mermaid-svg-RzN6beZGUqTf63q2{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-RzN6beZGUqTf63q2 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-RzN6beZGUqTf63q2 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-RzN6beZGUqTf63q2 .error-icon{fill:#552222;}#mermaid-svg-RzN6beZGUqTf63q2 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-RzN6beZGUqTf63q2 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-RzN6beZGUqTf63q2 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-RzN6beZGUqTf63q2 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-RzN6beZGUqTf63q2 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-RzN6beZGUqTf63q2 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-RzN6beZGUqTf63q2 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-RzN6beZGUqTf63q2 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-RzN6beZGUqTf63q2 .marker.cross{stroke:#333333;}#mermaid-svg-RzN6beZGUqTf63q2 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-RzN6beZGUqTf63q2 p{margin:0;}#mermaid-svg-RzN6beZGUqTf63q2 .mermaid-main-font{font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-RzN6beZGUqTf63q2 .exclude-range{fill:#eeeeee;}#mermaid-svg-RzN6beZGUqTf63q2 .section{stroke:none;opacity:0.2;}#mermaid-svg-RzN6beZGUqTf63q2 .section0{fill:rgba(102, 102, 255, 0.49);}#mermaid-svg-RzN6beZGUqTf63q2 .section2{fill:#fff400;}#mermaid-svg-RzN6beZGUqTf63q2 .section1,#mermaid-svg-RzN6beZGUqTf63q2 .section3{fill:white;opacity:0.2;}#mermaid-svg-RzN6beZGUqTf63q2 .sectionTitle0{fill:#333;}#mermaid-svg-RzN6beZGUqTf63q2 .sectionTitle1{fill:#333;}#mermaid-svg-RzN6beZGUqTf63q2 .sectionTitle2{fill:#333;}#mermaid-svg-RzN6beZGUqTf63q2 .sectionTitle3{fill:#333;}#mermaid-svg-RzN6beZGUqTf63q2 .sectionTitle{text-anchor:start;font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-RzN6beZGUqTf63q2 .grid .tick{stroke:lightgrey;opacity:0.8;shape-rendering:crispEdges;}#mermaid-svg-RzN6beZGUqTf63q2 .grid .tick text{font-family:"trebuchet ms",verdana,arial,sans-serif;fill:#333;}#mermaid-svg-RzN6beZGUqTf63q2 .grid path{stroke-width:0;}#mermaid-svg-RzN6beZGUqTf63q2 .today{fill:none;stroke:red;stroke-width:2px;}#mermaid-svg-RzN6beZGUqTf63q2 .task{stroke-width:2;}#mermaid-svg-RzN6beZGUqTf63q2 .taskText{text-anchor:middle;font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-RzN6beZGUqTf63q2 .taskTextOutsideRight{fill:black;text-anchor:start;font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-RzN6beZGUqTf63q2 .taskTextOutsideLeft{fill:black;text-anchor:end;}#mermaid-svg-RzN6beZGUqTf63q2 .task.clickable{cursor:pointer;}#mermaid-svg-RzN6beZGUqTf63q2 .taskText.clickable{cursor:pointer;fill:#003163!important;font-weight:bold;}#mermaid-svg-RzN6beZGUqTf63q2 .taskTextOutsideLeft.clickable{cursor:pointer;fill:#003163!important;font-weight:bold;}#mermaid-svg-RzN6beZGUqTf63q2 .taskTextOutsideRight.clickable{cursor:pointer;fill:#003163!important;font-weight:bold;}#mermaid-svg-RzN6beZGUqTf63q2 .taskText0,#mermaid-svg-RzN6beZGUqTf63q2 .taskText1,#mermaid-svg-RzN6beZGUqTf63q2 .taskText2,#mermaid-svg-RzN6beZGUqTf63q2 .taskText3{fill:white;}#mermaid-svg-RzN6beZGUqTf63q2 .task0,#mermaid-svg-RzN6beZGUqTf63q2 .task1,#mermaid-svg-RzN6beZGUqTf63q2 .task2,#mermaid-svg-RzN6beZGUqTf63q2 .task3{fill:#8a90dd;stroke:#534fbc;}#mermaid-svg-RzN6beZGUqTf63q2 .taskTextOutside0,#mermaid-svg-RzN6beZGUqTf63q2 .taskTextOutside2{fill:black;}#mermaid-svg-RzN6beZGUqTf63q2 .taskTextOutside1,#mermaid-svg-RzN6beZGUqTf63q2 .taskTextOutside3{fill:black;}#mermaid-svg-RzN6beZGUqTf63q2 .active0,#mermaid-svg-RzN6beZGUqTf63q2 .active1,#mermaid-svg-RzN6beZGUqTf63q2 .active2,#mermaid-svg-RzN6beZGUqTf63q2 .active3{fill:#bfc7ff;stroke:#534fbc;}#mermaid-svg-RzN6beZGUqTf63q2 .activeText0,#mermaid-svg-RzN6beZGUqTf63q2 .activeText1,#mermaid-svg-RzN6beZGUqTf63q2 .activeText2,#mermaid-svg-RzN6beZGUqTf63q2 .activeText3{fill:black!important;}#mermaid-svg-RzN6beZGUqTf63q2 .done0,#mermaid-svg-RzN6beZGUqTf63q2 .done1,#mermaid-svg-RzN6beZGUqTf63q2 .done2,#mermaid-svg-RzN6beZGUqTf63q2 .done3{stroke:grey;fill:lightgrey;stroke-width:2;}#mermaid-svg-RzN6beZGUqTf63q2 .doneText0,#mermaid-svg-RzN6beZGUqTf63q2 .doneText1,#mermaid-svg-RzN6beZGUqTf63q2 .doneText2,#mermaid-svg-RzN6beZGUqTf63q2 .doneText3{fill:black!important;}#mermaid-svg-RzN6beZGUqTf63q2 .doneText0.taskTextOutsideLeft,#mermaid-svg-RzN6beZGUqTf63q2 .doneText0.taskTextOutsideRight,#mermaid-svg-RzN6beZGUqTf63q2 .doneText1.taskTextOutsideLeft,#mermaid-svg-RzN6beZGUqTf63q2 .doneText1.taskTextOutsideRight,#mermaid-svg-RzN6beZGUqTf63q2 .doneText2.taskTextOutsideLeft,#mermaid-svg-RzN6beZGUqTf63q2 .doneText2.taskTextOutsideRight,#mermaid-svg-RzN6beZGUqTf63q2 .doneText3.taskTextOutsideLeft,#mermaid-svg-RzN6beZGUqTf63q2 .doneText3.taskTextOutsideRight{fill:black!important;}#mermaid-svg-RzN6beZGUqTf63q2 .crit0,#mermaid-svg-RzN6beZGUqTf63q2 .crit1,#mermaid-svg-RzN6beZGUqTf63q2 .crit2,#mermaid-svg-RzN6beZGUqTf63q2 .crit3{stroke:#ff8888;fill:red;stroke-width:2;}#mermaid-svg-RzN6beZGUqTf63q2 .activeCrit0,#mermaid-svg-RzN6beZGUqTf63q2 .activeCrit1,#mermaid-svg-RzN6beZGUqTf63q2 .activeCrit2,#mermaid-svg-RzN6beZGUqTf63q2 .activeCrit3{stroke:#ff8888;fill:#bfc7ff;stroke-width:2;}#mermaid-svg-RzN6beZGUqTf63q2 .doneCrit0,#mermaid-svg-RzN6beZGUqTf63q2 .doneCrit1,#mermaid-svg-RzN6beZGUqTf63q2 .doneCrit2,#mermaid-svg-RzN6beZGUqTf63q2 .doneCrit3{stroke:#ff8888;fill:lightgrey;stroke-width:2;cursor:pointer;shape-rendering:crispEdges;}#mermaid-svg-RzN6beZGUqTf63q2 .milestone{transform:rotate(45deg) scale(0.8,0.8);}#mermaid-svg-RzN6beZGUqTf63q2 .milestoneText{font-style:italic;}#mermaid-svg-RzN6beZGUqTf63q2 .doneCritText0,#mermaid-svg-RzN6beZGUqTf63q2 .doneCritText1,#mermaid-svg-RzN6beZGUqTf63q2 .doneCritText2,#mermaid-svg-RzN6beZGUqTf63q2 .doneCritText3{fill:black!important;}#mermaid-svg-RzN6beZGUqTf63q2 .doneCritText0.taskTextOutsideLeft,#mermaid-svg-RzN6beZGUqTf63q2 .doneCritText0.taskTextOutsideRight,#mermaid-svg-RzN6beZGUqTf63q2 .doneCritText1.taskTextOutsideLeft,#mermaid-svg-RzN6beZGUqTf63q2 .doneCritText1.taskTextOutsideRight,#mermaid-svg-RzN6beZGUqTf63q2 .doneCritText2.taskTextOutsideLeft,#mermaid-svg-RzN6beZGUqTf63q2 .doneCritText2.taskTextOutsideRight,#mermaid-svg-RzN6beZGUqTf63q2 .doneCritText3.taskTextOutsideLeft,#mermaid-svg-RzN6beZGUqTf63q2 .doneCritText3.taskTextOutsideRight{fill:black!important;}#mermaid-svg-RzN6beZGUqTf63q2 .vert{stroke:navy;}#mermaid-svg-RzN6beZGUqTf63q2 .vertText{font-size:15px;text-anchor:middle;fill:navy!important;}#mermaid-svg-RzN6beZGUqTf63q2 .activeCritText0,#mermaid-svg-RzN6beZGUqTf63q2 .activeCritText1,#mermaid-svg-RzN6beZGUqTf63q2 .activeCritText2,#mermaid-svg-RzN6beZGUqTf63q2 .activeCritText3{fill:black!important;}#mermaid-svg-RzN6beZGUqTf63q2 .titleText{text-anchor:middle;font-size:18px;fill:#333;font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-RzN6beZGUqTf63q2 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 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图表进行渲染,例如下面产生的一个序列图:
王五 李四 张三 王五 李四 张三 #mermaid-svg-XZp5fhCHTapzTeNN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-XZp5fhCHTapzTeNN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-XZp5fhCHTapzTeNN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-XZp5fhCHTapzTeNN .error-icon{fill:#552222;}#mermaid-svg-XZp5fhCHTapzTeNN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-XZp5fhCHTapzTeNN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-XZp5fhCHTapzTeNN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-XZp5fhCHTapzTeNN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-XZp5fhCHTapzTeNN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-XZp5fhCHTapzTeNN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-XZp5fhCHTapzTeNN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-XZp5fhCHTapzTeNN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-XZp5fhCHTapzTeNN .marker.cross{stroke:#333333;}#mermaid-svg-XZp5fhCHTapzTeNN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-XZp5fhCHTapzTeNN p{margin:0;}#mermaid-svg-XZp5fhCHTapzTeNN .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-XZp5fhCHTapzTeNN text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-XZp5fhCHTapzTeNN .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-XZp5fhCHTapzTeNN .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-XZp5fhCHTapzTeNN .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-XZp5fhCHTapzTeNN .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-XZp5fhCHTapzTeNN #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-XZp5fhCHTapzTeNN .sequenceNumber{fill:white;}#mermaid-svg-XZp5fhCHTapzTeNN #sequencenumber{fill:#333;}#mermaid-svg-XZp5fhCHTapzTeNN #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-XZp5fhCHTapzTeNN .messageText{fill:#333;stroke:none;}#mermaid-svg-XZp5fhCHTapzTeNN .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-XZp5fhCHTapzTeNN .labelText,#mermaid-svg-XZp5fhCHTapzTeNN .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-XZp5fhCHTapzTeNN .loopText,#mermaid-svg-XZp5fhCHTapzTeNN .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-XZp5fhCHTapzTeNN .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-XZp5fhCHTapzTeNN .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-XZp5fhCHTapzTeNN .noteText,#mermaid-svg-XZp5fhCHTapzTeNN .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-XZp5fhCHTapzTeNN .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-XZp5fhCHTapzTeNN .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-XZp5fhCHTapzTeNN .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-XZp5fhCHTapzTeNN .actorPopupMenu{position:absolute;}#mermaid-svg-XZp5fhCHTapzTeNN .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-XZp5fhCHTapzTeNN .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-XZp5fhCHTapzTeNN .actor-man circle,#mermaid-svg-XZp5fhCHTapzTeNN line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-XZp5fhCHTapzTeNN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 李四想了很长时间, 文字太长了 不适合放在一行. 你好!李四, 最近怎么样? 你最近怎么样,王五? 我很好,谢谢! 我很好,谢谢! 打量着王五... 很好... 王五, 你怎么样?
- 关于 UML图表 语法,参考 这儿,
流程图
#mermaid-svg-Zouu2xvAWs8wyeI8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Zouu2xvAWs8wyeI8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Zouu2xvAWs8wyeI8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Zouu2xvAWs8wyeI8 .error-icon{fill:#552222;}#mermaid-svg-Zouu2xvAWs8wyeI8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Zouu2xvAWs8wyeI8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Zouu2xvAWs8wyeI8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Zouu2xvAWs8wyeI8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Zouu2xvAWs8wyeI8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Zouu2xvAWs8wyeI8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Zouu2xvAWs8wyeI8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Zouu2xvAWs8wyeI8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Zouu2xvAWs8wyeI8 .marker.cross{stroke:#333333;}#mermaid-svg-Zouu2xvAWs8wyeI8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Zouu2xvAWs8wyeI8 p{margin:0;}#mermaid-svg-Zouu2xvAWs8wyeI8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Zouu2xvAWs8wyeI8 .cluster-label text{fill:#333;}#mermaid-svg-Zouu2xvAWs8wyeI8 .cluster-label span{color:#333;}#mermaid-svg-Zouu2xvAWs8wyeI8 .cluster-label span p{background-color:transparent;}#mermaid-svg-Zouu2xvAWs8wyeI8 .label text,#mermaid-svg-Zouu2xvAWs8wyeI8 span{fill:#333;color:#333;}#mermaid-svg-Zouu2xvAWs8wyeI8 .node rect,#mermaid-svg-Zouu2xvAWs8wyeI8 .node circle,#mermaid-svg-Zouu2xvAWs8wyeI8 .node ellipse,#mermaid-svg-Zouu2xvAWs8wyeI8 .node polygon,#mermaid-svg-Zouu2xvAWs8wyeI8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Zouu2xvAWs8wyeI8 .rough-node .label text,#mermaid-svg-Zouu2xvAWs8wyeI8 .node .label text,#mermaid-svg-Zouu2xvAWs8wyeI8 .image-shape .label,#mermaid-svg-Zouu2xvAWs8wyeI8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-Zouu2xvAWs8wyeI8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Zouu2xvAWs8wyeI8 .rough-node .label,#mermaid-svg-Zouu2xvAWs8wyeI8 .node .label,#mermaid-svg-Zouu2xvAWs8wyeI8 .image-shape .label,#mermaid-svg-Zouu2xvAWs8wyeI8 .icon-shape .label{text-align:center;}#mermaid-svg-Zouu2xvAWs8wyeI8 .node.clickable{cursor:pointer;}#mermaid-svg-Zouu2xvAWs8wyeI8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Zouu2xvAWs8wyeI8 .arrowheadPath{fill:#333333;}#mermaid-svg-Zouu2xvAWs8wyeI8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Zouu2xvAWs8wyeI8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Zouu2xvAWs8wyeI8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Zouu2xvAWs8wyeI8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Zouu2xvAWs8wyeI8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Zouu2xvAWs8wyeI8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Zouu2xvAWs8wyeI8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Zouu2xvAWs8wyeI8 .cluster text{fill:#333;}#mermaid-svg-Zouu2xvAWs8wyeI8 .cluster span{color:#333;}#mermaid-svg-Zouu2xvAWs8wyeI8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Zouu2xvAWs8wyeI8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Zouu2xvAWs8wyeI8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-Zouu2xvAWs8wyeI8 .icon-shape,#mermaid-svg-Zouu2xvAWs8wyeI8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Zouu2xvAWs8wyeI8 .icon-shape p,#mermaid-svg-Zouu2xvAWs8wyeI8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Zouu2xvAWs8wyeI8 .icon-shape .label rect,#mermaid-svg-Zouu2xvAWs8wyeI8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Zouu2xvAWs8wyeI8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Zouu2xvAWs8wyeI8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Zouu2xvAWs8wyeI8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 链接
长方形
圆
圆角长方形
菱形
- 关于 Mermaid 语法,参考 这儿,
FLowchart流程图
我们依旧会支持flowchart.js的流程图语法:
Created with Raphaël 2.3.0 开始 我的操作 确认? 结束 yes no
- 关于 Flowchart流程图 语法,参考 这儿.
导出与导入
导出
如果你想尝试使用此编辑器, 你可以在此篇文章任意编辑。当你完成了一篇文章的写作, 在上方工具栏找到 文章导出 ,生成一个.md文件或者.html文件进行本地保存。
导入
如果你想加载一篇你写过的.md文件,在上方工具栏可以选择导入功能进行对应扩展名的文件导入,
继续你的创作。
-
注脚的解释 ↩︎
*[HTML]: 超文本标记语言