HarmonyOS开发实战:笔友-main_pages.json 路由表与页面注册机制

前言

在 HarmonyOS ArkTS 声明式开发范式中,路由表 是页面注册与跳转的中枢神经。它以一个简单的 JSON 文件描述了应用中所有可访问的页面,配合 router.pushUrlrouter.replaceUrl 等 API 完成页面间导航。

本文将以开源鸿蒙笔友通信应用 xiexin 的 main_pages.json 为蓝本,详细剖析路由表的格式、与 module.json5 的契约关系、windowStage.loadContent 如何依赖路由表,以及路由表设计中的常见陷阱。

提示:本文假设你已经了解 HarmonyOS Stage 模型基础。如果还不熟悉,建议先阅读前两篇文章。

一、main_pages.json 的定位

main_pages.json 是 ArkTS 声明式开发范式下的页面路由表,它告诉系统:"我的应用里有哪些页面可以被加载"。

这个文件位于:

text 复制代码
entry/src/main/resources/base/profile/main_pages.json

路径解析如下:

路径段 含义
entry/src/main 主模块源码根目录
resources 资源目录
base 默认资源限定(无特殊配置时的资源目录)
profile profile 类型的资源,用于存放 JSON 配置
main_pages.json 文件名,可自定义但需在 module.json5 中引用

提示:除了 base 目录,资源还可以放在 darkzh_CNen_US 等限定目录中。系统会根据设备状态自动选择匹配的资源。这部分内容将在后续文章中详细讲解。

二、xiexin 的路由表完整内容

xiexin 的 main_pages.json 内容非常简洁:

json 复制代码
{
  "src": [
    "pages/Index",
    "pages/SplashPage",
    "pages/ComposePage",
    "pages/ReadLetterPage",
    "pages/PenPalDetailPage",
    "pages/AddPenPalPage",
    "pages/StatsPage",
    "pages/EditProfilePage"
  ]
}

整个文件只有两个字段:

  1. src:字符串数组,列出所有页面路径
  2. (隐式)文件位置:决定了路由表如何被 module.json5 引用

三、src 数组中的路径约定

src 数组中的每个字符串代表一个页面路径。这个路径有几条重要约定:

3.1 路径前缀 pages/

路径中的 pages/ 前缀对应文件系统中的实际位置:

text 复制代码
entry/src/main/ets/pages/Index.ets
                  ^^^^^^^^^^^^^^^^^
                  对应路由表中的 "pages/Index"

注意几个细节:

  1. 省略 .ets 后缀 :路由表中不写 .ets,系统自动补全
  2. 路径分隔符 :使用正斜杠 /,即使在 Windows 上也保持一致
  3. 大小写敏感pages/Indexpages/index 是不同的页面
  4. 路径无 ets/ 前缀 :因为 ets 已经是默认源码根目录

3.2 路径与 @Entry 装饰器的对应

src 数组中的每个路径必须对应一个使用 @Entry 装饰的 ArkTS 文件。以 pages/Index 为例:

typescript 复制代码
// entry/src/main/ets/pages/Index.ets
@Entry
@Component
struct Index {
  @State currentTab: number = 0;

  build() {
    Tabs({ barPosition: BarPosition.End, index: this.currentTab }) {
      // ...
    }
  }
}

注意以下几点:

  • 一个文件只能有一个 @Entry@Entry 标识"这是路由表入口",多入口会引发编译错误
  • @Entry 必须搭配 @Component@Entry 修饰的 struct 必须同时用 @Component 修饰
  • struct 名称可以任意:与路由路径无关,但建议与文件名保持一致以便维护

3.3 路径顺序与首屏加载

src 数组中的第一个路径默认是应用的首屏 。但 xiexin 的路由表第一个是 pages/Index

json 复制代码
{
  "src": [
    "pages/Index",    // 首屏
    "pages/SplashPage",
    // ...
  ]
}

这看起来与"应用启动时先看到 SplashPage"的设计矛盾。实际上,首屏加载由 EntryAbility 控制:

typescript 复制代码
// entry/src/main/ets/entryability/EntryAbility.ets
onWindowStageCreate(windowStage: window.WindowStage): void {
  windowStage.loadContent('pages/Index', (err) => {
    // ...
  });
}

loadContent('pages/Index') 显式指定加载 pages/Index,而不是默认的 pages/SplashPage。这是 xiexin 的一个设计取舍:

  • 方案 A :首屏加载 pages/SplashPage,引导结束后跳转到 pages/Index
  • 方案 B :首屏直接加载 pages/Index,引导页通过条件渲染内嵌

xiexin 当前采用了方案 A 的变体:路由表首项是 pages/Index,但 Index 内部会根据 hasSeenSplash 标志决定是否显示引导内容。这种设计避免了启动时的一次页面跳转,提升了首屏速度。

四、路由表与 module.json5 的契约

main_pages.json 不是孤立的配置文件,它通过 module.json5pages 字段被引用:

json5 复制代码
// entry/src/main/module.json5
{
  "module": {
    "name": "entry",
    "type": "entry",
    "mainElement": "EntryAbility",
    "deviceTypes": ["phone", "tablet", "2in1"],
    "pages": "$profile:main_pages",
    "abilities": [/* ... */]
  }
}

pages 字段的值 $profile:main_pages 是一个资源引用,解析规则如下:

引用格式 含义
$profile:main_pages 引用 resources/base/profile/main_pages.json
$media:app_icon 引用 resources/base/media/app_icon.png
$string:app_name 引用 resources/base/element/string.json 中的 app_name
$color:start_window_background 引用 resources/base/element/color.json 中的颜色

提示:$profile:main_pages 中的 main_pages 是文件名(不含 .json 后缀),$profile: 是资源类型前缀。系统会在 resources/<qualifier>/profile/ 目录下查找匹配的 JSON 文件。

五、路由表的加载时机

理解路由表的加载时机,对于排查"页面找不到"问题至关重要。整个加载过程分为三个阶段:

5.1 编译阶段

在工程编译时,hvigor 工具会扫描 module.json5 中的 pages 字段,找到对应的 main_pages.json,然后:

  1. 校验路径合法性 :检查每个路径是否对应真实的 .ets 文件
  2. 生成路由映射表:将路径字符串映射到编译后的字节码位置
  3. 注入路由元数据:把映射表打包进 HAP 文件

如果某个路径对应的文件不存在,编译时会报错:

text 复制代码
ERROR: Bundle 'pages/NotExist' is not found in routes.

5.2 安装阶段

HAP 文件被安装到设备后,系统会在首次启动应用时读取路由映射表,并预加载页面元数据。这一步通常很快,但页面代码本身不会立即加载。

5.3 运行时加载

当调用 router.pushUrl({ url: 'pages/ComposePage' })windowStage.loadContent('pages/Index') 时,系统才会:

  1. 查找路由映射:根据路径字符串找到对应的字节码位置
  2. 加载字节码:动态加载对应页面的字节码到 ArkTS 运行时
  3. 实例化组件 :调用 @Entry 修饰的 struct 构造函数
  4. 触发 aboutToAppear:执行组件初始化逻辑
  5. 执行 build:生成 UI 树并渲染

这种"按需加载"的设计有两个好处:

  • 减少首屏内存占用:未访问的页面字节码不加载
  • 加速冷启动:只加载首屏需要的代码

提示:如果你想进一步优化冷启动,可以使用 ArkTS 的 lazy import 语法延迟加载非首屏模块的代码。

六、路由表的扩展实践

让我们看看如何在 xiexin 中扩展一个新的设置页面。

6.1 创建页面文件

typescript 复制代码
// entry/src/main/ets/pages/SettingsPage.ets
@Entry
@Component
struct SettingsPage {
  @State darkMode: boolean = false;

  build() {
    Column({ space: 16 }) {
      Text('设置').fontSize(24).fontWeight(FontWeight.Bold)
      Row() {
        Text('深色模式').fontSize(16)
        Toggle({ type: ToggleType.Switch, isOn: this.darkMode })
          .onChange((isOn: boolean) => {
            this.darkMode = isOn;
            AppStorage.setOrCreate('darkMode', isOn);
          })
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)
      .padding(16)
      .backgroundColor(AppColors.WHITE)
      .borderRadius(12)
    }
    .height('100%')
    .backgroundColor(AppColors.PRIMARY_BG)
    .padding(16)
  }
}

6.2 注册路由

json 复制代码
{
  "src": [
    "pages/Index",
    "pages/SplashPage",
    "pages/ComposePage",
    "pages/ReadLetterPage",
    "pages/PenPalDetailPage",
    "pages/AddPenPalPage",
    "pages/StatsPage",
    "pages/EditProfilePage",
    "pages/SettingsPage"
  ]
}

6.3 跳转到设置页

typescript 复制代码
import { router } from '@kit.ArkUI';

// 在 Index 页面添加设置入口
Button('设置').onClick(() => {
  router.pushUrl({ url: 'pages/SettingsPage' });
});

这样就完成了一个新页面的接入。

七、路由表设计的常见陷阱

7.1 路径拼写错误

json 复制代码
{
  "src": [
    "page/Index"
  ]
}

错误:page 应为 pages。这种错误在编译期不会被捕获,但运行时调用 loadContent 会失败。

7.2 忘记更新路由表

新增了一个 ComposePage.ets 文件,但忘记在路由表中添加 "pages/ComposePage"。结果是:

  • 编译通过:文件本身被编译进 HAP
  • 运行时失败:router.pushUrl({ url: 'pages/ComposePage' }) 报错"路由不存在"

提示:建议在 CI/CD 流程中加入"路由表校验"步骤,自动扫描 src/main/ets/pages/ 下的 .ets 文件,与 main_pages.json 比对是否一致。

7.3 路由表条目过多

随着业务增长,main_pages.json 可能膨胀到几十甚至上百个条目。这本身不是问题,但会带来两个隐患:

  • 首屏代码量增加:路由表本身不大,但页面越多,编译产物中元数据越多
  • 维护成本上升:手动维护大列表容易遗漏

解决方案是采用模块化路由 :把不同业务模块的路由表拆分到不同 profile 文件,在 module.json5 中按需引用。

7.4 大小写敏感问题

json 复制代码
{
  "src": [
    "pages/index"
  ]
}

错误:文件实际是 Index.ets(首字母大写),路由路径却写成 index。在 Linux/macOS 文件系统上可能不报错,但路由查找时找不到匹配项。

八、main_pages.json 与路由跳转 API 的协作

理解路由表后,我们来看看它如何与 ArkUI 的 router API 协作。

8.1 router.pushUrl

typescript 复制代码
router.pushUrl({ url: 'pages/ComposePage' });

pushUrl 会将目标页面压入路由栈,当前页面保留在栈底。用户点击返回键时,会自动出栈,回到之前的页面。

8.2 router.replaceUrl

typescript 复制代码
router.replaceUrl({ url: 'pages/Index' });

replaceUrl替换当前页面,原页面从栈中移除。这种跳转方式常用于"启动引导页跳转主页"的场景------引导页不应该出现在返回栈里。

xiexin 的 SplashPage 就采用了这种模式:

typescript 复制代码
// SplashPage 的"开始写信"按钮
Button('开始写信')
  .onClick(() => {
    router.replaceUrl({ url: 'pages/Index' });
  })

这样用户从 SplashPage 进入 Index 后,按返回键不会回到 SplashPage,而是直接退出应用。

8.3 router.pushUrl 带参数

typescript 复制代码
router.pushUrl({
  url: 'pages/PenPalDetailPage',
  params: { id: 123 }
});

目标页面通过 router.getParams() 获取参数:

typescript 复制代码
@Entry
@Component
struct PenPalDetailPage {
  @State penPalId: number = 0;

  aboutToAppear(): void {
    const params = router.getParams() as Record<string, number>;
    this.penPalId = params.id;
  }

  build() { /* ... */ }
}

提示:router.getParams() 必须在 aboutToAppear 或之后的生命周期调用,在 build 之外的其他时机可能返回 undefined

8.4 router.back 返回上一页

typescript 复制代码
// 返回上一页
router.back();

// 返回指定页面(清除中间页面)
router.back({ url: 'pages/Index' });

router.back({ url: 'pages/Index' }) 会一直出栈,直到遇到 pages/Index。如果栈中没有这个页面,调用无效。

九、路由栈深度管理

HarmonyOS 的路由栈有最大深度限制(默认 32 层)。如果应用业务复杂,可能触发栈溢出:

text 复制代码
Error: The route stack exceeds the maximum limit.

管理路由栈深度的几个建议:

  1. replaceUrl 替代 pushUrl:当不需要保留历史页面时,用 replace 避免栈增长
  2. router.clear() 清栈:在退出登录等场景清空整个路由栈
  3. router.back({ url: '...' }) 深度返回:避免逐层 pop

十、main_pages.json 的进阶用法

10.1 多 profile 文件

module.json5pages 字段只能引用一个 profile 文件,但这个文件可以包含多个 src 数组

json 复制代码
{
  "src": [
    "pages/Index",
    "pages/SplashPage"
  ],
  "src-extension": [
    "pages/ComposePage",
    "pages/ReadLetterPage",
    "pages/PenPalDetailPage",
    "pages/AddPenPalPage",
    "pages/StatsPage",
    "pages/EditProfilePage"
  ]
}

这种写法允许按业务场景组织页面,但实际加载时仍会合并所有 src* 数组中的路径。

10.2 路由表与动态加载

对于大型应用,可以把"按需加载"的页面放在独立的 HAR/HSP 模块中,每个模块有自己的路由表。这种"模块化路由"是 HarmonyOS 多模块架构的关键能力。

总结

本文详细剖析了 HarmonyOS ArkTS 路由表 main_pages.json 的格式、契约关系、加载机制和扩展实践。我们看到 xiexin 的路由表虽然只有 8 个条目,却完整覆盖了笔友通信场景的所有页面,体现了"小而美"的设计哲学。

理解路由表的关键是把握"四个一"原则:一个 src 数组、一个路径约定、一个 module.json5 引用、一个 EntryAbility 首屏加载。这四个环节环环相扣,共同构成了 HarmonyOS 应用页面注册与跳转的基础设施。

下一篇文章我们将深入 module.json5,剖析模块能力声明、权限配置、abilities 数组等核心配置项。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源

相关推荐
qizayaoshuap3 小时前
# 颜色混合器 — HarmonyOS RGB调色板与Slider组件实战
华为·harmonyos
不言鹅喻4 小时前
HarmonyOS ArkTS 实战:实现一个校园体育场馆预约应用
pytorch·华为·harmonyos
●VON4 小时前
鸿蒙 PC Markdown 编辑器存储安全:AtomicFile 原子提交与故障注入
安全·华为·编辑器·harmonyos·鸿蒙
xianjixiance_4 小时前
HarmonyOS开发实战:小分享-ArkUI 基础组件详解——Text、Button、Image
华为·harmonyos
ov二号5 小时前
鸿蒙原生ArkTS布局方式之侧边栏SideBarContainer布局深度指南
华为·harmonyos
echohelloworld116 小时前
HarmonyOS开发实战:笔友-EmptyState 空状态组件的条件式占位设计
harmonyos·鸿蒙
qizayaoshuap6 小时前
# 倒计时器 — HarmonyOS TextInput与计时任务管理深入实践
pytorch·深度学习·华为·harmonyos
木木子227 小时前
# 猜数字游戏 — HarmonyOS交互逻辑与随机算法实现
算法·游戏·华为·交互·harmonyos
爱写代码的阿木7 小时前
基于鸿蒙OS开发附近社交游戏平台(十二)-狼人杀角色技能与投票系统
游戏·华为·harmonyos