文章目录
-
-
- 前言
- [一、 从 Router 到 Navigation:架构的范式转移](#一、 从 Router 到 Navigation:架构的范式转移)
- [二、 核心大脑:NavPathStack 路由栈管理](#二、 核心大脑:NavPathStack 路由栈管理)
- [三、 页面构造:NavDestination 与路由表设计](#三、 页面构造:NavDestination 与路由表设计)
- [四、 界面定制:摆脱默认样式的束缚](#四、 界面定制:摆脱默认样式的束缚)
- [五、 总结与实战](#五、 总结与实战)
-
前言
在鸿蒙应用的开发历程中,页面跳转一直是大家最先接触的功能之一。很长一段时间里,Router 模块都是我们手中的标配武器,那句 router.pushUrl 相信每一位开发者都烂熟于心。但在构建大型应用,尤其是面对平板、折叠屏这些复杂设备时,老旧的 Router 逐渐显露出了疲态。它是一个页面级别的全局单例,难以处理分屏、弹窗嵌套路由以及模块化的动态加载。这就像是用一把瑞士军刀去砍伐整片森林,虽然能用,但效率极低且手感生涩。
在 HarmonyOS 6 的时代,官方明确推荐我们全面拥抱 Navigation 组件。这不仅仅是一个组件的更替,更是一次架构思维的升级。Navigation 不再是一个简单的 API 调用,它是一个容器,一个能够容纳完整路由栈、标题栏和工具栏的超级容器。它将路由的管理权从系统底层交还到了开发者手中,让我们能够像操作数组一样精准地控制页面的进出栈。
今天,我们就把那个陈旧的 Router 放在一边,深入探讨如何利用 Navigation V2 架构和 NavPathStack 构建一个现代化、健壮的应用导航体系。

一、 从 Router 到 Navigation:架构的范式转移
要理解 Navigation 的强大,我们先得明白它解决了什么痛点。传统的 Router 是基于 Page(页面)的,每一个页面都是一个独立的 Ability 或者窗口层级。当我们想要在一个弹窗里再做一套局部导航,或者在平板的左侧菜单里嵌入一个独立的路由栈时,Router 就束手无策了。
Navigation 组件的出现彻底改变了这一局面。它本质上是一个 UI 组件,这意味着它可以被放置在界面的任何位置。你可以把它放在根节点作为全屏导航,也可以把它放在一个 Dialog 内部,甚至可以嵌套使用。
在 API 20 中,Navigation 采用了 组件级路由 的概念。每一个"页面"不再是 @Entry 修饰的独立文件,而是被 NavDestination 包裹的自定义组件。这种设计让页面变得极其轻量,页面的切换本质上就是组件的挂载与卸载,性能得到了巨大的提升。更重要的是,它配合 NavPathStack 实现了路由栈的可编程化,我们终于可以像操作数据一样去操作界面了。
二、 核心大脑:NavPathStack 路由栈管理
如果说 Navigation 是躯壳,那么 NavPathStack 就是它的灵魂。在 V2 版本中,我们不再直接调用组件的方法来跳转,而是创建一个 NavPathStack 的实例,并将其绑定到 Navigation 组件的 pathStack 属性上。这个栈对象就是我们操控界面的遥控器。
你需要实现一个复杂的登录流程:用户点击购买 -> 跳转登录 -> 跳转注册 -> 注册成功 -> 直接返回购买页 (跳过登录页)。在旧的 Router 模式下,你需要计算 delta 索引或者使用 replace 模式小心翼翼地堆叠。而在 NavPathStack 中,就方便多了。你可以随时调用 popToName 直接回到指定的路由锚点,或者操作栈数组,精准地移除中间的某几个页面。
数据的传递也变得优雅。当我们调用 pushPath 时,可以直接传入一个 param 对象。而在目标页面中,我们不需要再写繁琐的 router.getParams(),而是直接在 NavDestination 的 onShown 生命周期或者组件初始化时,从栈中获取参数。这种参数传递是类型安全的,且完全受控。此外,NavPathStack 还提供了强大的拦截器机制(Interception),让我们可以在路由跳转发生前进行鉴权拦截,比如用户未登录时直接重定向到登录页,这一切都在路由层面被优雅地拦截处理了。
三、 页面构造:NavDestination 与路由表设计
在 Navigation 架构下,我们的一级页面(根页面)通常直接写在 Navigation 的闭包里,而二级、三级页面则通过 NavDestination 来定义。这里有一个关键的概念转变:我们需要构建一个 路由映射表。
我们不再是通过文件路径去跳转,而是通过 路由名称(Name) 。我们需要在 Navigation 组件中配置 navDestination 属性,它接收一个 @Builder 构建函数。当 NavPathStack 请求跳转到 "DetailPage" 时,这个构建函数就会被触发,我们需要在这个函数里根据传入的 name 返回对应的 NavDestination 包裹的组件。
这种设计模式天然支持模块化开发。我们可以把不同模块的路由表分散在各自的 HAR 包中,最后在主工程中进行聚合。每个 NavDestination 都是一个独立的沙箱,它拥有自己的标题栏、菜单栏和生命周期(onShown, onHidden)。这对于开发者来说非常友好,我们可以在 onWillAppear 中发起网络请求,在 onWillDisappear 中保存草稿,页面的生命周期完全掌握在自己手中。
四、 界面定制:摆脱默认样式的束缚
Navigation 自带了标准的标题栏(TitleBar)和工具栏(ToolBar),这在快速开发原型时非常方便。但在实际的商业项目中,设计师往往会给出天马行空的顶部导航设计,比如透明渐变背景、复杂的搜索框或者异形的返回按钮。
很多初学者会困惑:我是该用系统自带的,还是自己画?我的建议是按需定制 。Navigation 和 NavDestination 都提供了 title 、menus 和 toolBar 属性。如果设计风格符合系统规范,直接传入资源配置即可,系统会自动适配深色模式和折叠屏布局。但如果设计差异巨大,我们可以通过 .hideTitleBar(true) 彻底隐藏系统标题栏,然后在内容区域(Content)的顶部放置我们自定义的 NavBar 组件。
这里有一个细节需要注意,当我们隐藏了系统标题栏后,原本的滑动返回手势依然有效,但左上角的返回箭头没了。我们需要自己实现一个返回按钮,并调用 this.pageStack.pop() 来手动触发返回。这种灵活性让我们既能享受系统手势的便利,又能完全掌控视觉呈现。
import { promptAction } from '@kit.ArkUI';
// 1. 定义路由参数模型
interface ContactParams {
id: string;
name: string;
phone: string;
}
@Entry
@Component
struct NavigationBestPracticePage {
// 核心修正:使用 @Provide 而不是 @State
// 这样后代组件 (DetailPage) 才能通过 @Consume 直接获取该对象
@Provide('pageStack') pageStack: NavPathStack = new NavPathStack();
// 模拟的首页数据
@State contacts: ContactParams[] = [
{ id: '1', name: '张三', phone: '13800138000' },
{ id: '2', name: '李四', phone: '13900139000' },
{ id: '3', name: '王五', phone: '15000150000' }
];
// -------------------------------------------------------
// 路由工厂:根据路由名称动态构建页面
// -------------------------------------------------------
@Builder
PagesMap(name: string, param: Object) {
if (name === 'DetailPage') {
// 跳转到详情页
DetailPage({
contactInfo: param as ContactParams
})
} else if (name === 'EditPage') {
// 跳转到编辑页
EditPage({
contactInfo: param as ContactParams
})
}
}
build() {
// 根容器:Navigation
Navigation(this.pageStack) {
// 首页内容区域
Column() {
Text('通讯录 (V2)')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ top: 20, bottom: 20 })
.width('100%')
.padding({ left: 16 })
List() {
ForEach(this.contacts, (item: ContactParams) => {
ListItem() {
Row() {
// 这里使用系统图标模拟头像,实际请替换为 app.media.xxx
Image($r('app.media.startIcon'))
.width(40)
.height(40)
.borderRadius(20)
.margin({ right: 12 })
.backgroundColor('#E0E0E0') // 兜底背景色
Column() {
Text(item.name).fontSize(16).fontWeight(FontWeight.Medium)
Text(item.phone).fontSize(14).fontColor('#999')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
// 跳转按钮
Button('查看')
.fontSize(12)
.height(28)
.onClick(() => {
// 核心动作:压栈跳转
this.pageStack.pushPathByName('DetailPage', item, true);
})
}
.width('100%')
.padding(12)
.backgroundColor(Color.White)
.borderRadius(12)
.margin({ bottom: 8 })
}
})
}
.padding(16)
.layoutWeight(1)
}
.width('100%')
.height('100%')
.backgroundColor('#F1F3F5')
}
// 绑定路由映射构建器
.navDestination(this.PagesMap)
// 首页的标题模式
.titleMode(NavigationTitleMode.Mini)
.hideTitleBar(true) // 首页隐藏系统标题栏,使用自定义内容
.mode(NavigationMode.Stack) // 强制使用堆叠模式
}
}
// -------------------------------------------------------
// 子页面 1:详情页 (使用 @Consume 获取 Stack)
// -------------------------------------------------------
@Component
struct DetailPage {
// 接收参数
contactInfo: ContactParams = { id: '', name: '', phone: '' };
// 获取当前的路由栈 (对应父组件的 @Provide)
@Consume('pageStack') pageStack: NavPathStack;
build() {
NavDestination() {
Column({ space: 20 }) {
Image($r('app.media.startIcon'))
.width(80)
.height(80)
.borderRadius(40)
.margin({ top: 40 })
.backgroundColor('#E0E0E0')
Text(this.contactInfo.name)
.fontSize(24)
.fontWeight(FontWeight.Bold)
Text(this.contactInfo.phone)
.fontSize(18)
.fontColor('#666')
Button('编辑资料')
.width('80%')
.margin({ top: 40 })
.onClick(() => {
// 继续压栈,跳转到编辑页
this.pageStack.pushPathByName('EditPage', this.contactInfo);
})
}
.width('100%')
.height('100%')
}
.title('联系人详情') // 设置系统标题
}
}
// -------------------------------------------------------
// 子页面 2:编辑页 (使用 onReady 获取 Stack)
// -------------------------------------------------------
@Component
struct EditPage {
@State contactInfo: ContactParams = { id: '', name: '', phone: '' };
@State newName: string = '';
// 独立维护 Stack 引用,不依赖 @Consume,解耦性更好
private stack: NavPathStack | null = null;
aboutToAppear(): void {
this.newName = this.contactInfo.name;
}
build() {
NavDestination() {
Column({ space: 16 }) {
Text('修改姓名:')
.fontSize(14)
.fontColor('#666')
.width('90%')
.margin({ top: 20 })
TextInput({ text: $$this.newName, placeholder: '请输入新名字' })
.backgroundColor(Color.White)
.width('90%')
.height(50)
.borderRadius(10)
Button('保存并返回')
.width('90%')
.margin({ top: 20 })
.onClick(() => {
// 模拟保存操作
if (this.stack) {
this.stack.pop(true); // 出栈
promptAction.showToast({ message: `保存成功: ${this.newName}` });
}
})
}
.width('100%')
.height('100%')
.backgroundColor('#F1F3F5')
}
.title('编辑')
.onReady((context: NavDestinationContext) => {
// 最佳实践:在 onReady 中获取当前页面的 stack
// 这种方式不需要父组件必须使用 @Provide,适用性更广
this.stack = context.pathStack;
})
}
}

五、 总结与实战
Navigation 组件配合 NavPathStack,标志着鸿蒙应用开发进入了 单窗口多组件(Single Window, Multi-Component) 的架构时代。它解决了 Router 时代的诸多顽疾,提供了更灵活的嵌套能力、更强大的路由栈控制以及更轻量的页面切换开销。
对于任何一个立志于构建专业级鸿蒙应用的开发者来说,尽早重构代码,迁移到 Navigation 架构,是提升应用质量的关键一步。