HarmonyOS Next:使用 Navigation Component 构建持久化标签页
Source: Dev.to

介绍
如果您正在使用 HarmonyOS Next 和 ArkUI/ArkTS 构建应用程序,您会知道流畅的用户体验有多重要。开发者常遇到的一个常见挑战是,即使用户导航到不同的屏幕,仍然保持 tab bar visible。
在本文中我们将:
- 解释在使用路由器时标签页为何会消失。
- 展示一种结合 Tabs 和 Navigation 组件的稳健解决方案。
问题:导航时标签页消失
在 ArkUI 中,我们经常使用 Tabs 组件来创建诸如 Home、Profile、Settings 等板块。标签栏位于屏幕底部,供用户在主视图之间切换。
仅在切换标签时一切正常。问题出现在当你在标签内部点击按钮,使用 ArkTS 的 router 跳转到新页面时。

为什么会出现这种情况?
Tabs 组件通常放在单个页面上。当你使用 router 跳转到另一页面时,会离开承载标签栏的原页面,导致标签栏消失。用户随之失去关键的导航元素,可能会感到困惑。
我们希望 Tabs 能够 无论用户在应用的某个章节深入到何种层级,都保持可见。
解决方案:将 Tabs 与 Navigation 结合
HarmonyOS Next 提供了一种更好的方法:将 Tabs 组件 一起 与 Navigation 组件使用。此组合可以让您:
- 将标签栏固定在底部。
- 为每个标签页提供独立的导航栈。
因此,您可以在标签页 内部 导航到详细页面,同时标签栏仍然显示在屏幕上。
为什么选择 Navigation 而不是 Router?
在深入代码之前,让我们先阐明为何在此场景下更倾向使用 Navigation。
“建议您在应用中使用 Component Navigation (Navigation),它提供了更强大的功能和自定义能力,作为路由框架。” – 官方 HarmonyOS 文档
- Router – 在 不同页面 之间切换。
- Navigation – 在 单个页面内部 处理 组件级路由。
由于持久化标签页模式需要保持在同一页面(即包含标签页的页面)上,Navigation 是合适的工具。它仅更改标签页的内容区域,保持标签栏不变,并且在复杂 UI 流程中提供更细粒度的控制。
构建持久化标签页 UI:逐步指南
步骤 1 – 设置主标签页结构
创建承载 Tabs 组件的入口组件。这将是应用的根组件。
// Index.ets (Main Tabs Component)
@Entry
@Component
export struct Index {
build() {
Tabs({ barPosition: BarPosition.End }) { // Tabs at the bottom
// Tab A – will use Navigation for its content
TabContent() {
NavigationExample() // Custom component for Tab A
}.tabBar('A') // Tab label
// Tab B – simple placeholder
TabContent() {
Column()
.width('100%')
.height('100%')
.backgroundColor('#007DFF')
}.tabBar('B')
// Tab C – simple placeholder
TabContent() {
Column()
.width('100%')
.height('100%')
.backgroundColor('#FFBF00')
}.tabBar('C')
// Tab D – simple placeholder
TabContent() {
Column()
.width('100%')
.height('100%')
.backgroundColor('#E67C92')
}.tabBar('D')
}.barBackgroundColor('#ffb9fdf4') // Tab bar background color
}
}
关键点
@Entry标记此组件为应用的入口点。Tabs({ barPosition: BarPosition.End })创建位于底部的标签栏。- 标签页 A 包含自定义的
NavigationExample组件,该组件将管理自己的导航栈。 - 标签页 B‑D 为演示用的简单彩色列。
步骤 2 – 在标签页内部实现导航
现在创建将放置在 标签页 A 中的组件。它将拥有自己的导航历史。
// NavigationExample.ets (Content for Tab A)
@Component
struct NavigationExample {
// Dummy data for a simple list
private arr: number[] = [1, 2, 3];
// Provide a NavPathStack so child pages can access navigation state
@Provide('pageInfos') pageInfos: NavPathStack = new NavPathStack();
// -------------------------------------------------
// Step 2.1 – Define the Navigation container
// -------------------------------------------------
build() {
Navigation({
// Optional: customize transition animations, etc.
}) {
// Page 1 – List view
Page() {
Column() {
ForEach(this.arr, (item) => {
Button(`Item ${item}`)
.onClick(() => {
// Push a new page onto the navigation stack
this.pageInfos.push({
name: `DetailPage${item}`,
params: { id: item }
});
});
})
}
}
// Page 2 – Detail view (generated dynamically)
Page({ name: (info) => info.name }) {
Column() {
Text(`Detail for item ${this.pageInfos.current?.params?.id}`)
Button('Back')
.onClick(() => this.pageInfos.pop());
}
}
}
}
}
说明
@Provide('pageInfos')创建一个 NavPathStack 实例,用于存储该标签页的导航历史。Navigation组件定义了 基于栈 的导航流程:- 页面 1 显示项目列表。点击项目会将 详情页面 推入栈中。
- 页面 2 显示所选项目的详情,并提供一个 返回 按钮来弹出栈。
由于导航发生在 NavigationExample 组件 内部,外部的 Tabs 组件不会被隐藏。
回顾
| ✅ 我们实现的目标 | 📌 我们是如何做到的 |
|---|---|
| 持久的底部标签栏 | 在应用根目录 (Index.ets) 放置 Tabs |
| 每个标签页的独立导航 | 在标签页内部使用 Navigation (NavigationExample.ets) |
| 在不丢失标签页的情况下实现无缝深层导航 | 使用 NavPathStack 和 @Provide 管理导航状态 |
Source:
导航目标
// This @Builder function tells Navigation what content to show for each path.
@Builder
pageMap(name: string) {
NavDestination() {
if (name === '1') {
Text("NavDestinationContent " + name)
} else if (name === '2') { // Example detail page for '2'
Text("Detail Content for Item " + name)
} else if (name === '3') { // Example detail page for '3'
Text("Another Detail Content for Item " + name)
} else {
// Fallback for other items, showing their number
Text("NavDestination Content for Item " + name)
}
}
.title("NavDestinationTitle " + name) // Title for the navigation bar
.onShown(() => {
console.info("show NavDestinationTitle " + name)
})
.onHidden(() => {
console.info("hide NavDestinationTitle " + name)
})
}
// Step 2.2: Building the Navigation Container and its Initial Content
build() {
Column() {
// The Navigation component itself
Navigation(this.pageInfos) { // It uses our NavPathStack for managing history
List({ space: 12 }) { // Initial content: A list of items
ForEach(this.arr, (item: number) => {
ListItem() {
Button("Go to Child " + item)
.width('100%')
.onClick(() => {
// When button is clicked, push a new path onto the Navigation stack
this.pageInfos.pushPathByName(item.toString(), '')
})
}
})
}
}
.navDestination(this.pageMap) // Link Navigation to our pageMap for destination definitions
}
.height('100%')
.width('100%')
}
分解 NavigationExample 的关键部分
-
@Provide('pageInfos') pageInfos: NavPathStack = new NavPathStack()–
这行代码至关重要。NavPathStack是Navigation用来在此标签页中管理屏幕历史的对象。@Provide确保Navigation能正确设置。 -
pageMap([@builder](https://dev.to/builder))–
可以把它看作内容映射。它告诉Navigation对每个特定的 “路径”(例如'2'或'3')应显示哪个 UI(NavDestination)。 -
Navigation(this.pageInfos) { … }–
这就是你的Navigation容器。其初始内容(大括号内的部分)是用户在此标签页首次看到的内容。 -
Button().onClick(…)–
点击按钮会调用this.pageInfos.pushPathByName(),向Navigation的堆栈中添加一个新的 “屏幕”(来自pageMap)。这样可以在不离开主标签页的情况下显示新内容。
输出
结果:无缝的用户体验
使用此设置时,当用户位于 Tab A 时,会看到一个列表。如果他们点击 “前往 Child 1”, 则会在屏幕顶部看到 Child 1 的详细内容,但 标签栏 (A, B, C, D) 仍会显示在底部。他们可以轻松在 Tab A 的历史记录中返回,或切换到 Tab B、C 或 D,而不会失去上下文。
本方法的优势
- 始终可见的标签页 – 您的应用主标签页始终显示在屏幕上,提供一致的导航体验。
- 独立导航 – 每个标签页可以拥有各自独立的历史栈,使复杂的应用更易于管理和理解。
- 更好的用户流程 – 用户可以在一个标签页内深入浏览内容,然后切换到另一个主区域而不会感到迷失。
- 代码整洁 – 通过为每个标签页分离导航逻辑,代码库变得更有条理且易于维护。
Conclusion
通过智能地组合 HarmonyOS Next 的 Tabs 和 Navigation 组件,您可以克服常见的标签栏消失问题。这种方法使得构建 UI 元素保持固定的应用 轻松且强大,大幅提升整体用户体验。
参考文献
作者:Muhammet Ali Ilgaz
