一项动效功能已经能展示,并不意味着第三方库的升级可以直接进入发布分支。内容卡片页面看上去只有“开始播放”和“停止播放”两个动作,实际交付时却可能受到传递依赖变化、资源打包方式调整、停止操作语义变化等因素影响。特别是旧版正常、新版偶发失败的场景,如果只留下新版安装包而没有可复现的旧版构建,所谓“紧急回退”就会变成重新猜测依赖关系。

前面三篇已经建立了一条稳定工程主线:依赖准入表包含 approvedVersion、licenseReviewed、sdkCompatible、hasOwner 和 needsUnexpectedPermission;EffectDriver 仅提供 start(resourceId): Promise<void> 与 stop(): void;CallbackEffectDriver 桥接真实库回调,PreviewEffectDriver 为控制器提供受控替身;CardEffectController 使用 epoch 与 tail 处理异步请求先后;ObservedEffectDriver 只记录启动或停止阶段的异常。最后一步不打算重新发明这套接口,而是回答一个交付问题:如何让依赖升级可以审查、可以比较,并在出问题时回到上一份可靠构建。

HarmonyOS 三方库升级与回退技术封面

一、升级的最小单位不是版本号,而是可复现的构建基线

设想评审时只看到一行版本变化:2.x 换成 3.x。这只能证明声明发生变化,不能证明安装结果、资源文件或编译行为相同。ohpm 的生产依赖应放在真正需要该库的模块 oh-package.json5 的 dependencies 中,安装后产生的解析结果则由锁文件记录。开发阶段为了查问题可以执行 ohpm list -d 2 看传递依赖;进入升级审查时,依赖声明、锁文件、SDK 配置和适配器源码必须作为同一次变更的证据。

这个原则有一个容易忽略的细节:双版本回退不是在用户手机里执行两次 ohpm install。ohpm 属于构建与依赖管理流程,安装到应用中的产物不能在运行时随意变成另一套源码包。这里的“旧版”和“候选版”,指从两个已冻结的源代码及锁文件基线分别生成、分别留存的应用构建。业务层的 EffectDriver 接口可以相同,但真实驱动的依赖版本要由构建分支或可追溯的构建输入决定。

可以把发布基线描述为一组明确字段,而不是仅写在群聊里:

// 工程评审记录使用的应用内类型,不是 ohpm 自带接口
export interface DependencyBaseline {
  revision: string;       // 对应的源码版本标识
  approvedVersion: string;
  lockDigest: string;     // 锁文件内容摘要,由构建流水线计算
  sdkTarget: string;
  adapterRevision: string;
}

export interface ApprovalRecord {
  licenseReviewed: boolean;
  sdkCompatible: boolean;
  hasOwner: boolean;
  needsUnexpectedPermission: boolean;
  dependencyDiffReviewed: boolean;
}

export function precheck(record: ApprovalRecord): string[] {
  const issues: string[] = [];
  if (!record.licenseReviewed) issues.push('许可证尚未完成审查');
  if (!record.sdkCompatible) issues.push('SDK兼容性未确认');
  if (!record.hasOwner) issues.push('缺少维护责任人');
  if (record.needsUnexpectedPermission) issues.push('发现未预期权限需求');
  if (!record.dependencyDiffReviewed) issues.push('锁文件差异未审查');
  return issues;
}

这些字段只有在附有真实审查结果时才具备发布意义。lockDigest 不能使用随意填入的字符串代替真实哈希;approvedVersion 也不能从模糊范围自动推断。实际审查时,应核对许可、公开版本记录、SDK 支持信息、维护状态和权限变化。如果升级包没有可公开核实的 API 文档,不应因为“能找到安装命令”就把未知方法写进正式代码。

依赖声明、锁文件与构建基线的对应关系

二、保持接口不变,替换候选驱动的构造位置

系列前文的核心契约只有两个方法。它的好处在升级阶段非常直观:页面依旧调用 CardEffectController.show(resourceId) 和 close(),而不依赖旧库的类名、事件常量或播放器对象。升级时,变更集中在 adapters/ 中真正调用第三方 SDK 的那一份实现。业务上的资源标识仍是稳定字符串,不能因为新库喜欢数字索引,就让所有调用者一起换含义。

// entry/src/main/ets/effect/EffectDriver.ets:延续前三篇
export interface EffectDriver {
  start(resourceId: string): Promise<void>;
  stop(): void;
}

export interface EffectHooks {
  open: (resourceId: string) => Promise<void>;
  close: () => void;
}

export class CallbackEffectDriver implements EffectDriver {
  private readonly hooks: EffectHooks;
  constructor(hooks: EffectHooks) { this.hooks = hooks; }
  async start(resourceId: string): Promise<void> {
    await this.hooks.open(resourceId);
  }
  stop(): void {
    this.hooks.close();
  }
}

这里的 EffectHooks 是由应用定义的回调契约,不等于某个第三方包的导出 API。只有在查阅候选版本的真实接口和资源加载约束后,才能把 open 与 close 绑定到真实实现。要特别检查停止操作是否允许重复调用、失败后是否还持有资源、页面销毁后是否会继续异步回调,以及同一个容器是否支持多个播放器实例。任何一个答案发生变化,都可能让此前的 epoch 和 tail 逻辑看似正确却仍然留下资源竞争。

对回退而言,最稳妥的边界是保持页面代码和业务接口不变,分别使用旧版基线构建与候选基线构建做同一套场景检查。这不等于把两个版本的同名包同时装进一个模块。若业务确实要求应用内动态切换两个驱动,还要额外处理两个独立实现及其资源的打包冲突;没有这个明确需求,不应为了“看起来方便”增加复杂度。

三、回归门禁要检查状态序列,而非只看最终画面

打开一张内容卡片能够播放,只覆盖了最短路径。过去两篇已经讨论过旧请求迟到、启动失败、关闭后重启等问题,升级时要把这些情况列为固定回归场景。PreviewEffectDriver 可以先检验业务控制器自身是否满足契约;候选第三方版本必须另做集成验证,不能让替身测试代替真实设备上的依赖兼容性检查。

下面用一个与库无关的检查记录函数,防止人工回归只写“通过”两个字。它不是 DevEco Studio 的系统测试接口,而是供测试人员或自动化脚本复用的纯 ArkTS 判定逻辑。

export type CaseId = 'normal' | 'empty' | 'rapidSwitch' |
  'cancelPending' | 'retryAfterFailure' | 'dispose';

export interface CaseResult {
  id: CaseId;
  passed: boolean;
  evidence: string;
}

export interface ReleaseDecision {
  accepted: boolean;
  blockedCases: CaseId[];
}

export function evaluate(results: CaseResult[]): ReleaseDecision {
  const required: CaseId[] = [
    'normal', 'empty', 'rapidSwitch',
    'cancelPending', 'retryAfterFailure', 'dispose'
  ];
  const blockedCases: CaseId[] = required.filter((id: CaseId) => {
    const found: CaseResult | undefined =
      results.find((item: CaseResult) => item.id === id);
    return found === undefined || !found.passed || found.evidence.trim() === '';
  });
  return { accepted: blockedCases.length === 0, blockedCases };
}

这里有两个刻意的限制。第一,缺少结果与明确失败一样会阻断进入下一阶段,避免遗漏场景被默认成通过。第二,evidence 必须指向实际保存的测试结果位置、日志或评审记录,而不是自动填“已测试”。rapidSwitch 需要验证新选择不会被先前的异步结果覆盖;cancelPending 要验证关闭后旧回调不能重启播放器;retryAfterFailure 应证明一次失败不会阻断后续队列;dispose 要确认页面释放后不再产生有效业务结果。测试者还应附目标 SDK、设备形态、安装产物和依赖版本,才能比较候选与基线。

另一个细节是观测层不能改变行为。ObservedEffectDriver 接收到异常时向 FaultSink 记录 DriverFault,仍应将异常向调用者传播。若为了减少日志而悄悄吞掉错误,CardEffectController 的 failed 状态就可能失去依据;这不是“日志优化”,而是破坏了已定义的业务契约。

候选版本升级门禁与失败回退决策流程

四、当候选版本失败,回退操作到底做什么

一旦候选版本的某项关键场景失败,首先保留失败时的源码修订、锁文件与首条诊断,再暂停该候选版本推广。不要先删除所有 oh_modules 再回忆原先装了什么。然后从留存的旧版构建基线生成或取得经过验证的旧版应用产物,按实际发布平台与版本回退政策评估是否能重新投放。应用市场版本号、签名和已发布用户的本地数据迁移都可能限制“直接发回旧包”;源码层能回退,不代表用户设备上的应用版本可以无条件降级。

因此要区分三种动作:开发分支回退意味着恢复依赖声明和锁文件;构建回退意味着重新使用相同源码、构建配置与签名链产出可追溯包;线上回退意味着遵循发布渠道的版本规则和用户数据兼容要求执行恢复策略。这三者不能被一条 git revert 混为一谈。对本系列卡片动效而言,资源文件格式是否由新版迁移过,也要纳入审核范围;否则旧适配器可能拿到无法识别的资源。

建议保留一份可讨论的升级决策表:固定旧版版本和锁文件摘要,记录候选版本及传递依赖差异,对照六类运行场景分别附证据,并明确具体回退负责人和触发条件。发布后继续监测动效失败率和停止异常等指标时,指标必须来自真正的采集数据,不能把设计阶段的阈值写成已经得到的线上统计值。

五、四篇连载最终沉淀了什么

第一篇解决“依赖能不能进入工程”,第二篇解决“业务怎样与实现隔开”,第三篇解决“依赖、构建和运行故障在哪里定位”,第四篇则把上述工程成果接入升级决策和回退流程。贯穿四篇始终未变的是 EffectDriver 的 start/stop 约定,以及由 CardEffectController 管理资源选择和过期请求的职责边界。变更频繁的库实现留在适配器层,变更较少的业务规则留在控制器层,可审查的版本证据留在构建管理层。

这样的设计不要求团队一开始选中永远不会变化的第三方库,而是让以后每次替换都有清晰的范围、回归门槛和失败退路。工程成熟度不是“从未出现升级故障”,而是在故障出现时,可以明确知道当前版本是什么、哪里变了、哪些场景必须复查以及如何安全地恢复。对于需要长期维护的 HarmonyOS 应用,这比把库名直接塞进每个按钮回调更有价值。

官方资料:添加依赖项、ohpm list、ohpm clean。

Logo

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

更多推荐