《HarmonyOS技术精讲-UI开发》第6篇:页面路由与导航

页面跳转是应用开发中最基本的能力。在 HarmonyOS NEXT 中,router.pushPathrouter.popPath 的组合经常被误用------很多人发现官方示例能运行,但实际项目里参数接收不到、返回后页面空白、状态丢失等问题频发。

这篇文章不讲概念,直接写一个完整的多页面跳转案例:A 页面点击按钮,跳转到 B 页面并传递一个用户 ID,B 页面接收并显示。代码全部可运行,同时把几个常见的坑和最佳实践说清楚。


它解决什么问题

HarmonyOS 的页面路由(@ohos.router)负责管理应用内页面的跳转与返回。它主要解决两个场景:

  • 页面跳转:从一个页面导航到另一个页面,同时携带参数。
  • 页面返回 :执行返回操作时,可以携带数据回传(通过 popPathparams)。

对比旧版本的 router.push(已废弃),router.pushPath 的优势在于:

  • 支持更好的类型安全(官方推荐)
  • 支持 recovery(页面栈恢复)
  • 配合 @Entry 的生命周期更清晰
对比点 router.pushPath router.push(旧)
参数传递 使用 params 对象 使用 params 对象
页面栈恢复 支持 不支持
类型安全 强类型推荐 弱类型
生命周期 正常触发 正常触发

如果只是简单跳转,pushPath 足够。如果需要复杂的页面栈管理(如重定向、替换页面),可以用 pushNamedRoutereplacePath


环境说明

text 复制代码
DevEco Studio 版本:DevEco Studio 6.1.0 及以上
HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上
目标设备:手机

核心实现

1. 页面注册(main_pages.json)

在项目的 resources/base/profile/main_pages.json 中注册两个页面:

json 复制代码
{
  "src": [
    "pages/Index",
    "pages/UserDetail"
  ]
}

如果不注册,跳转时会直接报错:Page not found

2. A 页面(Index.ets)

A 页面有一个按钮,点击后跳转到 B 页面,并携带 userId 参数。

typescript 复制代码
// pages/Index.ets
import { router } from '@kit.ArkUI'

@Entry
@Component
struct Index {
  build() {
    Column() {
      Button('跳转到用户详情')
        .onClick(() => {
          router.pushPath({
            url: 'pages/UserDetail',
            params: {
              userId: '12345'
            }
          })
        })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

注意paramsRecord<string, Object>,可以传递任意类型,但建议只传递基本类型或 JSON 序列化的对象。如果传递函数或日期对象,在 B 页面接收时可能丢失类型。

3. B 页面(UserDetail.ets)

B 页面接收参数并显示在界面上。

typescript 复制代码
// pages/UserDetail.ets
import { router } from '@kit.ArkUI'

@Entry
@Component
struct UserDetail {
  @State userId: string = ''

  aboutToAppear(): void {
    // 从路由参数中获取 userId
    const params = router.getParams() as Record<string, string>
    if (params && params.userId) {
      this.userId = params.userId
    }
  }

  build() {
    Column() {
      Text(`接收到的用户ID:${this.userId}`)
        .fontSize(20)
        .margin({ bottom: 20 })

      Button('返回')
        .onClick(() => {
          router.popPath()
        })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

关键点

  • 参数接收必须在 aboutToAppear 中执行(或 onPageShow),不能在 build() 中获取,因为 build() 会多次调用,而参数只在页面初始化时有效。
  • 使用 as Record<string, string> 做类型断言,确保类型安全。但注意 getParams() 返回的是 Object,如果参数是数字类型,可能会被转成字符串,具体取决于序列化方式。

常见问题与踩坑

问题 1:参数接收不到或者类型不对

现象 :B 页面 router.getParams() 返回 undefined 或者参数值是 [object Object]

原因

  • 页面未在 main_pages.json 注册,导致实际跳转失败。
  • params 传递了非可序列化类型(如函数、Symbol),或嵌套过深的对象。
  • build() 中读取参数,但 build() 执行时参数还未加载完成。

解法

  1. 检查 main_pages.json 是否包含目标页面。
  2. 传递的参数保持简单,推荐用基本类型或扁平对象。
  3. aboutToAppear 生命周期内读取参数,不要推迟。

问题 2:返回后 A 页面状态丢失

现象 :从 B 页面通过 popPath 返回后,A 页面之前的状态(如输入框内容、滚动位置)恢复了默认值。

原因popPath 默认会销毁当前页面实例,但 A 页面本身属于上一个页面栈,它的状态由框架自动恢复------但仅限于通过 @State@Prop 管理的属性。如果状态没有正确声明为响应式,就会丢失。

解法

  • 确保 A 页面中需要保持的状态都用 @State 装饰。
  • 如果 A 页面是 @Entry 组件,它的状态在返回后会自动保留(因为页面栈未销毁),但前提是页面没有被系统回收(低内存时可能销毁)。

最佳实践

  1. 避免硬编码 URL

    建议将页面路径定义为常量,例如 const PAGE_USER_DETAIL = 'pages/UserDetail'。否则后期重构时,一旦修改文件名,所有硬编码的跳转都会失效。

  2. aboutToAppear 接收参数

    这是页面初始化时最早的可控生命周期,保证参数在 UI 渲染前已正确赋值。不要在 build() 中依赖参数,因为 build() 可能被多次触发。

  3. 返回时回传数据

    如果需要从 B 页面向 A 页面回传数据,使用 router.popPath({ params: { result: 'ok' } })。A 页面可以通过 onPageShow 监听页面显示事件并获取回传参数。

  4. 页面栈深度控制

    连续多次 pushPath 会不断增加页面栈,可能导致内存问题。如果只需要覆盖当前页面,使用 router.replacePath


Demo 入口

完整的项目结构:

复制代码
entry/src/main/ets/
├── pages/
│   ├── Index.ets          // A 页面
│   └── UserDetail.ets     // B 页面
├── resources/
│   └── base/
│       └── profile/
│           └── main_pages.json   // 页面注册

Index.etsUserDetail.ets 的完整代码已在上文给出。


FAQ

Q1:为什么在模拟器上跳转正常,真机上却报错"页面不存在"?

A:模拟器会自动同步 main_pages.json,但真机调试时可能存在缓存未更新。请手动执行"清理项目"或卸载重装应用。另外,检查 url 路径是否与 main_pages.json 中定义的 src 完全一致(包括大小写)。

Q2:为什么 router.getParams() 返回的对象中的数字变成了字符串?

A:目前 HarmonyOS 的路由参数序列化会将所有基本类型转为字符串,这是已知的限制。推荐在传递数字时,在接收端手动 parseIntNumber() 转换。

Q3:页面返回后,原来 A 页面的异步请求还在执行,会导致状态错误?

A:是的。在 aboutToDisappear(页面即将销毁)中应当取消所有异步任务(如网络请求、定时器)。特别是使用了 async/await 的地方,绑定 AbortController 或清理 setTimeout


如果你也遇到类似的页面路由问题,可以重点检查生命周期和参数类型 ------ 这两处是初学者最容易出错的地方。