应用接近发布时,最令人头疼的并不总是业务代码。两个分支使用同一份页面源码,一个构建正常,另一个在依赖更新后出现样式偏差;或者测试同学确认的是昨天的组件组合,发布分支今天重新安装依赖,实际落入包内的版本已经变化。问题表面上像是页面回归,追查起来却要沿着依赖声明、锁定文件、安装结果和发布记录逐层核对。

我把依赖版本检查放到应用发布检查流程的起点,而不是等构建结束再追问哪个库发生变化。这里沿用一个项目管理应用的发布工作台:业务工程仍保留 entry 页面与公共组件模块,发布脚本负责收集候选依赖快照、对照上一次确认的基线,并把需要解释的差异交给负责人。这样,构建输入是否一致可以在提交发布包以前得到清楚的答复。

依赖锁定与版本漂移检查

从三份信息看依赖版本

HarmonyOS 工程中,oh-package.json5 负责描述依赖关系,既有工程级配置,也有模块级配置。模块的 dependencies 用于编译和运行所需的依赖,devDependencies 服务于开发和测试阶段,dynamicDependencies 则用于对应的动态 HSP 场景。把三种类型混在一起统计,会让发布清单出现大量与运行内容无关的变化,也容易遗漏确实需要纳入版本管理的运行依赖。

oh-package-lock.json5 保存已解析依赖树的准确版本信息。华为文档建议将它与工程一起提交版本控制,同时明确不建议用自行编写的分析工具直接读取、修改其内部内容。因此发布工作台采用一种更稳妥的分工:由 ohpm 维护锁定文件,通过 ohpm list 核对安装依赖,业务脚本只处理人工审阅或流水线整理后的规范化快照。快照属于发布流程的输入,不是 ohpm 的内部存储格式。

发布分支应检查锁定文件是否随依赖声明一起提交,安装操作完成后再查看实际依赖列表。对于更新动作,不能把每次 ohpm update 当成无成本的自动步骤,因为它可能根据版本范围推进子依赖。更新本身应当对应一个明确的需求或修复记录,并进入差异审查。

为发布快照确定稳定字段

依赖清单最少保存包名称、准确版本、用途和来源。名称区分大小写,版本保留三段语义化编号,用途区分运行、开发、动态,来源区分仓库与本地模块。这里的用途由工程声明和负责人的实际使用情况决定,不应单凭包名猜测。对于本地模块,还要保留模块对应的源码修订,避免两个版本号相同而实现发生变化。

同一个依赖可能经由多个上层组件引入;如果采集的是依赖树,应该先决定是保留每条路径还是只做去重的版本摘要。发布门禁选择后者作为主表,并将父依赖路径留在排查材料中。这样同一个包的两个冲突版本不会被覆盖:若同名出现两个准确版本,就把它列为冲突,而不是静默取最后一条。

在规范化快照中,排序也很重要。如果每次导出都沿用工具输出的临时顺序,比较报告会充满与版本无关的行变化。采用名称、版本和用途组成稳定排序键,再生成内容指纹,可以把审查重点集中到真实的输入差异上。

以下 Node.js 片段使用发布工作台中的示例快照。包名以 @sample 开头,用于说明清单结构;实际工程需要从当前 ohpm 安装结果整理相应字段。

const crypto = require('node:crypto');
const baseline = [
  { name: '@sample/ui-kit', version: '1.2.0', scope: 'runtime' },
  { name: '@sample/picture-kit', version: '1.4.0', scope: 'runtime' },
  { name: '@sample/checker', version: '0.9.0', scope: 'dev' },
  { name: '@sample/metrics', version: '0.8.0', scope: 'runtime' }
];
const candidate = [
  { name: '@sample/picture-kit', version: '1.5.0', scope: 'runtime' },
  { name: '@sample/ui-kit', version: '1.2.0', scope: 'runtime' },
  { name: '@sample/checker', version: '0.9.0', scope: 'dev' },
  { name: '@sample/metrics', version: '0.8.0', scope: 'runtime' }
];
function normalize(rows) {
  const validScope = new Set(['runtime', 'dev', 'dynamic']);
  const keys = new Set();
  return rows.map(row => {
    if (!/^\d+\.\d+\.\d+$/.test(row.version) || !validScope.has(row.scope)) {
      throw new Error(`依赖字段不符合发布清单格式: ${row.name}`);
    }
    const key = `${row.name}|${row.scope}`;
    if (keys.has(key)) throw new Error(`同名用途条目重复: ${key}`);
    keys.add(key);
    return { name: row.name, version: row.version, scope: row.scope };
  }).sort((a, b) => a.name.localeCompare(b.name) || a.scope.localeCompare(b.scope));
}
function fingerprint(rows) {
  return crypto.createHash('sha256').update(JSON.stringify(normalize(rows))).digest('hex');
}

把差异转成可处理的问题

一份有用的报告不应只有“通过”和“失败”。新增依赖可能意味着新增权限或许可证复核;删除依赖需要检查是否有代码残留;版本变化必须对应允许变更清单;用途从开发转为运行则应重新检查最终构建内容。即便更新的是小版本,也应记录变更理由,而不能用版本号的最后一位判断一定安全。

开发环境中的依赖对比视图

另一个容易忽略的情况是“锁定文件存在,但候选快照仍与基线不同”。这通常反映锁定文件被有意更新、重新解析后出现差异,或收集快照使用了另一套工程输入。门禁不负责判断更新一定不合理,而是要求把版本变更与对应的审查记录对齐。

第二段代码按包名和用途建立索引,输出新增、移除和版本变化。对同名包出现不同用途的情况,索引会把它拆成两个事件,方便进一步判断依赖分类是否调整。

function compareSnapshots(before, after) {
  const makeMap = rows => new Map(normalize(rows).map(x => [`${x.name}|${x.scope}`, x]));
  const left = makeMap(before);
  const right = makeMap(after);
  const changes = [];
  for (const key of new Set([...left.keys(), ...right.keys()])) {
    const oldItem = left.get(key);
    const nextItem = right.get(key);
    if (!oldItem) changes.push({ key, kind: 'added', to: nextItem.version });
    else if (!nextItem) changes.push({ key, kind: 'removed', from: oldItem.version });
    else if (oldItem.version !== nextItem.version) {
      changes.push({ key, kind: 'changed', from: oldItem.version, to: nextItem.version });
    }
  }
  return changes.sort((a, b) => a.key.localeCompare(b.key));
}
const changes = compareSnapshots(baseline, candidate);
console.log(changes);

让允许变更具有可追溯性

日常开发中允许依赖升级,但发布流程不宜只让负责人口头确认。工作台可以维护一张简短的批准表,键值写清“哪个包、哪个用途、批准到哪个版本”。对于新增或删除依赖,则应明确审核备注,避免一个旧版本升级批准条目意外放行了另一个包的新增。

下面的检查只接受准确命中的版本变化,其余差异保留为阻断项。这里不依赖 ohpm 的内部对象,也不对版本字符串做宽松的大小关系推断,因此输入和输出容易审阅。

const approved = new Map([
  ['@sample/picture-kit|runtime', '1.5.0']
]);
function auditVersionChanges(records, allowed) {
  return records.map(record => {
    const accepted = record.kind === 'changed' && allowed.get(record.key) === record.to;
    return { ...record, decision: accepted ? 'reviewed' : 'hold' };
  });
}
const decisions = auditVersionChanges(changes, approved);
console.log({ baselineHash: fingerprint(baseline), candidateHash: fingerprint(candidate) });
console.log(decisions);
if (decisions.some(item => item.decision === 'hold')) process.exitCode = 1;

工程接入和排错顺序

实际落地时,可以把三段代码合并为独立的 release-gate.js,把示例数组替换成经审阅的输入文件,再由构建流程在安装依赖后运行。扫描失败要区分三类:清单解析失败、版本变化未批准、输入版本缺少来源记录。不同错误对应不同责任人,比统一报一个“依赖异常”更容易处理。

如果出现同名包多版本冲突,应该回到 ohpm list 的依赖路径找出引入方;如果锁定文件与版本控制分支不一致,则先检查提交历史和更新命令,而不是编辑锁定文件。涉及本地 HAR 或源码依赖时,还应确认它们的源码修订和构建输入,必要时把这部分信息增加到快照字段。发布包使用的依赖状态不能仅靠 UI 页面展示的版本表来确认。

手机端的发布前依赖核对页面

这里还要注意异常后的退出行为:输入缺字段应立即中断版本对比;文件读取失败不能默认为空列表;允许清单过期不应自动回退到“全部通过”;出现变化后生成新报告不能覆盖旧报告的追溯记录。对开发者而言,依赖门禁的价值不是让升级更困难,而是让每一次升级都有清楚的上下文。

下一步如何接入发布清单

版本漂移治理解决了“用了什么版本”,接下来还要回答“这些依赖的许可证是否已核对、是否包含需要向用户披露的数据处理行为”。因此发布工作台会复用同一份依赖身份与版本快照,在下一阶段补齐许可来源、使用范围、数据类型和隐私文案映射。版本发生变化时,相应的许可与披露信息也需要重新检查,不能沿用旧包的结论。

设计中最重要的取舍,是让 ohpm 管理依赖解析,让发布工作台管理审查结果。两者职责分开,升级有记录、差异可解释,也降低了把工具内部格式当成稳定接口的维护成本。

参考华为开发者文档:

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐