第六章:页面路由
本章配套代码:
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)优势:
- 显式区分标题栏、内容区、工具栏,管理与动效更灵活
- 显式提供路由容器概念,支持在全模态、半模态、弹窗中显示
- 基于通用
@Builder,由开发者决定页面别名 ↔ 页面 UI 的映射关系 - 整合 UX 设计与一次开发多端部署,默认提供统一标题、页面切换与单双栏自适应
- 页面切换动效基于组件属性动效实现,过渡更丰富
- 开放页面栈对象,可继承做二次管理
⚠️
@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 // 背景模糊
}
)
💡
barStyle:STANDARD上下布局(默认)、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}`)
}
}
}
💡
param是object类型,映射时需用类型断言转换为具体类型:(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 映射)。
配置步骤:
- 在
src/main/resources/base/profile/新建route_map.json - 在
src/main/module.json5添加"routerMap": "$profile:route_map" - 在
route_map.json写入路由配置(名称/文件路径/构建函数/元数据) - 目标页面中配置入口 Builder 函数,函数名与
buildFunction一致 - 通过
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 PageMap的navDestination方式映射(小工程够用);大型应用推荐 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 | 页面销毁前 |