Deepmerge 跨端三方库仓颉适配全流程

本文记录 deepmerge 4.3.1 适配仓颉(Cangjie)的完整过程。适配目标是得到一个可以被仓颉项目直接依赖的跨端三方库,并在终端中用真实示例验证嵌套对象合并、数组拼接、源值覆盖、深拷贝和自定义数组策略等能力。

这类适配与 OpenHarmony 原生三方库不同:仓颉版本不依赖 ArkTS、HAP、设备传感器或系统权限,而是通过 cjpm 组织源码、通过静态类型表达 JavaScript 的常用深度合并语义,再以静态库形式供其他仓颉项目复用。

项目地址: AtomGit/nutpi_cj/deepmerge

上游仓库: TehShrike/deepmerge

开发工具: 华为云码道

一、背景

1.1 为什么适配 deepmerge

deepmerge 是 JavaScript 生态中用于递归合并对象的轻量工具库。它会合并嵌套对象,默认拼接数组,在类型冲突时使用源值,并且不会修改传入的目标对象和源对象。配置系统、请求参数、主题数据和状态快照都可以使用这种行为。

JavaScript 版本依赖动态对象、异构数组、符号键和运行时类型判断,而仓颉强调静态类型、泛型和显式的空值处理。直接把 JavaScript 源码逐行改成仓颉,难点不在语法替换,而在于重新确定对象、数组、标量和自定义策略的类型边界。

本项目把 deepmerge 4.3.1 的核心行为映射为 DeepValue 值树,保留 deepMerge、deepMergeAll、merge 和 mergeAll 等熟悉入口,并提供适合仓颉函数值的自定义数组合并接口。

1.2 跨端三方库的适配边界

当前项目提供的是仓颉跨端库,不绑定某一个 UI 框架或操作系统。适配边界可以概括为:

边界 仓颉实现
包管理 cjpm.toml、cjpm.lock、cjpm build 和 target/release/deepmerge_cangjie
数据表示 DeepValue、HashMap<String, DeepValue> 和 ArrayList<DeepValue>
对象访问 get、set 和显式的 Option<DeepValue>
数组合并 默认拼接和 (ArrayList<DeepValue>, ArrayList<DeepValue>) -> ArrayList<DeepValue> 策略
复制语义 clone 深拷贝,合并结果不复用输入容器
交付形态 仓颉静态库 libdeepmerge_cangjie.a 和接口文件 deepmerge_cangjie.cjo
验证方式 test/main.cj 测试入口和 example/example.cj 终端示例

这里没有 HAP 构建、ArkTS 桥接和真机系统能力验收。只要目标平台提供兼容版本的仓颉 SDK,就可以按同一套源码和 cjpm 配置构建并消费这个库。原始 JavaScript 文件、TypeScript 类型文件和 npm 构建目录继续保留在项目中,不参与仓颉包的源码扫描。

1.3 适配后的能力

当前版本围绕 deepmerge 的核心语义提供以下仓颉入口:

  • 值类型:NullValue、BoolValue、IntValue、FloatValue、StringValue、ObjectValue 和 ArrayValue;
  • 构造与访问:emptyObject、emptyArray、arrayOf、get 和 set;
  • 深度操作:clone、isEqual、deepMerge 和 deepMergeAll;
  • 兼容命名:merge 和 mergeAll 保留原库常用的调用习惯;
  • 自定义策略:deepMergeWithArrayMerge 接受显式的数组合并函数,支持覆盖数组或实现其他策略。

这些入口覆盖对象递归合并、数组默认拼接、类型冲突覆盖、输入不可变和多对象折叠等常见场景。JavaScript 选项中的动态 customMerge、isMergeableObject 和 clone: false 不直接伪装成仓颉 API,具体兼容边界在第七节说明。

二、实现路线图

text 复制代码
第 1 阶段:盘点 API       ── 对照 deepmerge 的合并语义,确定仓颉值类型和目录边界
第 2 阶段:建立 cjpm 工程 ── 固定包名、版本、源码目录和静态库输出
第 3 阶段:类型映射       ── 用 DeepValue、HashMap、ArrayList 和 Option 表达动态数据
第 4 阶段:实现合并核心   ── 完成对象递归、数组策略、深拷贝和兼容别名
第 5 阶段:测试与核对     ── 编译测试入口,执行嵌套合并、不可变和策略断言
第 6 阶段:终端演示       ── 构建 example,直接输出真实合并结果和效果图

每个阶段都有可检查的产物:工程阶段得到 cjpm.toml,实现阶段得到 target/release/deepmerge_cangjie,验证阶段得到可执行的测试入口,最终示例则输出一组可以直接观察的 deepmerge 仓颉运行结果。

三、逐步实现过程

第 1 阶段:盘点原库 API 和仓颉工程边界

1.1 识别动态 API

deepmerge 的 JavaScript API 接收任意对象和值。对象键可能来自原型链或可枚举符号,数组元素也可以是任意不同类型;customMerge 还能根据属性名返回另一个运行时函数。仓颉没有与 JavaScript Any 完全等价的无类型容器,因此本适配先选择可以由一个静态值树稳定表达的范围,再为数组策略提供明确的函数类型。

1.2 固定仓颉项目结构

以下目录树按照当前项目的仓颉适配文件整理,只展示仓颉相关文件;原始 deepmerge JavaScript/TypeScript 源码、测试和构建目录保持原位置,不在此处重复列出:

text 复制代码
deepmerge/
├── cjpm.toml
├── cjpm.lock
├── README.Cangjie.md
├── README.Cangjie_CN.md
├── src/
│   └── deepmerge.cj
├── test/
│   └── main.cj
└── example/
    ├── cjpm.toml
    ├── cjpm.lock
    └── example.cj

为了符合"只新增仓颉相关文件"的目录约束,所有仓颉实现集中在 src/deepmerge.cj。测试入口和示例工程独立于库源码,原有目录不移动、不重命名,也不改写原有 JavaScript 文件。

第 2 阶段:创建 cjpm 工程

根目录 cjpm.toml 使用静态库输出类型,并将源码目录、包名和版本固定下来:

toml 复制代码
[package]
  cjc-version = "1.1.3"
  name = "deepmerge_cangjie"
  version = "4.3.1"
  output-type = "static"
  src-dir = "src"
  target-dir = "target"

[dependencies]

适配包沿用 deepmerge 的 4.3.1 版本标识,包名使用仓颉可接受的 deepmerge_cangjie。构建成功后,主要产物是:

text 复制代码
target/release/deepmerge_cangjie/libdeepmerge_cangjie.a
target/release/deepmerge_cangjie/deepmerge_cangjie.cjo

其中 .a 用于链接,.cjo 提供编译器需要的接口信息。独立示例使用本地路径依赖:

toml 复制代码
[dependencies]
  deepmerge_cangjie = { path = ".." }

这样可以在不改变原 npm 工程的情况下,从 example/ 直接验证仓颉包的消费方式。

第 3 阶段:把 JavaScript 动态值映射为仓颉类型

3.1 数组和对象

JavaScript 的对象和数组需要一个统一的静态表示。本项目用枚举覆盖常见 JSON-like 值,并用仓颉集合承载递归结构:

cangjie 复制代码
public enum DeepValue {
    | NullValue
    | BoolValue(Bool)
    | IntValue(Int64)
    | FloatValue(Float64)
    | StringValue(String)
    | ObjectValue(HashMap<String, DeepValue>)
    | ArrayValue(ArrayList<DeepValue>)
}

对象使用 HashMap<String, DeepValue>,数组使用 ArrayList<DeepValue>。这样递归函数只需要处理一个明确的枚举类型,不需要引入仓颉无法静态检查的无类型 Any。

3.2 用 Option<T> 表示可能不存在的值

JavaScript 中读取不存在的对象属性通常得到 undefined。仓颉版本的 get 返回 Option<DeepValue>,调用方可以通过 Some 和 None 区分字段存在与否:

cangjie 复制代码
match (get(result, "name")) {
    case Some(StringValue(name)) => println("name: ${name}")
    case _ => println("name is missing")
}

这让空对象、缺失字段和错误的数据分支在编译期可见,而不是等到运行时触发空值错误。

3.3 回调函数保持参数类型

deepmerge 默认拼接数组,但 JavaScript 调用方也可以通过 arrayMerge 覆盖策略。仓颉版本把策略固定为两个 ArrayList<DeepValue> 到一个 ArrayList<DeepValue> 的函数值:

cangjie 复制代码
let overwritten = deepMergeWithArrayMerge(left, right, { _, source => source })

策略函数返回什么,合并结果就采用什么数组;默认的 deepMerge 则使用内部的克隆拼接策略,保证输入数组不会被修改。

第 4 阶段:分层实现源码

4.1 核心文件 src/deepmerge.cj

核心文件定义 DeepValue,并实现对象、数组、标量和空值的递归分支。deepMergeWithArrayMerge 先判断两个值的形状:两个对象递归合并,两个数组交给策略函数,其他组合复制源值覆盖目标值。

cangjie 复制代码
public func deepMerge(target: DeepValue, source: DeepValue): DeepValue {
    deepMergeWithArrayMerge(target, source, concatArrays)
}

对象分支先深拷贝目标字段,再逐个处理源字段;源字段不存在于目标时直接复制,两个字段都存在时递归处理。这保留了 deepmerge 的"目标字段保留、源字段覆盖或递归合并"语义。

4.2 扩展能力:深拷贝、对象访问和值比较

clone 递归复制对象和数组,标量则直接复制;get 提供安全字段读取,set 返回设置字段后的新对象,不修改原对象;isEqual 递归比较两个 DeepValue 值树,用于测试合并结果和不可变性。

cangjie 复制代码
let next = set(original, "version", IntValue(1))
let unchanged = isEqual(get(original, "version") ?? NullValue, NullValue)

这些辅助函数把对象访问和复制逻辑集中起来,避免示例和测试直接修改底层 HashMap,也让合并结果的所有权关系更容易检查。

4.3 兼容入口:多对象合并和别名

deepMergeAll 从空对象开始按顺序折叠值;后面的值按照 deepMerge 规则覆盖或递归合并。为了降低从 JavaScript 迁移的记忆成本,merge 和 mergeAll 作为同义入口保留:

cangjie 复制代码
let result = mergeAll([first, second, third])

deepMergeWithArrayMerge 是仓颉版本对原库 arrayMerge 能力的静态化表达,避免模拟 JavaScript 中按属性名动态返回函数的运行时行为。

第 5 阶段:建立自动化测试

test/main.cj 是一个直接链接仓颉静态库的可执行测试程序,覆盖以下行为:

测试组 示例
嵌套对象 foo.bar 和 foo.baz 递归合并
数组 默认数组拼接,结果长度为 3
类型和值 源字段覆盖、字符串字段复制
不可变性 合并后目标值仍保持原始内容
自定义策略 用源数组覆盖目标数组
多对象 deepMergeAll 与两对象合并结果一致

断言失败会抛出异常并以非零状态退出;全部通过时输出固定标记,便于脚本或 CI 判断:

text 复制代码
ALL DEEPMERGE CANGJIE TESTS PASS

由于用户要求测试文件位于根目录 test/main.cj,它不属于 cjpm 默认扫描的 src/*_test.cj 目录。实际验证时可以用上文静态库路径直接调用 cjc 编译测试入口;cjpm test 是否自动发现它取决于 SDK 的测试扫描规则。

第 6 阶段:核对 deepmerge 仓颉导出名称

源码中的公开入口集中在 src/deepmerge.cj,包括 DeepValue 和 12 个顶层公开函数:构造、访问、复制、比较、深度合并、自定义数组策略以及兼容别名。每个入口都在 README.Cangjie.md、测试或示例中有对应的使用说明。

这个核对用于发现公开函数的拼写遗漏和文档遗漏,不把声明数量当成 JavaScript 动态语义已经一比一完成。行为仍以仓颉签名、测试断言和下文的兼容边界为准。

四、完整验证步骤

以下命令均以仓库根目录 deepmerge/ 为当前目录。先确认已经安装的仓颉 SDK:

bash 复制代码
source ~/.zshrc
echo "$CANGJIE_HOME"
cjc --version
cjpm --version

然后检查并构建静态库:

bash 复制代码
cjpm check
cjpm build

编译并运行自动化测试入口:

bash 复制代码
cjc test/main.cj \
  --import-path target/release/deepmerge_cangjie \
  -L target/release/deepmerge_cangjie \
  -ldeepmerge_cangjie \
  -o /tmp/deepmerge-cangjie-test
/tmp/deepmerge-cangjie-test

再构建并运行独立的终端示例工程:

bash 复制代码
cd example
cjpm update
cjpm build
cjpm run

示例会直接调用 deepMerge、get 和 DeepValue 分支并打印结果,而不是打印预先写死的文本。cjpm 可能提示 target/ 没有 .cj 文件,这是目录扫描警告,不影响 cjpm build success 和示例运行。

五、运行效果

下面的效果图来自 example/example.cj 的真实仓颉程序运行,包含本地静态库构建后的 deepmerge Cangjie 调用结果:

截图中的示例输出为:

text 复制代码
cjpm update success
cjpm build success
merged numbers: 3 items
name: Cangjie
cjpm run finished

merged numbers: 3 items 说明 [1, 2] 与 [3] 按默认策略完成了数组拼接;name: Cangjie 说明源对象字段被复制到新结果。终端中的目录扫描 Warning 只表示 target/ 中没有 .cj 文件,不是构建或程序运行失败。

六、关键决策说明

6.1 为什么不用无类型 Any

仓颉适配的价值在于让调用方在编译期知道数据形状。若所有参数都退化成无类型值,虽然表面上接近 JavaScript,却会失去对象字段、数组元素和自定义策略返回值的静态检查。因此项目使用 DeepValue、HashMap<String, DeepValue>、ArrayList<DeepValue> 和明确的函数值签名,让常见调用保持可读、可检查和可预测。

6.2 为什么保留 deepmerge 函数名

保留 deepMerge、deepMergeAll、merge 和 mergeAll 等名称可以降低迁移成本。熟悉 deepmerge 的开发者只需要适应仓颉的枚举分支、集合构造和 Option<DeepValue> 返回值,不必重新记忆一套完全不同的深度合并入口。

6.3 为什么把测试和示例分开

测试程序负责断言嵌套对象、数组策略和不可变性,适合自动化执行;示例工程负责展示,适合在终端中观察真实结果和截图。两者都使用同一个 deepmerge 仓颉静态库,因此运行效果来自库的实际调用,而不是模拟输出。

七、兼容边界和遗留问题

JavaScript deepmerge 与仓颉的运行时模型不同,以下限制需要在接入时提前了解:

  1. JavaScript 的任意动态值、异构对象、原型链和符号键没有直接的仓颉静态等价物;当前对象操作以 HashMap<String, DeepValue> 为边界。
  2. DeepValue 支持的对象和数组是 JSON-like 值树,不直接承载 JavaScript 的 Date、RegExp、函数、DOM 节点或自定义原型实例。
  3. 仓颉的 ArrayList<DeepValue> 可以在值层面包含不同分支,但不会复刻 JavaScript 数组的任意运行时对象行为;更复杂的业务结构需要显式定义自己的枚举或结构体。
  4. deepMergeWithArrayMerge 只提供静态数组策略。原版按属性名动态选择 customMerge 的能力没有直接映射,调用方需要在业务层先确定策略函数。
  5. 原版的 isMergeableObject 和 clone: false 选项没有作为无类型开关暴露;仓颉版本固定使用 DeepValue 判断和深拷贝,优先保证结果不修改输入。
  6. deepMergeAll 面向对象值的常见合并场景,空输入从空对象开始;如果业务数据是专门的顶层数组,应直接使用 deepMerge 并提供数组策略。
  7. 数组策略可以返回调用方构造的容器,因此策略函数应自行决定是否克隆输入;默认 deepMerge 使用内部克隆拼接策略,不会共享输入数组。

这些是当前适配的语义边界,不是构建错误。只要 cjpm build 成功、测试程序退出码为 0、示例输出 merged numbers: 3 items 和 name: Cangjie,就说明当前版本已经完成本地编译和代表性行为验收。

八、接入方式

在另一个仓颉项目中,可以将本仓库作为本地依赖使用。先在本仓库根目录生成 target/release/deepmerge_cangjie,再把相同的 --import-path 和 -L 参数传给调用方的 cjc 命令。调用代码导入:

cangjie 复制代码
import deepmerge_cangjie.*
import std.collection.*

main(): Int64 {
    let left = ObjectValue(HashMap<String, DeepValue>([
        ("user", ObjectValue(HashMap<String, DeepValue>([("name", StringValue("Cangjie"))])))
    ]))
    let right = ObjectValue(HashMap<String, DeepValue>([
        ("user", ObjectValue(HashMap<String, DeepValue>([("version", IntValue(1))])))
    ]))
    let result = deepMerge(left, right)
    println(isEqual(result, deepMergeAll([left, right])))
    return 0
}

如果使用 cjpm 管理依赖,则在业务工程的 cjpm.toml 中加入本地路径依赖:

toml 复制代码
[dependencies]
  deepmerge_cangjie = { path = "../deepmerge" }

然后执行:

bash 复制代码
cjpm update
cjpm build
cjpm run

跨端项目可以复用相同的 DeepValue、合并函数和测试思路,具体的线程、定时器、文件系统或 UI 能力由宿主平台负责。

九、总结

本次适配完成了 deepmerge 4.3.1 到仓颉静态类型模型的迁移:工程结构、单文件核心实现、自动化测试入口、独立示例工程、双语 README 和 API 入口核对均已完成。对象使用 HashMap<String, DeepValue>,数组使用 ArrayList<DeepValue>,可能缺失的字段使用 Option<DeepValue>,数组策略通过带明确参数类型的函数值表达。

验证链路包括 cjpm check、cjpm build、测试入口编译和独立终端示例运行。最终效果图来自真实的仓颉可执行程序,能够直观看到三方库在嵌套对象、数组拼接和源字段复制场景中的输出。对于无法一比一映射 JavaScript 动态语义的部分,项目通过静态签名和文档边界保持行为可解释,方便后续继续扩展。

适配过程 FAQ

以下问题来自仓颉工程创建、cjpm 依赖、与 HarmonyOS 设备联调的实际流程。

Q1:cjpm 依赖解析失败或拉取超时?

  • 检查 cjpm.toml 中的依赖坐标与版本号;
  • 配置可用的仓颉包仓库镜像或代理;
  • 删除 target、cjpm.lock 后重新执行 cjpm update / cjpm build。

Q2:编译报找不到仓颉 SDK 或 cjc 版本不匹配?

  • 设置 CANGJIE_HOME 指向仓颉 SDK 根目录;
  • 确认 cjc --version、cjpm --version 与工程要求的版本一致;
  • 清理构建缓存后重新生成。

Q3:与 HarmonyOS 设备联调时安装失败?

  • 用 hdc list targets 确认设备在线;
  • 确认 HAP 已签名,bundleName 与签名 profile 一致;
  • 设备需加入调试 profile 的 deviceIds。

Q4:动态库或 FFI 符号找不到?

  • 检查导出符号名称、ABI 与打包路径;
  • 确认 .so 已随 HAP 一起打包到 libs/<abi>/;
  • 用 llvm-readelf -d 检查依赖库是否齐全。

Q5:测试用例在设备上失败,但本机通过?

  • 对比设备与主机的架构、SDK 版本和环境变量;
  • 用 hdc hilog 抓取运行日志;
  • 优先复现最小用例,确认是否为平台差异或时序问题。

十、参考资料

相关推荐
Patrick在香港1 小时前
同一行 strftime,两台机器跑出两种日期
python·datetime·时区·locale·日期处理
2601_967987371 小时前
用Python+OpenCV+YOLO写个游戏脚本:从自动瞄准到自动采集的保姆级教程
python·opencv·yolo·目标检测·游戏自动化
meiying88381 小时前
快递单号批量查询Python脚本:从手动到自动化的完整实现
开发语言·python·自动化
人工智能导论实践课1 小时前
【GUI桌面开发3】PyCharm配置外部工具
ide·python·pycharm
一不小心扯到淡1 小时前
键盘演奏:我写了一个键盘音效引擎
程序员·github
穆梓兰煊2 小时前
Rust 在 AI 基础设施中的边界:从 WASI Preview 2 到可插拔推理组件
github
一千柯橘2 小时前
LangChain 的实时输出处理: stream_events 和 Streaming
python·langchain·agent
贾公子2 小时前
图片 AIGC 检测接入实战——URL 与 Base64 的取舍、体积校验与「你送过去的可能不是那张图」
python·aigc
Ka1ros2 小时前
可暂停的Agent编排引擎
github