封面

HarmonyOS 工程里经常出现一种“页面能打开,所以配置肯定没问题”的错觉。实际上,产品、模块、应用元数据、Ability、页面清单、启动入口和图标资源分散在不同文件中;其中任何一处引用错位,都可能在构建、安装、冷启动或桌面图标阶段才暴露。

The_kemusan 的真实现状也不能简单概括为“已经接好 Navigation”。它是 Stage 模型工程,EntryAbility 通过 loadContent('pages/Index') 加载唯一入口页,main_pages.json 也只登记 pages/Index;首页内的八个功能区依靠 currentPage 条件渲染切换。源码中没有 router_map.jsonmodule.json5 也没有 routerMap 字段,更没有 NavPathStack 或旧 @ohos.router 调用。

配置闭环先从六类文件开始

先按所有权看文件,比从某个字段一路猜更稳:

文件当前工程负责什么不负责什么
build-profile.json5产品、SDK、运行系统、模块清单、签名配置页面实际加载
entry/build-profile.json5Stage 模式、entry 构建选项、target应用名称和图标
AppScope/app.json5bundle、版本、应用级 label/iconAbility 启动路径
entry/src/main/module.json5entry 模块、Ability、pages、backup 扩展页面内部功能切换
main_pages.jsonRouter 页面清单Navigation 系统路由表
EntryAbility.ets创建主窗口并加载 pages/Index业务页之间的导航栈

流程图

本文把“配置存在”“构建通过”“页面运行”“设备表现”“上架完成”视为不同证据层级。这里只基于当前源码核对引用关系,不把静态文件存在写成后面几层已经完成。

根 build-profile 管产品和模块,不要把签名内容带进公开文章

根配置声明了一个 default 产品,目标 SDK 为 6.1.0(23),兼容 SDK 为 6.0.2(22),运行系统为 HarmonyOS;同时登记 entrylibraryHSPlibraryHAR 三个模块。可公开的结构可以收敛成:

{
  "app": {
    "products": [
      {
        "name": "default",
        "targetSdkVersion": "6.1.0(23)",
        "compatibleSdkVersion": "6.0.2(22)",
        "runtimeOS": "HarmonyOS",
        "buildOption": {
          "strictMode": {
            "caseSensitiveCheck": true,
            "useNormalizedOHMUrl": true
          }
        }
      }
    ]
  },
  "modules": [
    { "name": "entry", "srcPath": "./entry" },
    { "name": "libraryHSP", "srcPath": "./libraryHSP" },
    { "name": "libraryHAR", "srcPath": "./libraryHAR" }
  ]
}

真实文件还包含 signingConfigs。证书路径、profile 路径、别名和加密字段都属于私密构建材料,不应复制到博客、截图、提交日志或问题单里。文章解释产品和模块关系已经足够;即使是加密后的密码文本,也不应该因为“看起来不可读”就公开。

根配置能证明模块被纳入构建图,不能证明 HSP/HAR 的业务已经被主页面调用。当前灯光练习主链路仍集中在 entry 的 Index.ets

entry/build-profile 说明 Stage 构建面,而不是页面路由

模块构建配置的关键字段如下:

{
  "apiType": "stageMode",
  "buildOption": {
    "resOptions": {
      "copyCodeResource": { "enable": false }
    }
  },
  "buildOptionSet": [
    {
      "name": "release",
      "arkOptions": {
        "obfuscation": {
          "ruleOptions": {
            "enable": false,
            "files": ["./obfuscation-rules.txt"]
          }
        }
      }
    }
  ],
  "targets": [
    { "name": "default" },
    { "name": "ohosTest" }
  ]
}

这里确认 entry 使用 Stage 模型,包含默认目标与 ohosTest 目标,release 混淆当前关闭。它不会告诉我们首页加载哪个页面,也不会自动创建 Navigation 路由表。文章如果把 apiType: stageMode 写成“已完成页面导航”,就跨越了配置边界。

release 混淆关闭也是一个需要在交付前明确的现状,而不是构建失败。是否开启要结合规则文件、反射/序列化使用和发布策略评估,不能只为了“看起来更安全”直接改开关。

AppScope 负责应用身份和桌面入口资源

AppScope/app.json5 保存应用级信息:

{
  "app": {
    "bundleName": "com.example.the_kemusan",
    "vendor": "example",
    "versionCode": 1000000,
    "versionName": "1.0.0",
    "buildVersion": "1",
    "icon": "$media:layered_image",
    "label": "$string:app_name"
  }
}

iconlabel 引用 AppScope 自己的资源空间。源码目录下确实有 AppScope/resources/base/media/layered_image.jsonbackground.pngforeground.png,因此应用级引用链在静态文件上是完整的。

bundleName 仍带有示例式命名,vendor 也是 example。这属于发布前需要确认的应用元数据,不能因为能构建就默认满足实际发行要求。版本号同样要和目标发布渠道中的版本策略对齐。

module.json5 把 pages、Ability、启动窗和 backup 扩展串起来

entry 模块的主配置同时声明了页面 profile、主 Ability 和备份扩展:

{
  "module": {
    "name": "entry",
    "type": "entry",
    "mainElement": "EntryAbility",
    "deviceTypes": ["phone"],
    "pages": "$profile:main_pages",
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "icon": "$media:layered_image",
        "startWindowIcon": "$media:layered_image",
        "startWindowBackground": "$color:start_window_background",
        "exported": true
      }
    ],
    "extensionAbilities": [
      {
        "name": "EntryBackupAbility",
        "type": "backup",
        "metadata": [
          {
            "name": "ohos.extension.backup",
            "resource": "$profile:backup_config"
          }
        ]
      }
    ]
  }
}

这里的 pages 指向 main_pages,不是 router_mapstartWindowIcon 指向 entry 模块自己的分层图标,应用级 app.json5 则解析 AppScope 下的同名资源。两个资源描述文件结构一致,但它们属于不同资源所有者,实际前景文件大小也不同,不能笼统说成“应用图标和启动图标就是同一张文件”。

exported: true 配合 home action/entity 让 EntryAbility 成为桌面入口。这个配置仍需通过安装后的桌面启动来验证,静态审计只确认字段关系。

结构图

当前页面入口是 main_pages 加 loadContent,不是 Navigation

main_pages.json 只有一项:

{
  "src": [
    "pages/Index"
  ]
}

EntryAbility.onWindowStageCreate() 加载的路径与它完全一致:

onWindowStageCreate(windowStage: window.WindowStage): void {
  windowStage.loadContent('pages/Index', (err) => {
    if (err.code) {
      hilog.error(DOMAIN, 'testTag',
        'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
      return;
    }
    hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
  });
}

这形成了“module → profile → Ability → 页面文件”的入口闭环。项目根本没有 entry/src/main/resources/base/profile/router_map.jsonmodule.json5 也没有 "routerMap": "$profile:router_map",因此不能把 main_pages.json 当成 Navigation 的系统路由表。

如果只排查冷启动白屏,先比对三处字符串:module.pages 的 profile 名、main_pages.src 的页面路径、loadContent() 的页面路径。三者任何一个大小写或路径不一致,都会破坏入口链路。

八个功能区是 Index 内部状态切换

首页卡片的 id 写入 currentPagebuild() 再依据该字符串选择 Builder:

private openModule(id: string, title: string): void {
  this.stopTimer();
  this.stopAltFlash();
  this.currentPage = id;
  this.pageTitle = title;
  if (id === PAGE_SUBJECT_ONE) {
    this.startTheoryPractice(SUBJECT_ONE, '科一灯光题', '');
  } else if (id === PAGE_SUBJECT_THREE_EXAM || id === PAGE_SUBJECT_THREE_FLOW) {
    this.resetSubjectThreeExam();
  } else if (id === PAGE_QUESTION_BANK) {
    this.resetQuestionBank();
  }
}

这种方式适合单页练习应用:没有独立页面栈,返回首页就是把 currentPage 设回 home。代价是无法直接获得 NavDestination 生命周期、栈管理、系统路由表和跨包页面按需加载等能力。

同样也不能称它为旧 Router,因为源码没有 router.pushUrl()。准确说法是“单入口页内的条件渲染状态机”。若迁移到 Navigation,应建立 Navigation + NavPathStack + NavDestination 的导航关系;页面名称映射既可以用 router_map.json 系统路由表,也可以通过 .navDestination(builder) 提供自定义路由表。不能把是否存在 router_map.json 当成识别 Navigation 的唯一条件。两种方式的边界可参考 华为 Navigation 跨包路由文档

layered_image 要分别核对应用级与模块级所有权

两处 layered_image.json 都采用相同结构:

{
  "layered-image": {
    "background": "$media:background",
    "foreground": "$media:foreground"
  }
}

静态核对时至少检查四件事:

  1. AppScope 下的描述文件能找到 AppScope 下的 background.pngforeground.png
  2. entry 下的描述文件能找到 entry 下的对应前景和背景。
  3. app.json5.icon 解析应用级资源,module.json5.icon/startWindowIcon 解析模块级资源。
  4. 冷启动与桌面图标要分别观察,不能用一张资源文件存在来替代设备表现。

分层图标描述的是前景与背景的组合规则,不是业务页面中的普通 Image。把它复制到 ArkUI 页面里手工叠放,既没有必要,也会绕开系统对应用图标的处理。

backup_config 存在,不代表业务数据已经可恢复

backup_config.json 当前只有:

{
  "allowToBackupRestore": true
}

module.json5 已把它挂到 EntryBackupAbility 的 metadata,但扩展实现只是记录回调并结束:

export default class EntryBackupAbility extends BackupExtensionAbility {
  async onBackup() {
    hilog.info(DOMAIN, 'testTag', 'onBackup ok');
    await Promise.resolve();
  }

  async onRestore(bundleVersion: BundleVersion) {
    hilog.info(DOMAIN, 'testTag', 'onRestore ok %{public}s',
      JSON.stringify(bundleVersion));
    await Promise.resolve();
  }
}

这里没有手工导出 PracticeStore 中的 exam_history,也没有在回调中逐条写回 Preferences,但这不等于系统不会备份业务文件。按照 华为应用数据备份恢复文档,未配置 includes 时框架使用默认范围,其中包含沙箱 Preferences 路径。因此,空的扩展回调不能作为“历史不会被备份”的依据。

当前可以确认系统备份扩展和允许配置已声明,但尚未通过设备操作证明历史记录在目标场景下恢复成功。真正验收时应确认历史已落盘、存储路径位于备份范围内,并核对恢复后的记录数、字段兼容和失败处理。只有需要自定义数据格式或升级迁移时,才进一步设计手工序列化和转换逻辑;它们不是使用系统文件备份的统一前置要求。

若要迁移到 Navigation,新增的是一套架构,不是改一个文件名

下面选择系统路由表作为未来迁移方案,当前工程尚未实现;若采用自定义路由表,第 3、4 步可改为给 Navigation.navDestination 绑定页面构建函数:

1. 在页面根部创建一个 NavPathStack,并由 Navigation 持有。
2. 把独立功能区改成以 NavDestination 为根的 @Component。
3. 为每个目标页导出 @Builder 工厂。
4. 新建 base/profile/router_map.json,并在 module.json5 声明 routerMap。
5. 用 pushPath/pushPathByName 替换 currentPage 字符串分发。
6. 在模拟器或真机验证跳转、返回、参数和生命周期。

新项目可以优先采用 Navigation + NavPathStack + router_map.json。但对当前单页应用,是否迁移取决于独立页面数量、跨模块需求、返回栈和多端分栏需求。没有这些诉求时,先把现有状态机写稳,比为了 API 新旧标签强行重构更合适。

用证据阶梯完成配置验收

层级可以证明什么当前文章是否完成
静态审计文件存在、字段和资源引用相互对应已核对
JSON5/资源编译语法、资源名、模块图可被构建系统接受需实际构建
模拟器启动Ability 能加载 Index,功能区可切换需运行操作
真机安装桌面图标、冷启动、系统模式和返回行为需设备确认
release 包签名、版本、混淆与发布包生成需 release 构建
应用市场提交材料、审核和上架状态与源码存在不同层级

配置类文章最容易越界的地方,就是把第一层写成最后一层。准确记录验证边界不仅更可信,也能帮助排查:若静态链路已对齐而设备仍异常,调查重点就该转向构建产物、安装包或系统资源选择,而不是反复改页面代码。

结语:闭环不是字段齐全,而是每个引用都有落点

当前工程的真实入口链很清楚:根 build-profile 纳入 entry,AppScope 提供应用身份,module 指向 main_pagesEntryAbility,Ability 加载唯一的 pages/Index,页面内部再用 currentPage 切换功能区。图标则分为 AppScope 与 entry 两个资源所有者;backup 扩展已声明,系统可按默认范围处理沙箱文件,但历史恢复结果仍需设备验证。

把这些边界写清楚,就能避免三种常见误判:把 main_pagesrouter_map,把页面条件渲染当 Navigation,把 backup 配置存在当恢复成功。后续无论继续保持单页,还是迁移到 Navigation,都能从已核实的配置事实出发。

Logo

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

更多推荐