HarmonyOS APP《画伴梦工厂》开发第44篇-页面路由注册与动态加载——main_pages配置

第6.2篇:页面路由注册与动态加载------main_pages 配置

难度 :⭐⭐ 进阶

前置知识 :第 1.6 篇 页面路由与生命周期

涉及源文件products/default/src/main/resources/base/profile/main_pages.jsonproducts/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 完整声明

json 复制代码
{
  "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 字段被模块引用:

json 复制代码
{
  "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 加载首页

typescript 复制代码
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 的工作原理:

  1. 根据传入路径在已注册的页面表中查找 pages/Index
  2. 创建该页面的 @Component 实例
  3. 将实例挂载到 WindowStage 创建的窗口上
  4. 触发页面的 aboutToAppear 生命周期

注意loadContent 每个 Ability 生命周期中只能调用一次,用于加载根页面 。后续的所有页面跳转都要通过 Router.pushUrl 等路由 API 完成。


四、Router.pushUrl:动态页面导航

当用户从首页点击"拍照"或"涂鸦"按钮时,应用需要通过 Router.pushUrl 跳转到目标页面。

4.1 基本跳转

最简单的跳转方式------仅指定 url

typescript 复制代码
// 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 字段传递任意对象:

typescript 复制代码
// 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
    }
  });
}

再来看一个更丰富的例子------从等待页面跳转到结果页面时,携带了完整的上下文信息:

typescript 复制代码
// 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 定义参数接口

在源文件中,先定义参数类型接口:

typescript 复制代码
// RecognitionWaitingPage.ets (内部定义)
interface WaitingParams {
  source: string;
  workSource: string;
  prompt: string;
  imageUri: string;
  coverUri: string;
}

5.2 在 aboutToAppear 中接收

typescript 复制代码
// 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 参数安全防御

项目中采用了防御性取值的模式:

typescript 复制代码
const params = this.getUIContext().getRouter().getParams() as WaitingParams;
if (params && params.source) {
  // 安全使用
}

为什么需要 if (params && params.source) 检查?

场景 风险 防御方式
直接跳转(无 params) paramsundefined if (params && ...)
参数缺失某个字段 params.sourceundefined if (... && params.source)
跳转来源不同 参数结构不匹配 逐个字段检查而非整体断言

这种模式在 RecognitionResultPage 中同样使用:

typescript 复制代码
// 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 实现

typescript 复制代码
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 在两个生命周期中调用:

typescript 复制代码
// 首次启动
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  this.syncLaunchTab(want);
  // ...
}

// 从后台恢复或接收到新的 Want
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  this.syncLaunchTab(want);
}

6.3 在首页消费

Index.etsaboutToAppear 中读取 AppStorage 的全局状态:

typescript 复制代码
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 中有一个特殊的路由条目:

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 回到首页:

typescript 复制代码
// RecognitionResultPage.ets
this.getUIContext().getRouter().replaceUrl({
  url: 'pages/Index',
  params: {
    tab: 'works'
  }
});

使用 replaceUrl 而不是 pushUrl 的原因:

考量 说明
栈深度控制 如果一路 pushUrl,从首页 → 拍照 → 等待 → 结果 → 首页,栈深度会达到 5 层
用户体验 用户从结果页回到首页后,按返回键应退出 App,而不是回到结果页
内存管理 减少页面实例数量,降低内存占用

8.2 返回导航(back)

各页面通过 Router.back() 实现返回上一页的功能:

typescript 复制代码
// 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 页面路径常量化

建议将页面路径定义为常量,避免字符串拼写错误:

typescript 复制代码
// 推荐做法
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 参数接口复用

每个涉及参数传递的页面建议定义唯一的参数接口,并在入口处集中解析:

typescript 复制代码
// 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_pages
  • products/default/src/main/ets/defaultability/DefaultAbility.ets --- Ability 入口,含 loadContentsyncLaunchTab
  • products/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 带参数跳转等待页
相关推荐
我是小a3 小时前
「财务管家」智能记账应用:HarmonyOS NEXT 页面布局设计与实现详解
华为·harmonyos·鸿蒙·鸿蒙系统
SuperHeroWu73 小时前
HarmonyOS7特性解读
harmonyos·特性·智能化·多窗形态·场景协同·空间化
hans汉斯3 小时前
基于改进交叉熵损失函数与Transformer的心电信号高风险分类研究
功能测试·深度学习·算法·yolo·目标检测·分类·transformer
心中有国也有家5 小时前
使用 DevEco Studio 配置 Flutter 鸿蒙签名
学习·flutter·华为·harmonyos
如此风景6 小时前
鸿蒙开发状态管理 V2 全部装饰器详解速查表
harmonyos
爱吃大芒果6 小时前
鸿蒙 ArkTS 网络工程进阶:@ohos.net.http 模块下的长连接、超时与防内存泄漏实战
华为·harmonyos
FrameNotWork6 小时前
HarmonyOS 6.0 沉浸式状态栏与安全区域适配实战
华为·harmonyos
绝世番茄6 小时前
鸿蒙原生ArkTS布局方式之Stack+Opacity叠加淡入淡出深度解析
华为·harmonyos·鸿蒙
心中有国也有家7 小时前
鸿蒙Flutter开发环境从零搭建教程(Windows/macOS双平台·避坑版)
学习·flutter·华为·harmonyos