灯光模拟HarmonyOS应用实战-19-构建配置、页面路由与图标资源如何闭环

HarmonyOS 工程里经常出现一种“页面能打开,所以配置肯定没问题”的错觉。实际上,产品、模块、应用元数据、Ability、页面清单、启动入口和图标资源分散在不同文件中;其中任何一处引用错位,都可能在构建、安装、冷启动或桌面图标阶段才暴露。
The_kemusan 的真实现状也不能简单概括为“已经接好 Navigation”。它是 Stage 模型工程,EntryAbility 通过 loadContent('pages/Index') 加载唯一入口页,main_pages.json 也只登记 pages/Index;首页内的八个功能区依靠 currentPage 条件渲染切换。源码中没有 router_map.json,module.json5 也没有 routerMap 字段,更没有 NavPathStack 或旧 @ohos.router 调用。
配置闭环先从六类文件开始
先按所有权看文件,比从某个字段一路猜更稳:
| 文件 | 当前工程负责什么 | 不负责什么 |
|---|---|---|
根 build-profile.json5 | 产品、SDK、运行系统、模块清单、签名配置 | 页面实际加载 |
entry/build-profile.json5 | Stage 模式、entry 构建选项、target | 应用名称和图标 |
AppScope/app.json5 | bundle、版本、应用级 label/icon | Ability 启动路径 |
entry/src/main/module.json5 | entry 模块、Ability、pages、backup 扩展 | 页面内部功能切换 |
main_pages.json | Router 页面清单 | Navigation 系统路由表 |
EntryAbility.ets | 创建主窗口并加载 pages/Index | 业务页之间的导航栈 |

本文把“配置存在”“构建通过”“页面运行”“设备表现”“上架完成”视为不同证据层级。这里只基于当前源码核对引用关系,不把静态文件存在写成后面几层已经完成。
根 build-profile 管产品和模块,不要把签名内容带进公开文章
根配置声明了一个 default 产品,目标 SDK 为 6.1.0(23),兼容 SDK 为 6.0.2(22),运行系统为 HarmonyOS;同时登记 entry、libraryHSP、libraryHAR 三个模块。可公开的结构可以收敛成:
{
"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"
}
}
icon 和 label 引用 AppScope 自己的资源空间。源码目录下确实有 AppScope/resources/base/media/layered_image.json、background.png 和 foreground.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_map;startWindowIcon 指向 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.json,module.json5 也没有 "routerMap": "$profile:router_map",因此不能把 main_pages.json 当成 Navigation 的系统路由表。
如果只排查冷启动白屏,先比对三处字符串:module.pages 的 profile 名、main_pages.src 的页面路径、loadContent() 的页面路径。三者任何一个大小写或路径不一致,都会破坏入口链路。
八个功能区是 Index 内部状态切换
首页卡片的 id 写入 currentPage,build() 再依据该字符串选择 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"
}
}
静态核对时至少检查四件事:
- AppScope 下的描述文件能找到 AppScope 下的
background.png和foreground.png。 - entry 下的描述文件能找到 entry 下的对应前景和背景。
app.json5.icon解析应用级资源,module.json5.icon/startWindowIcon解析模块级资源。- 冷启动与桌面图标要分别观察,不能用一张资源文件存在来替代设备表现。
分层图标描述的是前景与背景的组合规则,不是业务页面中的普通 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_pages 与 EntryAbility,Ability 加载唯一的 pages/Index,页面内部再用 currentPage 切换功能区。图标则分为 AppScope 与 entry 两个资源所有者;backup 扩展已声明,系统可按默认范围处理沙箱文件,但历史恢复结果仍需设备验证。
把这些边界写清楚,就能避免三种常见误判:把 main_pages 当 router_map,把页面条件渲染当 Navigation,把 backup 配置存在当恢复成功。后续无论继续保持单页,还是迁移到 Navigation,都能从已核实的配置事实出发。
更多推荐

所有评论(0)