欢迎光临
我们一直在努力

HarmonyOS6踩坑记录之Navigation + Tabs 嵌套后路由栈全乱了?每个 Tab 独立 NavPathStack 才是正解

文章目录

      • 前言
    • 问题出在哪?
      • 共享路由栈的连锁反应
    • 解决方案:每个 Tab 独立 NavPathStack
      • 架构改造
      • 代码实现
      • 子页面里怎么跳转?
    • 返回键的处理
      • 拦截返回键,做最后一层保护
    • 踩坑记录
      • 坑 1:Tab 切换时 NavPathStack 的页面"丢失"
      • 坑 2:NavDestination 的 Builder 函数参数类型
      • 坑 3:onBackPress 的返回值容易搞混
      • 坑 4:路由表别写在 Navigation 的 build 里面
    • 写在最后

前言

App 上线第二周,用户反馈了两个让我血压飙升的路由 Bug。

一个是:在首页 Tab 里进了两个详情页,切到商城 Tab 再切回来,页面栈没了,直接回到了首页。另一个更离谱:在商城的二级页面按系统返回键,居然跳到了首页 Tab 的页面。 这两个问题折腾了我整整两天。记录一下排查思路和最终方案,希望帮你少走弯路。

问题出在哪?

先说说我的初始架构,估计很多人一开始都会这么写:

@Entry
@Component
struct MainPage {
@State currentIndex: number = 0
// 全局共享一个 NavPathStack
private pageStack: NavPathStack = new NavPathStack()

build() {
Navigation(this.pageStack) {
Tabs({ barPosition: BarPosition.End, index: this.currentIndex }) {
TabContent() { HomeTab() }.tabBar('首页')
TabContent() { ShopTab() }.tabBar('商城')
TabContent() { ProfileTab() }.tabBar('我的')
}
}
.navDestination(this.routeMap)
.hideTitleBar(true)
.mode(NavigationMode.Stack)
}
}

看起来挺合理对吧?一个 Navigation 管全局路由,里面套一个 Tabs 做底部导航。

但问题就出在那个全局共享的 NavPathStack 上。

共享路由栈的连锁反应

当所有 Tab 共用一个 NavPathStack 时,路由栈是这样的:

用户操作:
1. 在首页 Tab 点击商品 → push('ProductDetail')
2. 在首页继续点击评论 → push('CommentPage')
3. 切到商城 Tab
4. 切回首页 Tab

此时路由栈状态:
[首页, ProductDetail, CommentPage]

问题来了:Tab 切换时,系统可能重建 TabContent 的视图,
但 NavPathStack 里的页面已经"悬空"了——它们引用的组件实例可能已经不存在。

更严重的是返回键的问题。因为只有一个栈,商城 Tab 的二级页面和首页 Tab 的二级页面混在一起。用户按返回键时,NavPathStack.pop() 不管当前是哪个 Tab,它只管从栈顶弹页面。

说白了:一个栈管多个 Tab,注定会乱。

解决方案:每个 Tab 独立 NavPathStack

核心思路其实就一句话:每个 Tab 维护自己的路由栈,互不干扰。

架构改造

把架构从"一个 Navigation 套 Tabs"改成"Tabs 里每个 TabContent 套一个 Navigation":

旧架构(有问题):
Navigation(全局 pageStack)
└── Tabs
├── TabContent → HomeTab
├── TabContent → ShopTab
└── TabContent → ProfileTab

新架构(正确):
Tabs
├── TabContent
│ └── Navigation(homeStack)→ HomeTab 的页面栈
├── TabContent
│ └── Navigation(shopStack)→ ShopTab 的页面栈
└── TabContent
└── Navigation(profileStack)→ ProfileTab 的页面栈

代码实现

先定义每个 Tab 的路由栈和路由配置:

// 每个 Tab 独立的路由栈
const homeStack: NavPathStack = new NavPathStack()
const shopStack: NavPathStack = new NavPathStack()
const profileStack: NavPathStack = new NavPathStack()

// 首页 Tab 的路由表
const homeRouteMap: Record<string, WrappedBuilder<[object]>> = {
'ProductDetail': wrapBuilder(buildProductDetail),
'CommentPage': wrapBuilder(buildCommentPage),
}

// 商城 Tab 的路由表
const shopRouteMap: Record<string, WrappedBuilder<[object]>> = {
'ShopDetail': wrapBuilder(buildShopDetail),
'OrderPage': wrapBuilder(buildOrderPage),
}

A structural diagram in sketch-notes style showing

然后在主页面里把 Navigation 下沉到每个 TabContent 内部:

@Entry
@Component
struct MainPage {
@State currentIndex: number = 0

build() {
Tabs({ barPosition: BarPosition.End, index: this.currentIndex }) {
TabContent() {
Navigation(homeStack) {
HomeTab()
}
.navDestination(homeRouteMap)
.hideTitleBar(true)
.mode(NavigationMode.Stack)
}
.tabBar('首页')

TabContent() {
Navigation(shopStack) {
ShopTab()
}
.navDestination(shopRouteMap)
.hideTitleBar(true)
.mode(NavigationMode.Stack)
}
.tabBar('商城')

TabContent() {
Navigation(profileStack) {
ProfileTab()
}
.navDestination(profileRouteMap)
.hideTitleBar(true)
.mode(NavigationMode.Stack)
}
.tabBar('我的')
}
}
}

改完之后,每个 Tab 的路由栈是完全独立的。在首页 Tab 里 push 的页面,不会影响商城 Tab 的栈;切 Tab 的时候,各 Tab 的页面栈状态都会被保持。

子页面里怎么跳转?

在子页面中使用对应的 NavPathStack 来跳转。推荐通过 AppStorage 或者参数传递的方式让子页面拿到对应的栈:

// HomeTab 内部
@Component
struct HomeTab {
build() {
Column() {
Text('商品列表')
List() {
ForEach(productList, (item: Product) => {
ListItem() {
ProductCard({ product: item })
.onClick(() => {
// 用首页专属的路由栈跳转
homeStack.pushPathByName('ProductDetail', { id: item.id })
})
}
})
}
}
}
}

// ProductDetail 页面
@Builder
function buildProductDetail(params: object) {
NavDestination() {
Column() {
Text('商品详情页')
Button('查看评论')
.onClick(() => {
// 继续在首页路由栈里 push
homeStack.pushPathByName('CommentPage', { productId: params.id })
})
}
}
}

这样路由跳转就走各自的栈了,互不干扰。

返回键的处理

解决了页面栈隔离,还有返回键的问题需要处理。

默认情况下,系统返回键会触发当前焦点所在 Navigation 的 pop() 操作。在我们的架构下,每个 TabContent 里都有自己的 Navigation,所以返回键的行为是:在当前 Tab 的路由栈里 pop。

这已经解决了"在商城按返回却跳到首页页面"的问题。但如果当前 Tab 的路由栈已经空了(只剩首页),再按返回键应该退出应用,而不是什么都不做。

拦截返回键,做最后一层保护

@Entry
@Component
struct MainPage {
@State currentIndex: number = 0

// 获取当前 Tab 对应的路由栈
private getCurrentStack(): NavPathStack {
switch (this.currentIndex) {
case 0: return homeStack
case 1: return shopStack
case 2: return profileStack
default: return homeStack
}
}

build() {
Tabs({ barPosition: BarPosition.End, index: this.currentIndex })
.onChange((index: number) => {
this.currentIndex = index
})
// … TabContent 定义同上
}
.onBackPress(() => {
const stack = this.getCurrentStack()
// 当前 Tab 的路由栈还有页面,pop 掉
if (stack.size() > 0) {
stack.pop()
return true // 拦截,不交给系统处理
}
// 当前 Tab 已经在根页面了
// 如果不在首页 Tab,先切回首页
if (this.currentIndex !== 0) {
this.currentIndex = 0
return true
}
// 已经在首页根页面了,返回 false 让系统处理(退出应用)
return false
})
}

这段逻辑处理了三种情况:

  • 当前 Tab 有二级页面 → pop 掉当前页面,留在当前 Tab
  • 当前 Tab 已经在根页面,但不在首页 → 切回首页 Tab
  • 已经在首页 Tab 的根页面 → 交给系统处理,正常退出

踩坑记录

坑 1:Tab 切换时 NavPathStack 的页面"丢失"

一开始我用的是 @State 来管理 NavPathStack,结果发现 Tab 切换时,被切走的 TabContent 可能触发组件重建,NavPathStack 里的页面引用就失效了。

解决方案:NavPathStack 不要用 @State 声明,用普通 private 或者 const 就行。它本身不需要触发 UI 刷新,页面的进出由 Navigation 组件自己管理。

坑 2:NavDestination 的 Builder 函数参数类型

路由表里的 Builder 函数签名是 WrappedBuilder<[object]>,接收的参数是 pushPathByName 第二个参数传进去的数据。我一开始定义成了具体类型,结果编译报错。

// 正确写法
@Builder
function buildProductDetail(params: object) {
NavDestination() {
// 通过 params 获取路由参数
Text(`商品ID: ${(params as Record<string, string>)['id']}`)
}
}

参数类型必须是 object,然后在内部自己做类型转换。

坑 3:onBackPress 的返回值容易搞混

onBackPress 返回 true 表示"我拦截了,系统你别管",返回 false 表示"我不处理,交给系统"。

我第一版代码写反了——当前 Tab 有页面时返回了 false,结果系统也执行了返回操作,页面直接被 pop 了两次。

坑 4:路由表别写在 Navigation 的 build 里面

路由表(routeMap)如果定义在组件的 build() 方法内部,每次组件刷新都会重新创建对象,可能导致 Navigation 重新注册路由。把路由表定义在组件外部或者作为 private 成员,避免不必要的重建。

写在最后

Navigation + Tabs 这个组合本身没毛病,问题出在路由栈的管理方式上。

核心原则就一条:每个 Tab 一个独立的 NavPathStack。别偷懒用全局共享,否则迟早要还债。 另外建议每个 Tab 的路由表也独立维护,这样模块化的好处很明显——路由配置分散到各自的文件里,不用在一个巨大的 routeMap 里找来找去。 如果你也在做类似的 Tab + Navigation 架构,希望这篇文章能帮你省点时间。有问题欢迎评论区交流。

赞(0)
未经允许不得转载:171主机测评 » HarmonyOS6踩坑记录之Navigation + Tabs 嵌套后路由栈全乱了?每个 Tab 独立 NavPathStack 才是正解
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址