这篇起因不是业务 bug,而是一条很容易把人带偏的运行错误。

我把远程配置能力封装成第三方 HAR,静态 import 一直正常。后来为了减少首屏初始化,把它改成变量形式的动态加载:

const moduleName = '@demo/remoteconfig'

结果真机直接报 Cannot find module。更麻烦的是,换一份 HAR 后又遇到 OhmUrl 解析失败;再检查依赖,发现 entry 里的依赖名和 HAR 自己的 name 还可能不一致。

我最后做了 DynamicHarGateLab,把这些问题拆成构建前检查。最终会话是 har_load_20261001_14,模块 @demo/remoteconfig,runtimeOnly=true,Dependency Match true,OhmUrl RESOLVED,Load Cost 86 ms。

一、dependencies 里有它,不等于变量 import 一定能找到它

HAR 已经写进 oh-package.json5,只能说明工程声明了生产依赖。变量表达式 import(moduleName) 还需要让构建系统知道,哪些包可能在运行时按名称加载。

我先对齐两份配置:

// entry/oh-package.json5
{
  "dependencies": {
    "@demo/remoteconfig":
      "file:../libs/remoteconfig"
  }
}

// build-profile.json5 中的核心配置意图
{
  "buildOption": {
    "runtimeOnly": {
      "packages": [
        "@demo/remoteconfig"
      ]
    }
  }
}

不同工程模板下,runtimeOnly 所在配置层级应以当前 schema 为准;这里真正不能错的是 packages 里的包名,它必须与工程依赖名一致。

runtimeOnly 不应该塞进所有三方依赖,只保留真正通过变量表达式动态加载的包。

二、第二个坑是“依赖名”和 HAR 内部 name 不是一回事

entry 里写 @demo/remoteconfig,本地 HAR 自己的 oh-package.json5 也必须使用匹配的包名。否则安装或构建日志里可能出现 dependency name mismatch,后面再看 Cannot find module 就会误判成动态 import 本身有问题。

我做了一个很小的 Guard:

class HarDependencyGuard {
  static verify(
    dependencyName: string,
    harPackageName: string
  ): boolean {
    if (dependencyName !== harPackageName) {
      hilog.error(
        0x0000,
        'HarGuard',
        `name mismatch: ${dependencyName} != ${harPackageName}`
      )
      return false
    }
    return true
  }
}

这一步只负责把模糊的构建问题提前变成确定判断。Demo 最终 Dependency Match=true、Package Name=@demo/remoteconfig;如果这里失败,就不再继续 import。

三、动态加载本身反而是最简单的一段

真正执行时,我把阶段日志写清楚:

private async loadModule(): Promise<void> {
  const moduleName =
    '@demo/remoteconfig'

  this.state = 'IMPORTING'
  const start = Date.now()

  try {
    const ns: ESObject =
      await import(moduleName)

    if (!ns) {
      throw new Error('empty module namespace')
    }

    this.loadCost =
      Date.now() - start
    this.state = 'RESOLVED'
  } catch (err) {
    this.buildErrors++
    this.state = 'FAILED'
    throw err
  }
}

我关心的是错误发生在哪一层:依赖未声明、包名不一致、runtimeOnly 漏配、OhmUrl 解析失败,还是模块本身异常。HiLog 会先打印 dependency match,再打印 runtimeOnly match,最后才进入 import。

四、遇到 OhmUrl 错误时,我不再第一时间删缓存

构建日志里如果出现 Failed to resolve OhmUrl 或类似解析错误,最常见的做法是删除 oh_modules、重新 Sync。偶尔能解决,但也可能只是暂时掩盖真正的配置问题。

我的排查顺序固定成:

  1. 看代码里的 import 名称;
  2. 看 entry 的 dependencies;
  3. 看 HAR 内部 oh-package.json5 的 name;
  4. 看 runtimeOnly packages;
  5. 最后才检查本地 HAR 路径、安装缓存和工具链。

如果是本地压缩包依赖,还要确认 CI 环境里对应文件真实存在。开发机上的相对路径能用,不代表流水线工作目录也一样。

五、我又加了一个构建前预检

不想等真机启动后才看到 Cannot find module,最有效的方法不是写更多运行时 catch,而是在构建前直接阻断错误配置。

function validateDynamicHar(
  dependencyName: string,
  packageName: string,
  runtimeOnly: string[]
): string[] {
  const errors: string[] = []

  if (dependencyName !== packageName) {
    errors.push('package-name-mismatch')
  }

  if (!runtimeOnly.includes(dependencyName)) {
    errors.push('runtimeOnly-missing')
  }

  return errors
}

正式工程可以让脚本读取 JSON5,再挂到构建任务前。只要 errors.length > 0 就直接失败,而不是把问题留给真机。

本次 Demo 最终 Fallback Count=0、Build Errors=0,说明不需要任何兜底加载。

六、调试页只展示构建链路真正需要的字段

项目结构里保留 DynamicHarPage.ets、entry 的 build-profile.json5 和 oh-package.json5,以及 libs/remoteconfig/oh-package.json5、index.ets,再配一个 HarDependencyGuard。

HiLog 固定出现:dependency name matched: @demo/remoteconfig、runtimeOnly package found、import module=@demo/remoteconfig、OhmUrl resolved、loadCost=86ms、State: VALIDATING -> RESOLVED。

七、手机结果页用来确认“配置正确”而不是“碰巧能跑”

最终运行页数据是:

  • Session:har_load_20261001_14
  • Module:@demo/remoteconfig
  • State:RESOLVED
  • runtimeOnly:true
  • Dependency Match:true
  • Package Name:@demo/remoteconfig
  • OhmUrl:RESOLVED
  • Load Cost:86 ms
  • Fallback Count:0
  • Build Errors:0
  • Last Build:14:41:26

这张结果页真正想证明的是:模块能被动态加载,是因为依赖名、HAR 包名、runtimeOnly 和最终解析结果都对齐,而不是某次 Sync 以后“突然好了”。

八、三方 HAR 还有几个容易被忽略的边界

HAR 很适合共享代码和资源,但动态加载策略应该跟模块边界一起设计。并不是所有库都适合为了“按需”而改成变量 import。

本地 file 依赖在 CI 最容易暴露路径问题,依赖升级后也要重新检查 HAR 的 name。另外 debug 和 release 的构建行为可能不同,正式上架前我会把预检放进 release 构建链路。

九、这次我把“Cannot find module”拆成五个检查点

最后的排查链路是:

DEPENDENCY → PACKAGE NAME → RUNTIMEONLY → OHMURL → IMPORT

以前看到 Cannot find module,我会先怀疑 import 语法。现在我更愿意从工程配置往前核对。对共享 HAR 或三方 SDK 来说,最理想的是错误的包名、白名单和路径根本进不了安装包。

Logo

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

更多推荐