跳到主要内容

第六章:页面路由

本章配套代码:code/Chapter6(Navigation 跳转 Demo) 前置要求:已完成状态管理章节

6.1 学习目标

  • 理解 HarmonyOS 两种路由方案(Navigation / Router)
  • 掌握 Navigation 组件:显示模式、标题栏、菜单栏、工具栏
  • 掌握页面跳转、参数传递、返回与返回数据获取
  • 掌握路由拦截(setInterception)与路由表(routerMap)配置
  • 了解页面生命周期

6.2 路由概述

路由负责管理应用页面的跳转与数据传递。

┌─────────────────────────────────────┐
│ 两种路由方案 │
│ │
│ Router 模块 Navigation组件 │
│ · 基于页面栈 · 基于组件树 │
│ · 页面级跳转 · 应用内导航 │
│ · 简单场景 · 推荐(支持 │
│ · 分栏/深链) │
└─────────────────────────────────────┘
方案优点缺点适用
Router简单直接功能单一简单跳转
Navigation灵活强大,推荐学习成本稍高主流应用

🔑 本课程推荐 Navigation,这是当前 HarmonyOS 应用开发的标准方案。

"页面"的定义差异(关键概念):

框架"页面"指什么
组件导航 Navigation一个 NavDestination 组件包含的内容
页面路由 @ohos.router一个 @Entry 装饰的自定义组件

组件导航(Navigation)优势:

  1. 显式区分标题栏、内容区、工具栏,管理与动效更灵活
  2. 显式提供路由容器概念,支持在全模态、半模态、弹窗中显示
  3. 基于通用 @Builder,由开发者决定页面别名 ↔ 页面 UI 的映射关系
  4. 整合 UX 设计与一次开发多端部署,默认提供统一标题、页面切换与单双栏自适应
  5. 页面切换动效基于组件属性动效实现,过渡更丰富
  6. 开放页面栈对象,可继承做二次管理

⚠️ @ohos.router 官方已不再推荐,本课程不展开;后续跳转一律使用 Navigation。

6.3 Navigation 组件

6.3.1 基本结构

@Entry
@ComponentV2
struct NavigationExample {
private navPathStack: NavPathStack = new NavPathStack() // ① 路由栈

build() {
Navigation(this.navPathStack) { // ② 绑定路由栈
Column() {
Text('首页')
}
}
.title('Navigation示例') // ③ 标题
.mode(NavigationMode.Stack) // ④ 模式
.navDestination(this.PageMap) // ⑤ 页面映射
}

@Builder // ⑥ 目标页面映射
PageMap(name: string, param: object) {
if (name === 'DetailPage') {
DetailPage({ ... })
}
}
}

Navigation 五要素:

要素说明
NavPathStack路由栈实例,管理页面栈
Navigation(stack)容器组件
.title()页面标题
.mode()Stack(单页)/ Split(分栏)
.navDestination()页面名称 → 组件映射

6.3.2 NavigationMode

模式说明适用
NavigationMode.Auto自适应(默认)<600vp 单栏,>600vp 分栏默认推荐
NavigationMode.Stack强制单栏,跳转时整页替换手机/窄屏
NavigationMode.Split强制分栏(左列表右详情),跳转只替换右栏平板/折叠屏

💡 导航目标页面建议用 NavDestination 作为根容器,可自动获得返回箭头、标题栏与转场动画。

6.3.3 标题栏 titleMode

标题栏位于顶部,.title() 设主标题,.titleMode() 设标题栏模式:

模式说明
NavigationTitleMode.Mini普通标题栏,适合一级页面
NavigationTitleMode.Full大标题,突出页面主题
NavigationTitleMode.Free滚动时标题随内容缩小(子标题不变淡出)
Navigation(this.navPathStack) { ... }
.title('主标题') // 也可用于 NavDestination 设置子标题
.titleMode(NavigationTitleMode.Mini)
.hideTitleBar(true) // 隐藏标题栏

6.3.4 菜单栏 menus

菜单栏位于右上角,通过 .menus() 设置,支持图标数组与自定义 Builder:

Navigation(this.navPathStack) { ... }
.menus([
{ value: '编辑', icon: $r('sys.media.ohos_ic_public_edit') },
{
value: '扫码',
icon: $r('sys.media.ohos_ic_public_scan'),
action: () => { /* 点击逻辑 */ }
}
])

💡 竖屏最多显示 3 个图标、横屏 5 个,多余图标自动收纳进"更多"菜单。

6.3.5 工具栏 toolbarConfiguration

工具栏位于底部,通过 .toolbarConfiguration(内容, 属性) 设置:

Navigation(this.navPathStack) { ... }
.toolbarConfiguration(
// 内容
[
{ value: '首页', icon: $r('sys.media.leave_home_fill') },
{
value: '分享',
icon: $r('sys.media.ohos_ic_public_share'),
action: () => this.navPathStack.pushPath({ name: 'pageA' })
},
{ value: '我的', icon: $r('sys.media.person_shield') }
],
// 属性
{
backgroundColor: '#ffffff', // 背景色
barStyle: BarStyle.STANDARD, // 布局方式
backgroundBlurStyle: BlurStyle.COMPONENT_ULTRA_THICK // 背景模糊
}
)

💡 barStyleSTANDARD 上下布局(默认)、STACK 层叠布局(内容上层)、SAFE_AREA_PADDING 安全区。

6.4 页面跳转

6.4.1 pushPath 推入页面

// 无参数跳转
this.navPathStack.pushPath({ name: 'DetailPage' })

// 带参数跳转
this.navPathStack.pushPath({
name: 'DetailPage',
param: { title: '来自首页的消息', id: 42 }
})

Push 系列三种形式:

API说明
pushPath({ name, param })普通跳转(对象传参)
pushPathByName(name, param)按名称跳转(基本类型直接传)
pushDestination({ name, param })带错误码异步回调的跳转
// 带错误码跳转:成功/失败均有回调
this.navPathStack.pushDestination({ name: 'DetailPage', param: { id: 1 } })
.then(() => console.info('跳转成功'))
.catch(() => console.error('跳转失败'))

6.4.2 接收参数

通过 @Builder PageMap 映射到目标组件,参数经 @Param 接收:

@ComponentV2
struct DetailPage {
@Param title: string = ''
@Param id: number = 0

build() {
Column() {
Text(`标题: ${this.title}`)
Text(`ID: ${this.id}`)
}
}
}

💡 paramobject 类型,映射时需用类型断言转换为具体类型:(param as ParamType).title

6.4.3 带返回回调跳转(onPop)

跳转时注册 onPop 回调,页面出栈时拿到返回数据:

this.navPathStack.pushPathByName('DetailPage', { id: 42 }, (popInfo: PopInfo) => {
console.info('返回页面: ' + popInfo.info.name)
console.info('返回数据: ' + JSON.stringify(popInfo.result))
})

6.4.4 参数获取

NavDestination 子页首次创建触发 onReady返回时通过 onResult 接收路由参数:

NavDestination() {
Text('详情页')
}
.onReady((context: NavDestinationContext) => {
console.info('页面参数: ' + JSON.stringify(context.pathInfo.param))
})
.onResult((result: PopInfo) => {
console.info('返回数据: ' + JSON.stringify(result.result))
})

也可主动从栈中查询参数:

this.navPathStack.getAllPathName()          // 栈中所有页面 name 集合
this.navPathStack.getParamByName('PageOne') // 指定页面参数
this.navPathStack.getParamByIndex(1) // 指定索引的参数
this.navPathStack.getIndexByName('PageOne') // 指定页面的索引集合

6.4.5 返回上一页

// 方式一:通过路由栈引用(推荐,本项目采用)
this.navPathStack.pop()

// 方式二:通过 UIContext 获取(无需传递栈引用)
const navPathStack = this.getUIContext().getNavPathStack()
navPathStack.pop()

💡 方式二更通用,但 Navigation 初始化阶段可能取不到。传递 navPathStack 引用(方式一)更可靠。

6.4.6 其他导航方法(栈操作全集)

Pop 返回系列:

API说明
pop()返回上一页
popToName(name)返回到指定名称页面
popToIndex(i)返回到指定索引页面
clear()清空栈(回根页面)

Replace 替换系列(原页面不留栈记录):

navPathStack.replacePath({ name: 'NewPage', param: { id: 1 } })
navPathStack.replacePathByName('NewPage', { id: 1 })
navPathStack.replaceDestination({ name: 'NewPage' }) // 带错误码回调

Remove 删除系列:

navPathStack.removeByName('DetailPage')          // 按名称删除
navPathStack.removeByIndexes([1, 3]) // 按索引删除
navPathStack.removeByNavDestinationId('1') // 按页面 id 删除

Move 移动系列(把指定页面移到栈顶):

navPathStack.moveToTop('DetailPage')   // 按名称移动
navPathStack.moveIndexToTop(1) // 按索引移动

6.5 路由拦截(setInterception)

NavPathStack.setInterception() 设置跳转拦截回调,接收一个 NavigationInterception 对象,包含三个回调:

回调时机
willShow跳转前回调,可操作栈实现拦截/重定向
didShow跳转后回调,此回调内操作栈在下次跳转生效
modeChange单双栏显示状态变更时触发

案例:未登录访问个人信息 → 拦截到登录页:

import { promptAction } from '@kit.ArkUI'

@Entry
@ComponentV2
struct Index {
@Local isLogin: boolean = false
private navPathStack: NavPathStack = new NavPathStack()

@Builder
PageMap(name: string, param: object) {
if (name === 'user') {
User({ isLogin: this.isLogin, navPathStack: this.navPathStack })
} else if (name === 'login') {
Login({ isLogin: this.isLogin, navPathStack: this.navPathStack })
}
}

build() {
Navigation(this.navPathStack) {
Button('查看用户信息')
.onClick(() => this.navPathStack.pushPath({ name: 'user' }))
}
.onAppear(() => {
this.navPathStack.setInterception({
willShow: (
from: NavDestinationContext | 'navBar',
to: NavDestinationContext | 'navBar',
operation: NavigationOperation,
isAnimated: boolean
) => {
if (typeof to === 'string') return // 'navBar' 为根页面
if (to.pathInfo.name === 'user' && !this.isLogin) {
this.navPathStack.pop() // 取消本次跳转
promptAction.openToast({ message: '未登录,请先登录' })
this.navPathStack.pushPath({ name: 'login' }) // 重定向到登录页
}
}
})
})
.navDestination(this.PageMap)
}
}

@ComponentV2
struct User {
@Param isLogin: boolean = false
@Param navPathStack: NavPathStack = new NavPathStack()

build() {
NavDestination() {
Text('用户信息')
Button('退出登录')
.onClick(() => this.navPathStack.pop())
}
}
}

@ComponentV2
struct Login {
@Param isLogin: boolean = false
@Param navPathStack: NavPathStack = new NavPathStack()

build() {
NavDestination() {
Button('登录')
.onClick(() => this.navPathStack.pop())
}
}
}

💡 核心:进入 willShow 回调时路由栈已发生变化,需先 pop() 取消再重定向。

6.6 路由表配置(routerMap)

路由表本质是页面名称 ↔ 页面组件的映射表,在触发跳转时按名称动态加载页面模块,实现模块解耦(跳转时无需 navDestination 映射)。

配置步骤:

  1. src/main/resources/base/profile/ 新建 route_map.json
  2. src/main/module.json5 添加 "routerMap": "$profile:route_map"
  3. route_map.json 写入路由配置(名称/文件路径/构建函数/元数据)
  4. 目标页面中配置入口 Builder 函数,函数名与 buildFunction 一致
  5. 通过 pushPath 等接口跳转

route_map.json:

{
"routerMap": [
{
"name": "Home",
"pageSourceFile": "src/main/ets/view/Home.ets",
"buildFunction": "homeBuilder",
"data": { "description": "主页模块" }
},
{
"name": "login",
"pageSourceFile": "src/main/ets/view/Login.ets",
"buildFunction": "loginBuilder",
"data": { "description": "登录模块" }
}
]
}

目标页面(Home.ets):

@Builder
export function homeBuilder() {
Home()
}

@ComponentV2
struct Home {
build() {
NavDestination() {
Text('Home 组件效果')
}
.hideBackButton(true) // 取消默认返回箭头
}
}

💡 本课程综合示例用 @Builder PageMapnavDestination 方式映射(小工程够用);大型应用推荐 routerMap 实现模块解耦。

6.7 页面生命周期

页面(@Entry)具有完整生命周期:

@Entry
@ComponentV2
struct MyPage {
aboutToAppear(): void {
console.log('组件即将挂载')
}

aboutToDisappear(): void {
console.log('组件即将销毁')
}

onPageShow(): void { // 页面显示(从其他页面返回时也触发)
console.log('页面显示')
}

onPageHide(): void { // 页面隐藏
console.log('页面隐藏')
}

onBackPress(): boolean { // 返回键拦截
console.log('拦截返回')
return true // true=拦截,false=不拦截
}

build() {
Text('My Page')
}
}
回调时机
aboutToAppear首次渲染前
onPageShow页面可见时
onPageHide页面不可见时
onBackPress按返回键时
aboutToDisappear页面销毁前

6.8 综合示例

对应 code/Chapter6Index.ets,完整演示三种跳转:

@Entry
@ComponentV2
struct Index {
private navPathStack: NavPathStack = new NavPathStack()

build() {
Navigation(this.navPathStack) {
Column({ space: 20 }) {
Text('Chapter 6: Page Router')
.fontSize(24)
.fontWeight(FontWeight.Bold)

Column({ space: 12 }) {
Button('Push 无参数跳转')
.width('100%')
.onClick(() => {
this.navPathStack.pushPath({ name: 'DetailPage' })
})

Button('Push 带参数跳转')
.width('100%')
.onClick(() => {
this.navPathStack.pushPath({
name: 'DetailPage',
param: { title: '来自首页的消息', id: 42 }
})
})

Button('Replace 替换跳转')
.width('100%')
.onClick(() => {
this.navPathStack.replacePath({
name: 'DetailPage',
param: { title: 'Replace 模式', id: 99 }
})
})
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(12)
}
.width('100%')
.height('100%')
.padding(16)
}
.title('首页')
.mode(NavigationMode.Stack)
.navDestination(this.PageMap)
}

@Builder
PageMap(name: string, param: object) {
if (name === 'DetailPage') {
DetailPage({
title: (param as ParamType).title,
id: (param as ParamType).id,
navPathStack: this.navPathStack
})
}
}
}

interface ParamType {
title: string
id: number
}

@ComponentV2
struct DetailPage {
@Param title: string = ''
@Param id: number = 0
@Param navPathStack: NavPathStack = new NavPathStack()

build() {
Column({ space: 16 }) {
Text('详情页')
.fontSize(24)
.fontWeight(FontWeight.Bold)

Column({ space: 8 }) {
Text(`标题: ${this.title}`)
.fontSize(18)
Text(`ID: ${this.id}`)
.fontSize(16)
.fontColor('#007DFF')
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(12)

Button('返回上一页')
.width('100%')
.onClick(() => {
this.navPathStack.pop()
})
}
.width('100%')
.height('100%')
.padding(16)
.backgroundColor('#f5f5f5')
}
}

运行效果:

  • 首页三个按钮分别触发 Push 无参/带参/Replace 跳转
  • 详情页显示接收到的 titleid 参数
  • 点击"返回上一页"通过 pop() 回到首页
  • Replace 模式跳转后,首页从栈中移除(返回直接退出)

运行效果截图:

Chapter6 首页(选择跳转方式)

Push 无参数跳转详情页

Push 带参数跳转详情页

Replace 替换跳转详情页

6.9 分栏布局(平板)

Navigation 支持 Split 分栏,平板自动左右分栏,手机自动单页:

Navigation() {
Row() {
// 左侧列表
Column() {
List() {
ForEach(['床前明月光', '疑是地上霜', '举头望明月', '低头思故乡', '海内存知己'], (item: string) => {
ListItem() {
Text(item)
}
})
}
}
.width('30%')

// 右侧详情
Column() {
Text('详情内容')
}
.width('70%')
}
}
.title('分栏布局')
.mode(NavigationMode.Split)

💡 Split 模式是"一多"(一次开发多端部署)的重要实现,详见第十一章。

6.10 常见问题

Q:Navigation 和 Router 如何选择? 推荐 Navigation:功能完整(分栏/深链/转场/路由表)、官方主推。Router 已不推荐。

Q:跳转目标页面要不要包 NavDestination 推荐包。NavDestination 是 Navigation 框架下的"页面"容器,自动提供返回箭头、标题栏与转场动画。

Q:paramobject 类型,如何传递 class 对象? 类型断言转换:(param as UserModel)。复杂对象可整体传递。

Q:详情页如何拿到返回按钮? Navigation 自动提供导航栏返回箭头。自定义返回可通过 onBackPress 或显式按钮。

Q:如何拿到跳转页的返回数据? 两种方式:跳转时注册 pushPathByName(name, param, onPop) 回调;或目标页 NavDestinationonResult

Q:页面参数能传 @ObservedV2 对象吗? 可以,但注意跨页面对象修改的响应性,复杂场景推荐用 AppStorageV2 共享。

Q:navDestination 映射和 routerMap 路由表怎么选? 小工程用 navDestination 足够;大型应用模块多、要解耦时用 routerMap 动态加载。

6.11 本章小结

知识点说明
Navigation 五要素路由栈/容器/标题/模式/映射
显示模式Auto(默认自适应)/ Stack / Split
标题栏/菜单栏/工具栏titleMode / menus / toolbarConfiguration
Push 跳转pushPath / pushPathByName / pushDestination
返回数据onPop 回调、onResultgetParamByName
栈操作pop / replace / remove / move / clear 全集
路由拦截setInterception 的 willShow / didShow / modeChange
路由表routerMap 动态加载、模块解耦
生命周期onPageShow/onPageHide/onBackPress 等

6.12 课后练习

  1. 实现一个三页面应用:列表页 → 详情页 → 设置页
  2. 列表页点击项,携带对象参数跳转详情页
  3. 实现返回拦截(二次确认退出)与路由拦截(未登录跳登录页)
  4. menus 给首页加两个菜单项,用 toolbarConfiguration 加底部工具栏
  5. 尝试用 Split 模式实现平板分栏
  6. 用 routerMap 路由表重构 Chapter6,验证动态加载跳转
  7. 将练习整合进 Chapter6/Index.ets

6.13 参考资料

评论

加载中…
加载中...