页面路由导航:Router与Navigation组件的跳转传参(7)
前言:选择正确的导航方案
在鸿蒙 ArkTS 开发中,页面跳转与导航是应用骨架的核心。目前 ArkUI 提供了两套路由方案:
- Navigation 组件(推荐):基于组件化的路由容器,适用于绝大多数场景,特别是需要复杂交互、多端适配(一次开发,多端部署)的应用。
- Router 模块(不推荐):基于页面路径的跳转方式,功能较基础,页面栈有上限(32层),主要用于简单的页面跳转或兼容旧代码。
本文将重点讲解官方推荐的Navigation组件,并简要对比Router模块。
一、 Navigation 组件:组件级路由的新标准
Navigation是一个路由导航的根视图容器,它支持单栏(Stack)、分栏(Split)和自适应(Auto)三种显示模式,能够根据窗口大小自动切换布局,非常适合折叠屏和平板设备。
核心概念:
- NavPathStack:导航路径栈,用于管理页面的入栈(Push)、出栈(Pop)和替换(Replace)。
- NavDestination:子页面容器,必须嵌套在
Navigation组件中使用。
1. 基础路由配置与跳转
要实现Navigation路由,首先需要配置路由表,并在module.json5中注册。
步骤一:配置路由表 (router_map.json)
在src/main/resources/base/profile目录下创建router_map.json:
{ "routerMap": [ { "name": "pageOne", "pageSourceFile": "src/main/ets/pages/PageOne.ets", "buildFunction": "PageOneBuilder" }, { "name": "pageTwo", "pageSourceFile": "src/main/ets/pages/PageTwo.ets", "buildFunction": "PageTwoBuilder" } ] }注:buildFunction是页面组件对应的@Builder函数名称。
步骤二:在 module.json5 中注册
"module": { "routerMap": "$profile:router_map" }步骤三:主页面与跳转实现
// Index.ets (主页面) import { router } from '@kit.ArkUI'; @Entry @Component struct NavigationPage { // 创建导航路径栈 navStack: NavPathStack = new NavPathStack(); aboutToAppear() { // 将栈对象存入 AppStorage,方便子页面获取 AppStorage.setOrCreate<NavPathStack>('navStack', this.navStack); } build() { Navigation(this.navStack) { // 导航页内容(首页内容) Column({ space: 20 }) { Text('这是首页') .fontSize(30) Button('跳转到 PageOne') .onClick(() => { // 方式1:通过名称跳转,可携带参数 this.navStack.pushPath({ name: 'pageOne', param: '我是首页传来的参数' }); }) Button('跳转到 PageTwo (带回调)') .onClick(() => { // 方式2:带返回回调的跳转 this.navStack.pushPathByName('pageTwo', '参数2', (popInfo) => { console.info('PageTwo 返回了:' + JSON.stringify(popInfo.result)); }); }) } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) } .mode(NavigationMode.Stack) // 设置为单栏模式 .title('主标题') .hideTitleBar(false) } }步骤四:子页面接收参数与返回
// src/main/ets/pages/PageOne.ets // 1. Builder 函数:作为路由入口 // name 是路由名称(通常用不到),param 是传入的参数 @Builder export function PageOneBuilder(name: string, param: Object) { // 【修正点】:只传入 value,去掉不存在的 name 属性 // 注意:如果 param 可能是 undefined,建议做一下类型转换或判空 PageOne({ value: param as string }); } @Component export struct PageOne { navPathStack: NavPathStack = new NavPathStack(); // 定义接收参数的属性 @State value: string = ''; // 【新增】:在组件初始化时处理参数 aboutToAppear() { // 这里可以做一些额外的初始化逻辑 console.info('PageOne 已加载,参数为: ' + this.value); } build() { NavDestination() { Column({ space: 20 }) { Text(`接收到的参数: ${this.value}`) .fontSize(24) .fontWeight(FontWeight.Bold) Button('返回上一页') .onClick(() => { // 普通返回 this.navPathStack.pop(); }) Button('返回并带回数据') .backgroundColor('#007DFF') .fontColor(Color.White) .onClick(() => { // 带结果返回,触发上一页的回调 this.navPathStack.pop({ result: '我是PageOne带回的数据' }); }) } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) } .title('PageOne') // onReady 主要用于获取 Context 和 PathStack,不建议在这里做业务数据赋值 .onReady((ctx: NavDestinationContext) => { this.navPathStack = ctx.pathStack; }) } }// src/main/ets/pages/PageTwo.ets @Builder export function PageTwoBuilder(name: string, param: string) { PageTwo({ value: param }); } @Component export struct PageTwo { value: string = ''; @State showValue: string = ''; private navPathStack: NavPathStack = new NavPathStack(); build() { NavDestination() { Column({ space: 20 }) { Text(`PageTwo 收到: ${this.showValue}`) .fontSize(24) Button('返回并触发回调') .backgroundColor('#E65555') .onClick(() => { // 这里触发的 result 会回到 Index.ets 的 pushPathByName 回调中 this.navPathStack.pop({ result: 'PageTwo 任务完成' }); }) } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) } .title('PageTwo') .onReady((ctx: NavDestinationContext) => { this.navPathStack = ctx.pathStack; this.showValue = ctx.pathInfo?.param as string || '无参数'; }) } }二、 参数传递进阶:对象与数组的序列化陷阱
在使用Navigation或Router进行页面跳转传参时,如果传递的是复杂对象或数组,经常会遇到数据丢失或无法刷新的问题。
问题原因:
路由传参在底层会经历“序列化 + 反序列化”的过程。对于被@ObservedV2和@Trace装饰的类对象,序列化后属性名会被添加__ob_前缀,导致反序列化后失去观察能力,甚至属性名错乱。
解决方案:
- 基础类型/简单对象:直接传递,接收时注意类型断言。
- 复杂对象/数组:建议使用
JSON.stringify()序列化后传递,接收方再用JSON.parse()解析。
代码示例:
// Index.ets (主页面) import { router } from '@kit.ArkUI'; @Entry @Component struct NavigationPage { // 创建导航路径栈 navStack: NavPathStack = new NavPathStack(); aboutToAppear() { // 将栈对象存入 AppStorage,方便子页面获取 AppStorage.setOrCreate<NavPathStack>('navStack', this.navStack); } build() { Navigation(this.navStack) { // 导航页内容(首页内容) Column({ space: 20 }) { Text('这是首页') .fontSize(30) Button('跳转到 PageOne') .onClick(() => { // 方式1:通过名称跳转,可携带参数 this.navStack.pushPath({ name: 'pageOne', param: '我是首页传来的参数' }); }) Button('跳转到 PageTwo (带回调)') .onClick(() => { // 方式2:带返回回调的跳转 this.navStack.pushPathByName('pageTwo', '参数2', (popInfo) => { console.info('PageTwo 返回了:' + JSON.stringify(popInfo.result)); }); }) Button('跳转到文章页 (传数组)') .onClick(() => { // 发送方逻辑:准备数组并序列化 let titles: string[] = ["文章1", "文章2", "文章3"]; // 将数组序列化为字符串传递 this.navStack.pushPathByName("articlePage", JSON.stringify(titles)); }) } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) } .mode(NavigationMode.Stack) // 设置为单栏模式 .title('主标题') .hideTitleBar(false) } }// src/main/ets/pages/ArticlePage.ets // 1. 路由入口 Builder(必须加上,否则路由找不到页面) @Builder export function ArticlePageBuilder(name: string, param: Object) { // 将路由传来的参数传给组件的 initialData 属性 ArticlePage({ initialData: param as string }); } @Component export struct ArticlePage { // 接收路由传来的 JSON 字符串 initialData: string = ''; @State buttonTitles: string[] = []; private navPathStack: NavPathStack = new NavPathStack(); // 2. 在 aboutToAppear 中解析参数(比 onReady 更安全规范) aboutToAppear() { if (this.initialData) { try { this.buttonTitles = JSON.parse(this.initialData) as string[]; } catch (e) { console.error('参数解析失败:', e); } } } build() { NavDestination() { Column() { Text('接收到的文章列表:').fontSize(24).margin({ bottom: 20 }) ForEach(this.buttonTitles, (title: string) => { Button(title).margin(10) }) } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) } .title('文章页面') .onReady((ctx: NavDestinationContext) => { this.navPathStack = ctx.pathStack; }) } }{ "routerMap": [ { "name": "pageOne", "pageSourceFile": "src/main/ets/pages/PageOne.ets", "buildFunction": "PageOneBuilder" }, { "name": "pageTwo", "pageSourceFile": "src/main/ets/pages/PageTwo.ets", "buildFunction": "PageTwoBuilder" }, { "name": "articlePage", "pageSourceFile": "src/main/ets/pages/ArticlePage.ets", "buildFunction": "ArticlePageBuilder" } ] }三、 Router 模块:传统路由方式
Router模块位于@kit.ArkUI中,适用于简单的页面跳转。虽然官方不再推荐作为首选,但在某些轻量级场景下依然有用。
1. 基础跳转
import { router } from '@kit.ArkUI'; // 跳转到指定页面 router.pushUrl({ url: 'pages/Detail', params: { id: 123 } // 传递参数 }, router.RouterMode.Standard);2. 接收参数
import { router } from '@kit.ArkUI'; @Entry @Component struct Detail { @State id: number = 0; aboutToAppear() { // 获取路由参数 const params = router.getParams() as Record<string, number>; if (params) { this.id = params['id']; } } }3. 页面返回
// 返回上一页 router.back(); // 返回指定页面 router.back({ url: 'pages/Index' });四、 总结:Navigation 与 Router 对比
| 特性 | Navigation (推荐) | Router (不推荐) |
|---|---|---|
| 路由容器 | 提供Navigation容器组件,支持标题栏、工具栏联动 | 无容器概念,基于页面栈管理 |
| 页面栈限制 | 无上限,支持无限跳转 | 最大 32 层,需手动清理 |
| 转场动画 | 支持自定义转场和共享元素动画 | 仅支持简单自定义动画 |
| 路由拦截 | 支持setInterception设置拦截 | 不支持 |
| 适用场景 | 复杂应用、多端适配、沉浸式页面 | 简单跳转、旧项目维护 |
最佳实践建议:
- 新项目:请统一使用
Navigation组件,利用NavPathStack管理页面栈。 - 参数传递:传递复杂数据时,务必使用
JSON.stringify和JSON.parse进行序列化与反序列化,避免引用丢失或属性名错乱。 - 状态管理:结合
AppStorage或LocalStorage可以在不同页面间共享状态,减少参数传递的复杂度。
五、 架构进阶:动态路由与模块解耦
在大型项目中,静态路由表会导致模块间强耦合。建议补充Navigation 的动态路由方案,实现业务模块的彻底解耦:
- 系统路由表(推荐):从 API version 12 开始,Navigation 支持在业务模块(HSP/HAR)中独立配置
router_map.json。触发跳转时,系统会自动完成路由模块的动态加载与组件构建,无需主工程硬编码依赖。 - 自定义路由表:开发者可封装统一的路由管理模块,将
NavPathStack注入其中。各业务页面通过@Builder和WrappedBuilder封装后注册到路由模块,配合动态import()实现按需加载,防止首屏加载大量代码导致卡顿。
六、 高级交互:路由拦截与沉浸式体验
Navigation 提供了 Router 无法比拟的底层控制力,可补充以下高阶 API 实战:
- 路由拦截(setInterception):支持在页面跳转前进行全局鉴权或状态检查。例如,在跳转核心业务页前拦截判断用户是否登录,未登录则重定向至登录页,登录成功后自动恢复原跳转。
- 沉浸式与自定义属性:支持通过
backgroundBlurStyle设置页面背景模糊,或通过.hideTitleBar(true)隐藏默认标题栏,配合自定义CustomNavigationBar实现完全自定义的顶部导航与沉浸式全屏体验。 - 共享元素转场:Navigation 天然支持共享元素动画,可实现列表项到详情页的平滑过渡,大幅提升应用的视觉连贯性(Router 不支持此特性)。
七、 性能优化:参数传递与动态加载
针对原文提到的“序列化陷阱”,可补充底层原理与性能优化建议:
- 引用传递 vs 深拷贝:明确指出 Navigation 在传递参数时底层采用引用传递,而 Router 采用深拷贝。因此,对于超大对象或复杂数组,Navigation 不仅避免了 JSON 序列化带来的性能损耗,还能保持对象引用,配合
@Observed实现跨页面的响应式更新。 - 组件动态加载:Router 使用
@Entry修饰页面,模块加载时会生成全量页面;而 Navigation 可配合动态加载机制,仅在pushPath触发时才实例化目标组件,显著降低内存占用。
八、 状态管理与生命周期最佳实践
补充在复杂路由场景下的状态管理范式:
- NavPathStack 的共享范式:除了
AppStorage,更推荐使用@Provide/@Consume在组件树内共享NavPathStack,这能确保路由状态与 UI 树的强绑定,避免内存泄漏并保证状态更新的实时性。 - 精准的生命周期监听:Navigation 提供了比 Router 更细粒度的页面生命周期。建议开发者利用
onShown/onHidden处理页面可见性相关的业务逻辑(如暂停/恢复视频播放、刷新数据),利用onReady获取NavDestinationContext进行安全的参数解析与栈对象绑定。 - 全局路由监听:通过
uiObserver.on('navDestinationUpdate')注册全局监听,可实现统一的路由埋点、页面停留时长统计等无侵入式监控。
