HarmonyOS APP《画伴梦工厂》开发第44篇-页面路由注册与动态加载——main_pages配置
第6.2篇:页面路由注册与动态加载——main_pages 配置
难度:⭐⭐ 进阶
前置知识:第 1.6 篇 页面路由与生命周期
涉及源文件:products/default/src/main/resources/base/profile/main_pages.json、products/default/src/main/ets/defaultability/DefaultAbility.ets
一、概述
在 HarmonyOS 应用中,页面导航是最基础也是最核心的能力之一。不同于单页应用(SPA)在同一个页面内通过组件切换实现"伪导航",HarmonyOS 的页面路由系统是一种真正意义上的原生页面栈管理机制——每次跳转都对应着新的页面实例的创建与旧页面的入栈/出栈。
"画伴梦工厂"包含超过 10 个独立页面:首页、拍照识别页、自由涂鸦页、AI 等待页、结果展示页、3D 模型页、鸿蒙特性页,以及自适应、响应式布局演示页等。这些页面如何被系统识别?如何从代码中跳转?参数如何跨页面传递?路由栈如何管理?本文将围绕这些问题,从 main_pages.json 声明出发,逐层深入页面路由的完整链路。
二、main_pages.json:路由声明表
HarmonyOS 的页面路由系统采用声明式注册机制。所有可被路由导航的页面,都必须先在 main_pages.json 中注册。
2.1 配置文件位置
products/default/src/main/resources/base/profile/main_pages.json
2.2 完整声明
{
"src": [
"pages/Index",
"pages/PhotoRecognitionPage",
"pages/FreeDoodlePage",
"pages/RecognitionWaitingPage",
"pages/RecognitionResultPage",
"pages/HarmonyFeaturesPage",
"pages/ModelViewerPage",
"creationcard/pages/CreationCard",
"pages/AdaptiveIndex",
"pages/ResponsiveIndex",
"pages/SystemCapabilitiesIndex"
]
}
2.3 注册规则
| 规则 | 说明 |
|---|---|
| 路径约定 | 相对路径,相对于 src/main/ets/ 目录,无需 .ets 后缀 |
| 大小写敏感 | 路径必须与文件名完全一致(包括大小写) |
| 子目录支持 | 支持多层目录,如 creationcard/pages/CreationCard |
| 首页必须注册 | loadContent 使用的页面路径必须在数组中 |
| 顺序无关 | 数组顺序不影响路由行为,仅用于编译期扫描 |
关键理解:
main_pages.json本质上是编译期的页面清单。在构建时,编译器会扫描该文件列出的所有路径,为每个页面生成对应的路由入口代码。未注册的页面在运行时无法通过Router.pushUrl跳转。
2.4 module.json5 连接配置
main_pages.json 通过 module.json5 中的 pages 字段被模块引用:
{
"module": {
"name": "default",
"type": "entry",
"mainElement": "DefaultAbility",
"pages": "$profile:main_pages",
// ...
}
}
$profile:main_pages 是资源引用语法,编译器在构建时将 resources/base/profile/main_pages.json 解析为最终的路由表。这种设计使得路由配置与代码解耦——修改页面列表只需编辑 JSON,无需改动 TypeScript 代码。
三、入口加载:loadContent 与 DefaultAbility
应用启动时,系统首先创建 DefaultAbility,然后在其 onWindowStageCreate 生命周期中加载首页。
3.1 加载首页
export default class DefaultAbility extends UIAbility {
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.');
});
}
}
3.2 loadContent 解析
| 参数 | 值 | 说明 |
|---|---|---|
| 页面路径 | 'pages/Index' |
对应 main_pages.json 中注册的第一个页面 |
| 回调 | (err) => void |
加载完成后的回调,err.code 为 0 表示成功 |
loadContent 的工作原理:
- 根据传入路径在已注册的页面表中查找
pages/Index - 创建该页面的
@Component实例 - 将实例挂载到
WindowStage创建的窗口上 - 触发页面的
aboutToAppear生命周期
注意:
loadContent每个Ability生命周期中只能调用一次,用于加载根页面。后续的所有页面跳转都要通过Router.pushUrl等路由 API 完成。
四、Router.pushUrl:动态页面导航
当用户从首页点击"拍照"或"涂鸦"按钮时,应用需要通过 Router.pushUrl 跳转到目标页面。
4.1 基本跳转
最简单的跳转方式——仅指定 url:
// Index.ets - openCreationFlow
private openCreationFlow(index: number) {
this.selectCreationMode(index);
if (index === 0) {
this.getUIContext().getRouter().pushUrl({ url: 'pages/PhotoRecognitionPage' });
} else if (index === 1) {
this.getUIContext().getRouter().pushUrl({ url: 'pages/FreeDoodlePage' });
} else if (index === 3) {
this.getUIContext().getRouter().pushUrl({ url: 'pages/ModelViewerPage' });
} else {
this.openCreationTab();
}
}
4.2 带参数的跳转
大多数场景下需要向目标页面传递数据。通过 params 字段传递任意对象:
// CreationComponents.ets - navigateToGeneration
private navigateToGeneration() {
this.activeStep = 2;
this.generationProgress = 100;
this.noticeText = '正在送去生成动画';
this.getUIContext().getRouter().pushUrl({
url: 'pages/RecognitionWaitingPage',
params: {
source: '图片生成',
workSource: 'photo',
prompt: this.buildVideoPrompt(),
imageUri: this.capturedImageUri,
coverUri: this.capturedImageUri
}
});
}
再来看一个更丰富的例子——从等待页面跳转到结果页面时,携带了完整的上下文信息:
// RecognitionWaitingPage.ets - 跳转到结果页
this.getUIContext().getRouter().pushUrl({
url: 'pages/RecognitionResultPage',
params: {
source: this.source,
workSource: this.workSource,
prompt: this.prompt,
imageUri: this.imageUri,
coverUri: this.coverUri,
videoUri: this.videoUri,
workId: this.workId,
recognitionResult: this.recognitionResult
}
});
4.3 获取 UIContext
项目中统一使用 this.getUIContext().getRouter() 获取路由实例,而非旧版的 router.pushUrl() 全局函数。这种方式的优势在于:
- 绑定到当前组件:确保路由操作与当前 UI 上下文一致
- 支持多窗口场景:在分屏或自由多窗模式下,每个窗口有独立的 UIContext
- 类型安全:返回的
Router实例与当前组件的生命周期关联
五、参数接收:getParams() + 类型断言
目标页面通过 getParams() 接收参数,并使用类型断言(as 关键字)将参数转换为预期的类型。
5.1 定义参数接口
在源文件中,先定义参数类型接口:
// RecognitionWaitingPage.ets (内部定义)
interface WaitingParams {
source: string;
workSource: string;
prompt: string;
imageUri: string;
coverUri: string;
}
5.2 在 aboutToAppear 中接收
// RecognitionWaitingPage.ets
@State private source: string = '';
@State private prompt: string = '';
@State private workSource: string = 'photo';
private imageUri: string = '';
private coverUri: string = '';
aboutToAppear() {
const params = this.getUIContext().getRouter().getParams() as WaitingParams;
if (params && params.source) {
this.source = params.source;
}
if (params && params.workSource) {
if (params.workSource === 'doodle') {
this.workSource = 'doodle';
} else if (params.workSource === 'ai-chat') {
this.workSource = 'ai-chat';
} else {
this.workSource = 'photo';
}
}
if (params && params.prompt) {
this.prompt = params.prompt;
}
// ...
}
5.3 参数安全防御
项目中采用了防御性取值的模式:
const params = this.getUIContext().getRouter().getParams() as WaitingParams;
if (params && params.source) {
// 安全使用
}
为什么需要 if (params && params.source) 检查?
| 场景 | 风险 | 防御方式 |
|---|---|---|
| 直接跳转(无 params) | params 为 undefined |
if (params && ...) |
| 参数缺失某个字段 | params.source 为 undefined |
if (... && params.source) |
| 跳转来源不同 | 参数结构不匹配 | 逐个字段检查而非整体断言 |
这种模式在 RecognitionResultPage 中同样使用:
// RecognitionResultPage.ets
aboutToAppear() {
const params = this.getUIContext().getRouter().getParams() as ResultParams;
if (params && params.source) {
this.source = params.source;
}
if (params && params.prompt) {
this.prompt = params.prompt;
}
if (params && params.imageUri) {
this.imageUri = params.imageUri;
}
if (params && params.coverUri) {
this.coverUri = params.coverUri;
}
// ...
}
六、syncLaunchTab:AppStorage 跨页面状态
除了 Router 参数传递,"画伴梦工厂"还通过 AppStorage 实现了一种外部启动参数传递机制——当用户从桌面快捷方式、服务卡片或外部链接启动应用时,Want 对象中的参数需要传递给目标页面。
6.1 syncLaunchTab 实现
export default class DefaultAbility extends UIAbility {
private syncLaunchTab(want: Want): void {
if (!want.parameters) {
return;
}
const tabValue = want.parameters['tab'];
if (typeof tabValue === 'string') {
AppStorage.setOrCreate<string>('launchTab', tabValue);
}
const workIndexValue = want.parameters['workIndex'];
if (typeof workIndexValue === 'number') {
AppStorage.setOrCreate<number>('launchWorkIndex', workIndexValue);
} else if (typeof workIndexValue === 'string') {
const parsedIndex: number = Number.parseInt(workIndexValue, 10);
if (parsedIndex >= 0) {
AppStorage.setOrCreate<number>('launchWorkIndex', parsedIndex);
}
}
}
}
6.2 调用时机
syncLaunchTab 在两个生命周期中调用:
// 首次启动
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
this.syncLaunchTab(want);
// ...
}
// 从后台恢复或接收到新的 Want
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
this.syncLaunchTab(want);
}
6.3 在首页消费
Index.ets 在 aboutToAppear 中读取 AppStorage 的全局状态:
aboutToAppear() {
// 先尝试读取 Router 参数
const params = this.getUIContext().getRouter().getParams() as IndexParams;
if (params && params.tab === 'works') {
// 处理路由参数跳转
}
// 再尝试读取 AppStorage 的启动参数
const launchTab = AppStorage.get<string>('launchTab');
if (launchTab === 'works') {
const launchWorkIndex = AppStorage.get<number>('launchWorkIndex');
if (typeof launchWorkIndex === 'number') {
this.openWorkDetail(launchWorkIndex);
} else {
this.openWorkDetail(this.selectedWorkIndex);
}
// 消费后清除,避免重复触发
AppStorage.delete('launchTab');
AppStorage.delete('launchWorkIndex');
}
}
6.4 两种参数传递机制对比
| 机制 | 传递场景 | 生命周期 | 数据存储 |
|---|---|---|---|
| Router params | 页面间跳转 | 仅当前跳转有效 | 内存中临时存储 |
| AppStorage + syncLaunchTab | 外部启动(卡片、快捷方式、Widget) | 跨 Ability 生命周期 | AppStorage 全局存储,消费后清除 |
这种双层设计体现了对不同入口场景的系统性思考:用户直接在 App 内导航使用 Router,外部唤醒使用 AppStorage 桥接。
七、卡片页面路由:creationcard/pages/CreationCard
main_pages.json 中有一个特殊的路由条目:
"creationcard/pages/CreationCard"
这是服务卡片的配置页面。HarmonyOS 的服务卡片(Form)本身并不需要通过路由跳转显示,但卡片对应的配置页或预览页需要以页面的形式存在,以便在用户点击卡片配置按钮时跳转。
creationcard/pages/CreationCard 的路径层级与普通页面不同——它位于 creationcard 子目录下,体现了项目对卡片功能模块的独立管理。在 ArkUI 中,无论页面位于哪个子目录,只要在 main_pages.json 中注册,都可以通过 Router.pushUrl({ url: 'creationcard/pages/CreationCard' }) 正常跳转。
八、路由栈管理
8.1 pushUrl 与 replaceUrl
pushUrl 将新页面压入路由栈,保留当前页面在栈中(用户可按返回键回到上一页)。而 replaceUrl 则用新页面替换当前页面,不保留当前页在栈中。
在"画伴梦工厂"的结果页中,用户查看完结果后点击"查看我的作品",使用 replaceUrl 回到首页:
// RecognitionResultPage.ets
this.getUIContext().getRouter().replaceUrl({
url: 'pages/Index',
params: {
tab: 'works'
}
});
使用 replaceUrl 而不是 pushUrl 的原因:
| 考量 | 说明 |
|---|---|
| 栈深度控制 | 如果一路 pushUrl,从首页 → 拍照 → 等待 → 结果 → 首页,栈深度会达到 5 层 |
| 用户体验 | 用户从结果页回到首页后,按返回键应退出 App,而不是回到结果页 |
| 内存管理 | 减少页面实例数量,降低内存占用 |
8.2 返回导航(back)
各页面通过 Router.back() 实现返回上一页的功能:
// PhotoRecognitionPage.ets
this.getUIContext().getRouter().back();
// FreeDoodlePage.ets
this.getUIContext().getRouter().back();
// RecognitionWaitingPage.ets
this.getUIContext().getRouter().back();
// HarmonyFeaturesPage.ets
this.getUIContext().getRouter().back();
back() 方法的行为:
- 弹出当前页面,销毁其实例
- 显示路由栈中的上一个页面
- 触发当前页面的
aboutToDisappear生命周期 - 触发上一个页面的
onPageShow生命周期(如果实现了)
8.3 路由栈结构示例
用户操作路径:
首页 → 拍照识别页 → 等待页 → 结果页 → 查看作品(首页)
路由栈变化:
1. [首页] ← pushUrl 首页
2. [首页, 拍照识别页] ← pushUrl PhotoRecognitionPage
3. [首页, 拍照识别页, 等待页] ← pushUrl RecognitionWaitingPage
4. [首页, 拍照识别页, 等待页, 结果页] ← pushUrl RecognitionResultPage
5. [首页] ← replaceUrl Index (替换结果页)
在步骤 5 中,如果使用的是 pushUrl,路由栈会变为 [首页, 拍照识别页, 等待页, 结果页, 首页]——用户需要按 5 次返回键才能退出应用。使用 replaceUrl 后,结果页被首页替换,栈深度骤减为 1。
九、路由管理最佳实践
9.1 页面路径常量化
建议将页面路径定义为常量,避免字符串拼写错误:
// 推荐做法
const ROUTE_PAGES = {
INDEX: 'pages/Index',
PHOTO_RECOGNITION: 'pages/PhotoRecognitionPage',
FREE_DOODLE: 'pages/FreeDoodlePage',
WAITING: 'pages/RecognitionWaitingPage',
RESULT: 'pages/RecognitionResultPage',
MODEL_VIEWER: 'pages/ModelViewerPage',
} as const;
// 使用时
this.getUIContext().getRouter().pushUrl({ url: ROUTE_PAGES.WAITING, params: {...} });
虽然项目中未采用此模式(直接使用字符串字面量),但对于大型项目,常量化可以有效避免"页面路径变更后需要全局搜索替换"的困境。
9.2 参数接口复用
每个涉及参数传递的页面建议定义唯一的参数接口,并在入口处集中解析:
// RecognitionWaitingPage.ets
interface WaitingParams {
source: string;
workSource: string;
prompt: string;
imageUri: string;
coverUri: string;
}
// 统一在 aboutToAppear 中解析
aboutToAppear() {
const params = this.getUIContext().getRouter().getParams() as WaitingParams;
this.source = params?.source ?? '';
this.workSource = (params?.workSource as 'photo' | 'doodle' | 'ai-chat') ?? 'photo';
this.prompt = params?.prompt ?? '';
this.imageUri = params?.imageUri ?? '';
this.coverUri = params?.coverUri ?? '';
}
使用空值合并运算符(??)可以提供更简洁安全的默认值处理。
9.3 路由栈深度控制
避免路由栈过深(建议不超过 5 层)。项目中的实践经验:
- 线性流程(拍照 → 等待 → 结果):使用
pushUrl依次入栈 - 返回起点(结果页 → 首页):使用
replaceUrl替换而非入栈 - 模态页面(如鸿蒙特性展示页):使用
pushUrl入栈,但提供明确的"返回"按钮 - 非必要不入栈:如果当前页面可以被替换且无需保留历史,优先用
replaceUrl
9.4 Ability 间路由 vs 页面内路由
| 路由类型 | API | 适用场景 |
|---|---|---|
| 页面内路由 | Router.pushUrl / Router.replaceUrl |
同一 Ability 内部的页面跳转 |
| Ability 间跳转 | startAbility / startAbilityForResult |
跨模块或跨应用跳转 |
"画伴梦工厂"采用的是单 Ability + 多 Page 架构,所有页面跳转都使用 Router API,没有使用跨 Ability 跳转。这种架构的优势在于:
- 单一 UIAbility 实例,资源开销小
- 页面间切换无需跨进程通信
- 状态共享简单(通过 AppStorage 或单例服务)
9.5 生命周期管理
路由操作会触发页面生命周期,需要特别注意:
| 路由操作 | 当前页面 | 目标页面 |
|---|---|---|
pushUrl |
onPageHide |
aboutToAppear + onPageShow |
replaceUrl |
aboutToDisappear |
aboutToAppear + onPageShow |
back() |
aboutToDisappear |
onPageShow |
在 aboutToAppear 中初始化的资源(定时器、监听器),一定要在 aboutToDisappear 中清理,避免内存泄漏。
总结
本文围绕"画伴梦工厂"的页面路由系统,从路由注册到动态加载,从参数传递到栈管理,进行了全面解析:
| 知识点 | 关键内容 |
|---|---|
| main_pages.json | 编译期路由清单,所有可跳转页面必须在此注册 |
| module.json5 引用 | "pages": "$profile:main_pages" 建立配置与模块的连接 |
| loadContent | DefaultAbility 中加载根页面,每个 Ability 仅调用一次 |
| Router.pushUrl | getUIContext().getRouter().pushUrl() 实现页面入栈跳转 |
| 参数传递 | params 对象传递任意数据,目标页面通过 getParams() + 类型断言接收 |
| syncLaunchTab | AppStorage 桥接外部启动参数,支持快捷方式/卡片唤醒 |
| 卡片路由 | creationcard/pages/CreationCard 服务卡片配置页注册 |
| 路由栈管理 | pushUrl 入栈 vs replaceUrl 替换;back() 出栈返回 |
| 最佳实践 | 参数接口复用、空值合并、栈深度控制、生命周期对称管理 |
路由系统是应用的骨架——良好的路由设计可以让页面导航清晰、参数传递安全、栈管理高效。下一篇我们将探讨日志系统设计与可观测性,看如何通过 hilog 封装实现全链路的调试与监控能力。
参考源码
products/default/src/main/resources/base/profile/main_pages.json— 页面路由注册表products/default/src/main/module.json5— 模块配置,引用$profile:main_pagesproducts/default/src/main/ets/defaultability/DefaultAbility.ets— Ability 入口,含loadContent和syncLaunchTabproducts/default/src/main/ets/pages/Index.ets— 首页,路由参数消费 + AppStorage 读取products/default/src/main/ets/pages/RecognitionWaitingPage.ets— 等待页,参数接收 + 跳转结果页products/default/src/main/ets/pages/RecognitionResultPage.ets— 结果页,参数接收 +replaceUrl示例products/default/src/main/ets/components/CreationComponents.ets— 创作组件,pushUrl带参数跳转等待页
更多推荐

所有评论(0)