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

难度:⭐⭐ 进阶
前置知识:第 1.6 篇 页面路由与生命周期
涉及源文件products/default/src/main/resources/base/profile/main_pages.jsonproducts/default/src/main/ets/defaultability/DefaultAbility.ets

6.2-1

一、概述

在 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 的工作原理:

  1. 根据传入路径在已注册的页面表中查找 pages/Index
  2. 创建该页面的 @Component 实例
  3. 将实例挂载到 WindowStage 创建的窗口上
  4. 触发页面的 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) paramsundefined if (params && ...)
参数缺失某个字段 params.sourceundefined 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.etsaboutToAppear 中读取 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_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 带参数跳转等待页
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐