一个团队同时维护手机、平板和折叠屏界面时,最麻烦的依赖问题不一定表现为编译失败。有时两位同事各自执行一次安装,应用都能打开,但某个三方UI库的实际解析版本不同;随后截图、回归结果和线上问题都无法对应到同一份依赖环境。排查时如果只看oh-package.json5中的版本范围,很容易误以为“写的一样,装的自然一样”。

这篇把焦点放在锁文件与安装快照之间的差异,而不是继续讨论上一轮做过的HAR资源同名问题。我给团队预设了一个DepFence发布预检页:任务DF-808,目标发布批次R2.7.0,审核8个直接依赖条目,7项一致、1项版本漂移。@demo/photo-view期望2.3.0,当前快照却是2.3.1,因此团队内部门禁输出HOLD。包名和数量均为构造数据,不代表真的从远程ohpm仓库安装或完成审核。

一、别把声明版本和已解析版本混为一谈

华为官方ohpm文档说明,ohpm install可以根据工程的oh-package.json5安装依赖,存在oh-package-lock.json5时,安装还受到锁文件中版本记录的影响;官方ohpm list可列出安装的库及依赖关系。由此能得到一个工程上很实用的判断:依赖声明是意图,锁文件代表解析约束,而真正参与本次构建的安装结果仍应留下可核对的快照。

例如依赖声明写^2.3.0,并不意味着所有人永远安装到2.3.0。更新、重建锁文件或切换源后,都可能让可解析版本发生变化。反过来,锁文件文本变化也不一定意味着二进制行为变化,排序或元数据调整都可能改变文本。机器应该报告差异,而不能单靠某个摘要不同就宣告三方库有漏洞。

DepFence只负责发布前的可复现性证据。不处理许可证归属,不执行自动漏洞扫描,也不把“团队批准依赖版本”伪装成“应用市场官方审核标准”。这三层责任要分开,否则工具很容易越做越大,最终没人相信它的结果。

二、冻结发布基线,而不是事后补写正确答案

实际落地时,我会在确定发布候选构建后生成release/deps-baseline.json,记录经过团队批准的库名、精确版本与发布批次;再从本轮构建环境导出release/deps-resolved.json。后者可以由CI适配官方ohpm list --json结果后形成,但字段映射属于团队脚本,不能臆测ohpm JSON在所有版本下的内部结构。

第一段代码示范最小输入契约。为了让八条数据可读,这里通过数组给出八个经人工批准的直接依赖,实际名称都是演示包;不使用*或范围比较来冒充精确锁定。正式项目应把间接依赖也纳入版本证据,并把两份快照与构建工件关联。

const baseline = {
  release: 'R2.7.0',
  deps: [
    ['@demo/photo-view', '2.3.0'], ['@demo/layout-grid', '1.8.0'],
    ['@demo/text-utils', '3.1.2'], ['@demo/chart-mini', '0.9.5'],
    ['@demo/date-format', '1.2.1'], ['@demo/cache-core', '2.0.0'],
    ['@demo/route-link', '1.0.4'], ['@demo/error-view', '1.6.3']
  ]
};
// resolved 与 baseline 同形;仅 @demo/photo-view 为 2.3.1。

这里把名称和版本组成二元组,便于人眼检查差异;大规模项目可使用明确的对象字段,同时附加源、完整性校验信息、许可证人工确认记录。最重要的是基线先于预检结果冻结。如果脚本发现漂移后又自动更新基线,版本门禁就会变成永远放行的表面功夫。

三、用精确比较替代模糊的“好像没变”

检查器不应只遍历基线并寻找同名条目,还要确认解析快照里没有重复包名、缺少版本或多余包。不能因为名单中七个匹配项都对了,就忘了第八个条目。对于@demo/photo-view,2.3.0与2.3.1无论改动大小,都是“本次不符合已冻结基线”的明确事实;是否接受这个升级则属于下一步的评估决策。

第二段代码用普通Node.js比较两份结构化快照,返回所有差异,并让空快照或重复名称直接抛错。所有状态名是应用自定义结果,不是ohpm命令输出的官方枚举。

function toMap(rows) {
  if (!Array.isArray(rows) || rows.length === 0) throw new Error('EMPTY_DEPS');
  const map = new Map();
  for (const [name, version] of rows) {
    if (!name || !version || map.has(name)) throw new Error('INVALID_DEP');
    map.set(name, version);
  }
  return map;
}

function compareDeps(expected, actual) {
  const a = toMap(expected), b = toMap(actual);
  const names = new Set([...a.keys(), ...b.keys()]);
  return [...names].filter(name => a.get(name) !== b.get(name))
    .map(name => ({ name, expected: a.get(name) ?? null, actual: b.get(name) ?? null }));
}

这个实现有意没有做“自动更新”。如果预检阶段直接将当前快照替换成审批基线,后续追踪就失去意义。正确路径是先冻结差异报告,核对三方库升级日志和实际使用的接口,必要时做针对性回归,再由负责人决定更新依赖基线还是恢复旧版本。恢复版本时也应重新安装与重新构建,而不是只把JSON里的数字改回去。

如果当前快照有未知库,names并集会把它识别为expected=null;如果基线库在当前环境缺失,则actual=null。这两种情况都不能被统计为“版本漂移1项”而一笔带过,报表里应该保留准确原因。示例的DF-808只有一个确定的版本不一致,其他七个条目精确匹配,因此不会混入缺失项或未知项。

四、锁文件摘要和语义差异不是同一层证据

团队往往希望给锁文件做一个SHA-256,防止无意覆盖。这是有效的文件完整性手段,但摘要只告诉我们字节是否相同,并不能证明解析后的所有库都具有相同的行为。配置文件里一个空格变动就可能改变摘要,而包本身可能没有发生变化。因此我建议报告同时保存锁文件摘要和结构化依赖差异,不能让其中一个完全替代另一个。

第三段示意发布门禁写入:从两份经过解析的JSON快照比较,报告包括目标批次、匹配数量、漂移条目和HOLD状态;如果状态不满足基线,设置非零退出码。写盘失败本身也要判为预检失败,不能输出绿色完成标记。node:fs/promises是Node.js本地构建辅助代码,不会自动运行在鸿蒙应用进程。

import { readFile, writeFile, mkdir } from 'node:fs/promises';
const expected = JSON.parse(await readFile('release/deps-baseline.json', 'utf8'));
const resolved = JSON.parse(await readFile('release/deps-resolved.json', 'utf8'));
if (expected.release !== resolved.release) throw new Error('RELEASE_MISMATCH');
const drift = compareDeps(expected.deps, resolved.deps); // 调用上段自定义函数
const report = {
  taskId: 'DF-808', release: expected.release, total: expected.deps.length,
  matched: expected.deps.length - drift.length, drift,
  gate: drift.length ? 'HOLD' : 'READY'
};
await mkdir('reports', { recursive: true });
await writeFile('reports/dep-audit.json', JSON.stringify(report, null, 2));
if (report.gate !== 'READY') process.exitCode = 2;

有一个细节不能藏在示例后面:如果存在未知库和缺失库,matched应改为逐项交集的真实匹配数,而不是简单的“总数减差异数”;本文构造数据只有单一版本漂移,所以这个简化公式在DF-808里成立。项目要走到生产时,可以用a.get(name) === b.get(name)逐项求值,再分别统计missing、extra和versionChanged,消除数量上的歧义。

五、手机上的HOLD只是团队的提醒牌

DepFence演示页面展示八个已批准的依赖和本次解析结果。固定任务DF-808、目标R2.7.0、总数8、匹配7、漂移1、问题库@demo/photo-view、期望2.3.0、当前2.3.1、门禁HOLD。三条视觉日志统一为16:41:10 target=R2.7.0 deps=8、16:41:11 matched=7 drift=1与16:41:11 gate=HOLD module=@demo/photo-view。

这个手机界面不意味着移动应用能在用户设备上执行ohpm install。真实项目里,检查脚本在开发机或CI上运行,移动端只展示预先准备的结构化报告;如果不需要移动端查看,完全可以省略这一层UI。图里的模拟器和HiLog都是为了让技术文章讲清状态而绘制的演示,并非真实构建日志。

六、把回归检查变成可重复的发布习惯

上线前至少要覆盖四条验收路径:完全一致应返回READY;单一版本变动应为HOLD;包名缺失或新增必须写入差异报告;空文件、重复ID或JSON损坏必须以错误结束,而不是退化为“无需检查”。如果切换远程源或清理oh_modules,还要重新执行官方ohpm install并记录本轮锁文件、工具版本和依赖解析快照,不能混用上一轮的结果。

多分支并行发布时,报告输出应带发布批次目录,避免两个流水线争写同一份reports/dep-audit.json。当用户从检查页返回、再打开另一个发布分支,旧任务的迟到结果也只能进入历史记录,不能覆盖新的DF-809判断。界面上的HOLD只表示当前DF-808还没有通过团队自定义准入条件,必须由后续复核动作明确解除。

最终要保留两个结论:ohpm提供依赖声明、安装和锁定机制,本文比较与门禁是团队自行开发的外层质量约束;版本差异并不自动等于缺陷,但对需要复现的候选构建来说,未经解释的漂移足以暂停发布。本轮没有连接真实ohpm远程仓库、执行鸿蒙编译或提交应用市场审核,文章所有数字均属于设计好的离线Demo。

参考资料:华为ohpm install命令 https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/ide-hmos-ohpm-install ;ohpm list命令 https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/ide-ohpm-list ;依赖项配置 https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/ide-hmos-hvigor-dependencies ;Node.js文件读写 https://nodejs.org/api/fs.html 。

Logo

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

更多推荐