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