同样是点击内容卡片后没有动效,为什么有的故障要查 oh-package-lock.json5,有的要查 ArkTS 类型报错,还有的必须回到 CardEffectController 的异步状态?如果把三个阶段混在一起,最常见的结果就是:明明是依赖没有解析成功,却在页面里改了一圈 try/catch;明明是驱动初始化失败,却反复删除安装目录,最后让锁文件也跟着改变了。

上一阶段,内容卡片工程已经固定了 EffectDriver 的 start(resourceId): Promise<void>、stop(): void 两个方法,CallbackEffectDriver 负责桥接实现回调,PreviewEffectDriver 为控制器提供可控替身。控制器通过 epoch 和 tail 管理过期请求与串行顺序。这些契约继续保留。本篇不是重新写一个播放器,而是把定位顺序落实为可以复用的工程流程:先确认依赖,再确认编译,最后观察运行时状态。

HarmonyOS 三方库分层排障技术封面

一、同样的“没有效果”,三层含义完全不同

依赖层解决“包从哪里来、解析到什么版本、传递依赖是否稳定”。构建层解决“这个包及其类型、资源、模块关系能不能被当前编译环境接受”。运行层解决“当 start 真正执行时,资源是否有效、异步请求是否过期、底层错误是否传播”。三个阶段是有先后关系的:依赖装不上,就没有必要谈播放状态;构建失败,也无法根据界面空白直接断定资源错误。

先记录现象发生在什么动作之后,而不是随手给它贴上“库异常”的标签:是执行安装命令后报错,还是执行 Build 时出现某个文件的错误,抑或应用启动后某张卡片才不显示?在团队协作时,描述故障应包含动作、阶段、涉事模块和首次可见的错误信息。尤其是依赖问题,报错中的模块路径和锁文件变化常常比一句“重装试过了”更有价值。

依赖解析、构建校验与运行时调用的三段式排查路径

这里沿用系列中内容卡片工程的目录职责:entry/oh-package.json5 声明当前模块所需生产依赖;entry/src/main/ets/effect/EffectDriver.ets 保留业务接口;CardEffectController.ets 负责选择与取消;adapters/ 放经过审查的具体库桥接实现。包本身叫什么、许可如何、某个方法是否真实存在,仍以批准版本的官方或项目维护者文档为准。依赖准入表中的 approvedVersion、licenseReviewed、sdkCompatible、hasOwner、needsUnexpectedPermission 不因为排障而取消。

二、依赖错误先检查锁文件,别先清空现场

问:两台开发机的源代码相同,为什么安装结果不同?首先比较 oh-package.json5 的声明,再比较随工程管理的 oh-package-lock.json5。源代码版本一致,不意味着传递依赖的实际解析必然一致;如果一台机器更新了锁文件,另一台没有,重现条件就发生了变化。还要对齐 ohpm 工具版本、目标 SDK、项目级和模块级依赖位置,以及仓库配置是否指向同一来源。

可以从项目目录运行以下官方提供的命令,按顺序获得版本与依赖树信息。-r 用于查看工程各模块依赖;-d 2 只展开一定深度。命令并不会证明包是安全的,输出也不能代替许可审查。

ohpm --version
ohpm list
ohpm list -r
ohpm list -d 2

如果安装失败,要保存最早出现的错误、对应模块路径和依赖声明,再决定是仓库不可达、指定版本不可用、包文件损坏还是本地路径写错。不能因为最终提示“安装失败”就直接修改业务接口。华为的构建错误码说明中,对“本地依赖无效”这类问题明确指向模块级依赖路径;这类错误应该先修 oh-package.json5 中的路径配置,而不是修改 EffectDriver.start。

有人习惯先运行 ohpm clean 再安装,但该命令默认会清除安装产物及锁文件。若目的是保留版本解析依据、只清理安装产物,可以在确认工具版本和已备份现场的情况下使用 ohpm clean --keep-lockfile。这里的顺序很重要:先记录与对比,后清理与重装。保留锁文件也不保证所有环境问题消失;若镜像不可达或真实包本身有缺陷,清理缓存只会延长定位时间。

# 保存项目的依赖声明与锁文件变更后再执行
ohpm clean --keep-lockfile
ohpm install
ohpm list -r

三、构建能否过,取决于实际入口和类型边界

问:依赖树看起来正确,仍然无法构建,下一步查什么?先找到首条编译诊断所在的具体文件。如果报错落在适配器导入处,核查导入路径是否为包当前版本公开导出的模块;如果出现在某个方法参数或返回类型上,核查该方法是否属于批准版本,是否存在 ArkTS 类型不兼容;如果报错来自资源处理,就核查资源是否确实随应用或 HAR 被打包。依赖存在与API 可以被当前工程调用是两道不同的检查。

这套工程刻意不让 CardEffectController 导入三方类型,下面是适配接口的完整边界。它不是某个具体商业动效库的公开接口;外部包的真实方法仅由经过验证的 EffectHooks 适配提供,避免通过“猜一个方法名”解决构建错误。

// 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();
  }
}

当某个新版本把原来同步的关闭函数改成异步行为时,仅凭 TypeScript 类型通过不能保证原有资源释放语义没变。当前接口规定 stop(): void,驱动实现必须保证业务看到的关闭动作满足既定约定;如果真实库要求等待异步清理,应在技术评审中决定是适配层内部序列化,还是明确升级整个契约,而不能偷偷丢弃 Promise。编译诊断解决的是类型层面的硬性问题,生命周期和竞态还要靠下一层检查。

实际构建报错也要区分模块描述和库实现的问题:例如本地 HAR/HSP 的路径、模块声明、useNormalizedOHMUrl 配置不一致,都应先对照华为对应错误码资料。修复必须尽量针对首个确定原因,不要一次性同时升级 SDK、升级 ohpm、删除锁文件并重构适配器,否则无法判断是哪一步恢复了构建。

四、运行失败时,日志只记录可操作的线索

一旦确认安装与构建阶段没有阻断,才进入运行状态排查。这里需要回答的是:resourceId 是否非空?触发这次启动时控制器是否已经释放?epoch 是否仍为最新?底层驱动抛出的异常有没有沿 show() 的 Promise 返回页面?旧请求完成后是否被拒绝写回当前资源?这五个问题与包下载失败毫无关系。

为了避免页面读到具体第三方异常格式,可以在既有 EffectDriver 外增加项目内部的观测包装器。它把资源标识、调用阶段和失败事实交给注入的记录函数,同时仍将原异常抛给控制器,不擅自吞掉失败。输入 resourceId 应使用业务允许的非敏感标识;若资源名包含账号或个人路径,就改用短哈希或脱敏关联键。

// entry/src/main/ets/effect/ObservedEffectDriver.ets
import { EffectDriver } from './EffectDriver';

export type DriverPhase = 'start' | 'stop';
export interface DriverFault {
  phase: DriverPhase;
  resourceId: string;
  message: string;
}
export type FaultSink = (fault: DriverFault) => void;

export class ObservedEffectDriver implements EffectDriver {
  private activeId: string = '';

  private readonly inner: EffectDriver;
  private readonly sink: FaultSink;

  constructor(inner: EffectDriver, sink: FaultSink) {
    this.inner = inner;
    this.sink = sink;
  }

  async start(resourceId: string): Promise<void> {
    try {
      await this.inner.start(resourceId);
      this.activeId = resourceId;
    } catch (error) {
      this.sink({ phase: 'start', resourceId,
        message: '驱动启动发生异常' });
      throw error;
    }
  }

  stop(): void {
    try {
      this.inner.stop();
      this.activeId = '';
    } catch (error) {
      this.sink({ phase: 'stop', resourceId: this.activeId,
        message: '驱动关闭发生异常' });
      throw error;
    }
  }
}

这里没有给驱动异常“自动恢复成功”的假结论。包装器只提供记录与传播:如果 stop() 会抛异常,原先要求幂等且安全关闭的契约就被破坏了,应修正具体适配器的关闭语义,而不是让 ObservedEffectDriver 吞掉错误假装状态已恢复。CardEffectController 中的 tail 负责保证后续排队任务不会因上次 show 拒绝而永远挂起,epoch 则负责阻止过期资源覆盖新选择。观测层既不修改它们,也不替代它们。

失败分层与控制器异常传播的核查关系

五、把“复现成功”写成一套可判断的操作

建议排查时采用一条固定路径:首先检查依赖声明、工具版本和锁文件有没有意外变化;然后在保留错误现场的前提下读取首条构建诊断,判断来源属于路径、类型还是资源打包;最后使用 PreviewEffectDriver 注入一次启动失败,验证 CardEffectController 是否报告 failed、旧资源是否停止、下一次调用是否仍能排队。若要检查真正三方库的兼容性,须换成真实实现并使用批准版本、目标 SDK 和设备进行独立验证。

对运行阶段,至少覆盖“首次启动成功、空资源、快速切换、关闭后迟到结果、启动失败后重试、页面释放后再调用”六种条件。它们不是为了证明某个开源库普遍可靠,而是为了检查当前业务契约。由测试环境实际取得的错误信息和设备配置可以进入故障单;没有运行依据时,文章所列的是可复现检查步骤与预期判定,而不是现成测试结论。

第三阶段的收获是让排查顺序变得可信:依赖层有版本证据,构建层有首条类型或资源诊断,运行层有驱动与控制器的明确责任界限。下一篇将围绕依赖版本升级、双版本回退演练和发布门禁收尾,确保升级不是靠“这次能打开页面”来决定。

官方资料:添加依赖项、ohpm list、ohpm clean、依赖类构建错误码。

Logo

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

更多推荐