《鸿蒙原生应用开发实战》第四篇:多页面导航与参数传递实战

前言

一个真正的应用不可能只有一个页面。页面之间的跳转、参数传递、数据回传是开发中频率最高的操作。HarmonyOS 提供了强大的 router 路由模块来管理页面导航。

在本篇中,我们将结合「光遇·心境」应用的 5 个页面,深入讲解:

  • router 路由核心 API
  • 页面间参数传递的多种方式
  • 页面生命周期与数据加载时机
  • 路由栈管理与返回处理
  • 参数类型安全与错误处理

一、路由系统概述

路由表配置

所有页面必须在 main_pages.json 中注册:

{
  "src": [
    "pages/Index",
    "pages/ScenePage",
    "pages/DetailPage",
    "pages/FavPage",
    "pages/ProfilePage"
  ]
}

注册时只写路径(不含 .ets 后缀),路径相对于 ets/ 目录。5 个页面组成了一个完整的导航体系:

                    ┌─────────────┐
                    │   Index     │  ← 首页(入口)
                    └──────┬──────┘
          ┌────────────────┼────────────────┐
          ▼                ▼                ▼
    ┌──────────┐    ┌───────────┐    ┌───────────┐
    │ ScenePage│    │  FavPage  │    │ProfilePage│
    └─────┬────┘    └───────────┘    └───────────┘
          ▼
    ┌───────────┐
    │DetailPage │  ← 详情页(可来自多个入口)
    └───────────┘

二、router 核心 API

导入方式

API 23 强制要求从 @ohos.router 导入:

// ✅ 正确 — API 23 支持的方式
import router from '@ohos.router';

// ❌ 错误 — API 23 不导出此路径
// import { router } from '@kit.AbilityKit';

页面跳转

// 基本跳转(无参数)
router.pushUrl({ url: 'pages/FavPage' });

// 带参数跳转
router.pushUrl({
  url: 'pages/DetailPage',
  params: { sceneId: 1 }
});

// 多个参数
router.pushUrl({
  url: 'pages/ScenePage',
  params: {
    category: '海洋',
    from: 'homepage'
  }
});

页面返回

// 简单返回上一页
router.back();

// 返回并携带数据回传
router.back({
  url: 'pages/Index',
  params: { result: 'fromDetail' }
});

获取参数

// 在目标页面的 aboutToAppear 中获取参数
aboutToAppear(): void {
  const params = router.getParams() as Record<string, Object>;
  if (params && params['sceneId']) {
    const id = params['sceneId'] as number;
    this.scene = getSceneById(id);
  }
}

三、参数传递实战案例

案例1:首页 → 场景列表(分类筛选)

首页的分类入口点击后,将分类名称传递给 ScenePage:

// Index.ets — 分类入口点击
CategoryItem(name: string) {
  Column() {
    Text(this.categoryIcons[name] || '✨').fontSize(28)
    Text(name).fontSize($r('app.float.caption_font_size'))
  }
  .onClick(() => {
    router.pushUrl({
      url: 'pages/ScenePage',
      params: { category: name }   // 传递"晨光"/"森林"等
    });
  })
}

在 ScenePage 接收并使用参数:

// ScenePage.ets
@State selectedCategory: string = '全部';
@State scenes: SceneItem[] = getScenes();

aboutToAppear(): void {
  const params = router.getParams() as Record<string, Object>;
  if (params && params['category']) {
    const cat = params['category'] as string;
    this.selectedCategory = cat;
    this.scenes = getScenesByCategory(cat);  // 根据分类筛选
  }
}

效果:从首页点击"海洋"分类 → 直接跳转到 ScenePage 并展示所有海洋场景。

案例2:多个入口 → 详情页

DetailPage 可以从 3 个不同入口进入:

// 入口1:首页的每日推荐卡片
// Index.ets
router.pushUrl({
  url: 'pages/DetailPage',
  params: { sceneId: this.dailyScene.id }
});

// 入口2:场景列表的卡片
// ScenePage.ets
router.pushUrl({
  url: 'pages/DetailPage',
  params: { sceneId: item.id }
});

// 入口3:收藏列表的卡片
// FavPage.ets
router.pushUrl({
  url: 'pages/DetailPage',
  params: { sceneId: item.id }
});

详情页统一接收:

// DetailPage.ets
@State scene: SceneItem | undefined = undefined;

aboutToAppear(): void {
  const params = router.getParams() as Record<string, Object>;
  if (params && params['sceneId']) {
    const id = params['sceneId'] as number;
    this.scene = getSceneById(id);
    if (this.scene) {
      this.checkFavStatus();
    }
  }
}

设计原则:DetailPage 不关心从哪个入口来的,它只读取 sceneId 参数。这种松耦合的设计让代码更容易维护。


四、页面生命周期与数据加载

页面生命周期方法

方法 触发时机 适用场景
aboutToAppear() 页面即将显示(每次进入) 加载数据、读取参数
onPageShow() 页面每次显示时 刷新数据(从其他页面返回时)
onPageHide() 页面隐藏时 暂停动画/音视频
aboutToDisappear() 页面销毁前 释放资源

aboutToAppear vs onPageShow 的区别

// 场景:收藏页 → 详情页 → 点击收藏 → 返回收藏页

// aboutToAppear() — 只在新创建页面时调用
// 第一次进入 FavPage: ✅ 调用
// 从 DetailPage 返回 FavPage: ❌ 不调用(页面还在栈中)

// onPageShow() — 每次页面显示时都调用
// 第一次进入 FavPage: ✅ 调用
// 从 DetailPage 返回 FavPage: ✅ 调用

这就是为什么我们在 FavPage 中使用 aboutToAppear 而不是 onPageShow —— 因为收藏列表在返回时需要自动刷新,我们需要在每次显示时重新加载数据:

@Component
struct FavPage {
  @State favScenes: SceneItem[] = [];

  aboutToAppear(): void {
    this.loadFavScenes();  // 每次进入页面时刷新收藏列表
  }

  loadFavScenes(): void {
    const favIds: number[] = AppStorage.get<number[]>(FAV_KEY) || [];
    const list: SceneItem[] = [];
    for (const id of favIds) {
      const scene = getSceneById(id);
      if (scene) {
        list.push(scene);
      }
    }
    this.favScenes = list;
    this.favCount = list.length;
  }
}

性能建议

  • aboutToAppear 中不要做耗时操作(如网络请求),如果需要异步加载数据,使用 aboutToAppear 发起请求,用 @State 监听结果
  • 频繁刷新的数据用 onPageShow,一次性初始化用 aboutToAppear

五、路由栈管理

路由栈的工作机制

router 内部维护了一个页面栈(LIFO):

初始状态: [Index]
push FavPage: [Index, FavPage]
push DetailPage: [Index, FavPage, DetailPage]
back():       [Index, FavPage]     ← DetailPage 出栈
back():       [Index]              ← FavPage 出栈

router.pushUrl 与 router.replaceUrl

API 行为 路由栈
pushUrl 压入新页面,保留当前页面 栈增长
replaceUrl 替换当前页面,不保留 栈不变
back() 弹出栈顶页面 栈缩小
clear() 清空栈 空栈

replaceUrl 的典型使用场景是登录页 → 首页:

// 登录成功后替换,用户无法返回到登录页
router.replaceUrl({ url: 'pages/Index' });

避免路由栈过深

理论上路由栈可以无限增长,但过深的栈会消耗内存。建议:

  • 不用的页面及时 back()
  • 使用 replaceUrl 替代某些场景的 pushUrl
  • 监听页面栈数量

六、参数传递的高级话题

参数类型安全

由于 router.getParams() 返回的是 Object 类型,需要进行类型断言:

// 安全获取参数的模式
aboutToAppear(): void {
  const params = router.getParams() as Record<string, Object> | undefined;
  
  // 使用可选链和类型守卫
  const sceneId = params?.['sceneId'];
  if (typeof sceneId === 'number') {
    this.scene = getSceneById(sceneId);
  }
  
  // 或者使用断言 + 兜底
  const cat = (params?.['category'] as string) || '全部';
  this.selectedCategory = cat;
}

可以传递的数据类型

类型 示例 是否支持
string { name: 'hello' }
number { id: 42 }
boolean { isAdmin: true }
Object { user: { name: 'Tom' } }
Array { ids: [1,2,3] }
Function { cb: ()=>{} } ❌ 不支持
复杂嵌套 对象含多层嵌套 ✅ 会被序列化

参数的大小限制

路由参数不要传大数据(超过 10KB 的数据建议用 AppStorage 或全局变量共享),因为参数在序列化/反序列化过程中有性能开销。


七、完整的导航流程图

┌─────────────────────────────────────────────────────────────┐
│                     用户操作流程                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  打开App → Index(首页)                                     │
│     ├── 点击"每日推荐" → push({sceneId}) → DetailPage       │
│     │     └── 点击收藏 ❤️ → AppStorage 更新 → back          │
│     │                                                       │
│     ├── 点击"晨光"分类 → push({category: '晨光'}) → ScenePage│
│     │     └── 点击场景卡片 → push({sceneId}) → DetailPage    │
│     │           └── back → back                             │
│     │                                                       │
│     ├── 点击❤️按钮 → push({}) → FavPage                     │
│     │     ├── 点击卡片 → push({sceneId}) → DetailPage        │
│     │     └── 点击取消收藏 → AppStorage 更新                 │
│     │                                                       │
│     └── 点击👤按钮 → push({}) → ProfilePage                 │
│           └── back                                          │
│                                                             │
│  任意页面 ← back 返回上一页                                  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

八、常见踩坑记录

坑1:使用 @kit.AbilityKit 的 router

现象:编译通过但运行时 crash
原因:API 23 不导出 @kit.AbilityKit 中的 router
解决:统一使用 import router from '@ohos.router'

坑2:参数接收不到

现象router.getParams() 返回 undefined
原因:在 aboutToAppear 之前调用,或页面未通过 router 进入
解决:在 aboutToAppear 生命周期中获取参数

坑3:返回时数据没有刷新

现象:从详情页取消收藏返回收藏页,列表没有变化
原因:FavPage 在 aboutToAppear 中加载数据,但页面在栈中未被销毁,不会调用 aboutToAppear
解决:使用 onPageShow() 代替,或在 AppStorage 上注册监听


九、路由与状态管理的协作模式

// 推荐的协作模式
// 1. 路由负责页面导航和轻量参数
router.pushUrl({
  url: 'pages/DetailPage',
  params: { sceneId: 1 }     // 只传递 ID,不传递完整数据
});

// 2. 共享数据通过 AppStorage 管理
AppStorage.set<number[]>(FAV_KEY, favList);

// 3. 页面从数据仓库获取完整数据
const scene = getSceneById(sceneId);  // 通过 ID 获取

这种设计的好处:

  • 参数简洁,只传 ID 不传整个对象
  • 数据源统一,避免多页面数据不一致
  • 调试方便,数据流向清晰

在这里插入图片描述

总结

本篇我们深入学习了:

  1. ✅ router 路由核心 API 与导入规范
  2. ✅ 4 种参数传递实战场景
  3. ✅ 页面生命周期与数据加载时机
  4. ✅ 路由栈管理与返回机制
  5. ✅ 参数类型安全与错误处理
  6. ✅ 路由与状态管理的协作模式

路由是连接页面的桥梁,掌握好这一章,多页面应用开发就会得心应手。最后一篇将聚焦完整的收藏功能闭环与应用发布准备。

下一篇预告:收藏功能、资源管理与构建发布 —— 从功能闭环到 HAP 包发布全流程

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐