鸿蒙开发实战:router 与 navigation 组件的核心差异及选型指南
在鸿蒙应用开发过程中,处理页面跳转和导航结构是绕不开的核心环节。其中,router(路由)和 navigation(导航容器)是最常用的两个工具,但很多刚接触鸿蒙开发的开发者容易混淆二者的用法。其实从设计初衷来看,它们就有着明确的分工:router 更像是 “页面间的传令兵”,专注于跳转逻辑和数据传递;而 navigation 则是 “导航系统的总控制台”,不仅能实现跳转,还自带一套完整的导航 UI 体系。下面结合实际开发经验,从定位、功能、代码实现到场景选择,全方位解析二者的区别。
一、本质定位:各司其职的导航工具
刚开始用这两个组件时,我曾尝试用 router 实现底部标签切换,结果不仅要自己写标签栏样式,还要处理切换时的页面状态保存,耗时又容易出问题。后来才明白,二者的设计理念完全不同,根本不是替代关系。
| 对比维度 | router(路由) | navigation(导航容器) |
|---|---|---|
| 核心角色 | 页面跳转的 “逻辑执行者” | 导航体验的 “完整解决方案” |
| 设计目标 | 轻量灵活,专注跳转逻辑 | 标准化导航,降低 UI 开发成本 |
| 与 UI 关系 | 无依赖,纯逻辑层工具 | 强绑定,需作为容器包裹页面内容 |
简单来说,如果你只需要 “从 A 页面跳到 B 页面”,用 router 就够了;但如果需要 “带标题栏、返回按钮、底部标签的完整导航界面”,navigation 才是更合适的选择。
二、功能特性:从跳转逻辑到 UI 体验的差异
1. 页面跳转与返回机制
router 的跳转逻辑很纯粹,通过pushUrl和back两个核心方法就能实现页面的入栈和出栈。比如从首页跳转到详情页,调用router.pushUrl后,页面会被加入路由栈,返回时用router.back弹出栈顶页面即可。但这里要注意,router 不会帮你做任何 UI 层面的东西,返回按钮需要自己用 Button 组件实现,标题栏也得手动用 Row 和 Text 拼接。
而 navigation 自带了一套完整的导航交互。只要把页面内容放进 navigation 容器里,它就会自动生成顶部导航栏,左侧默认带返回按钮,点击就能触发back操作,完全不用手动开发。跳转用pushPath方法,但参数传递会自动关联当前容器的上下文,在复杂页面结构中更不容易出问题。
2. 导航 UI 的实现成本
这是二者最直观的区别。用 router 开发时,所有导航相关的 UI 都要从零开始做。比如要做一个顶部标题栏,得用 Row 组件设置宽度 100%,加 padding,设置背景色,再放 Text 组件当标题;底部标签栏则需要用 Tabs 组件配合 TabContent,还要自己处理选中状态的样式。
navigation 则把这些常用导航 UI 都封装好了。想要顶部导航栏,只需要在初始化时传入title参数;也有menus等功能可以直接使用,省去了大量重复的 UI 代码。
3. 参数传递的实际应用
在参数传递方面,二者的基础用法相似,都支持通过pushUrl的params属性传参,也能通过AppStorage实现跨页面数据共享。但在实际开发中,navigation 的参数传递更贴合容器内的页面逻辑。
比如用 navigation 实现的多标签页,每个标签页内的页面跳转,参数会自动限定在当前标签的路由栈中,不会影响其他标签页的状态;而 router 的参数传递是全局的,如果不注意路由栈的管理,在多模块跳转时容易出现参数混乱的问题。
4. 适用场景的实际划分
根据我多次项目开发的经验,二者的适用场景可以清晰地划分:
router 的适用场景:
-
简单的跨页面跳转,比如设置页里 “关于我们” 的跳转
-
需要高度自定义导航 UI 的场景,比如带搜索框的特殊标题栏
-
跨模块的页面跳转,比如从商品列表模块跳到支付模块
navigation 的适用场景:
-
标准的多标签页应用,比如电商 APP 的 “首页 - 分类 - 购物车 - 我的”
-
需要统一导航风格的页面集群,比如新闻 APP 的文章列表和详情页
-
后台管理系统的侧边导航 + 顶部标题栏组合界面
三、代码实战:两种组件的实际开发案例
场景 1:商品列表跳转到详情页
用 router 实现跳转
这里我做了一个简单的商品列表页,点击商品卡片跳转到详情页,并传递商品 ID 和名称参数。
商品列表页(Pages/GoodsList.ets)
import router from '@ohos.router';
interface GoodItem {
id: number
name: string
price: number
}
@Entry
@Component
struct GoodsListPage {
// 模拟商品数据
@State goodsList: GoodItem[] = [
{ id: 101, name: "鸿蒙开发实战手册", price: 59 },
{ id: 102, name: "ArkUI组件大全", price: 49 }
];
build() {
Column() {
// 手动实现顶部导航栏
Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
Text("商品列表")
.fontSize(22)
.fontWeight(FontWeight.Bold)
}
.width('100%')
.padding({ left: 16, top: 12, bottom: 12 })
.backgroundColor('#ffffff')
.shadow({ radius: 2, color: '#eeeeee' })
// 商品列表
List({ space: 10 }) {
ForEach(this.goodsList, (item: GoodItem) => {
ListItem() {
Row({ space: 15 }) {
Column({ space: 5 }) {
Text(item.name)
.fontSize(16)
Text(`¥${item.price}`)
.fontSize(14)
.fontColor('#ff4400')
}
.flexGrow(1)
}
.width('100%')
.padding(12)
.backgroundColor('#ffffff')
.borderRadius(8)
.onClick(() => {
// 跳转详情页并传递参数
router.pushUrl({
url: 'pages/GoodsDetail',
params: { goodsId: item.id, goodsName: item.name }
});
})
}
})
}
.padding(12)
.flexGrow(1)
}
.backgroundColor('#f5f5f5')
.width('100%')
.height('100%')
}
}
商品详情页(Pages/GoodsDetail.ets)
import router from '@ohos.router';
interface RouterParams {
goodsId: number
goodsName: string
}
@Entry
@Component
struct GoodsDetailPage {
aboutToAppear(): void {
let data = router.getParams() as RouterParams
this.goodsId = data.goodsId
this.goodsName = data.goodsName
}
// 接收路由参数
@State goodsId: number = 0
@State goodsName: string = '未知商品';
build() {
Column() {
// 手动实现带返回按钮的标题栏
Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
Button('←')
.fontSize(18)
.backgroundColor('transparent')
.onClick(() => {
// 返回上一页
router.back();
})
Text(`${this.goodsName}详情`)
.fontSize(20)
.fontWeight(FontWeight.Bold)
}
.width('100%')
.padding({ left: 12, top: 12, bottom: 12 })
.backgroundColor('#ffffff')
.shadow({ radius: 2, color: '#eeeeee' })
// 详情内容
Column({ space: 20 }) {
Text(`商品ID:${this.goodsId}`)
.fontSize(16)
Text('商品描述:这里是商品的详细介绍内容...')
.fontSize(14)
.fontColor('#666666')
}
.padding(20)
.flexGrow(1)
}
.backgroundColor('#f5f5f5')
.width('100%')
.height('100%')
}
}
用 navigation 实现
同样的场景,用 navigation 可以省去导航栏的开发,直接聚焦业务内容。
商品列表页(Pages/GoodsList.ets)
export interface Good {
id: number
name: string
price: number
}
@Entry
@Component
struct GoodsListPage {
@State goodsList: Good[] = [
{ id: 101, name: "鸿蒙开发实战手册", price: 59 },
{ id: 102, name: "ArkUI组件大全", price: 49 }
];
@Provide pathStack: NavPathStack = new NavPathStack()
build() {
// 初始化navigation容器,设置标题
Navigation(this.pathStack) {
List({ space: 10 }) {
ForEach(this.goodsList, (item: Good) => {
ListItem() {
Row({ space: 15 }) {
Column({ space: 5 }) {
Text(item.name)
.fontSize(16)
Text(`¥${item.price}`)
.fontSize(14)
.fontColor('#ff4400')
}
.flexGrow(1)
}
.width('100%')
.padding(12)
.backgroundColor('#ffffff')
.borderRadius(8)
.onClick(() => {
// 跳转详情页
this.pathStack.pushPath({ name: 'Detail', param: item })
})
}
})
}
.padding(12)
}
// 设置导航栏样式
.backgroundColor('#f5f5f5')
}
}
商品详情页(Pages/GoodsDetail.ets)
import router from '@ohos.router';
// import { Good } from './Index'
interface Good {
id: number
name: string
}
@Builder
function DetailBuilder() {
NavDestination() {
GoodsDetailPage()
}
.hideTitleBar(true)
}
@Entry
@Component
struct GoodsDetailPage {
@Consume pathStack: NavPathStack
aboutToAppear(): void {
let data: Good[] = this.pathStack.getParamByName('Detail') as Good[]
this.goodsId = data[0].id
this.goodsName = data[0].name
}
// 接收路由参数
@State goodsId: number = 0
@State goodsName: string = '未知商品';
build() {
Column() {
// 手动实现带返回按钮的标题栏
Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
Button('←')
.fontSize(18)
.backgroundColor('transparent')
.onClick(() => {
// 返回上一页
this.pathStack.pop()
})
Text(`${this.goodsName}详情`)
.fontSize(20)
.fontWeight(FontWeight.Bold)
}
.width('100%')
.padding({ left: 12, top: 12, bottom: 12 })
.backgroundColor('#ffffff')
.shadow({ radius: 2, color: '#eeeeee' })
// 详情内容
Column({ space: 20 }) {
Text(`商品ID:${this.goodsId}`)
.fontSize(16)
Text('商品描述:这里是商品的详细介绍内容...')
.fontSize(14)
.fontColor('#666666')
}
.padding(20)
.flexGrow(1)
}
.backgroundColor('#f5f5f5')
.width('100%')
.height('100%')
}
}
场景 2:底部标签栏实现
用 navigation 实现(推荐方案)
底部标签栏是 APP 的常见结构,用 navigation 实现只需几行核心代码:
import { SymbolGlyphModifier } from '@kit.ArkUI';
@Entry
@Component
struct NavigationExample {
@Provide('navPathStack') navPathStack: NavPathStack = new NavPathStack();
@State menuItems: Array<NavigationMenuItem> = [
{
value: 'menuItem1',
icon: 'resources/base/media/ic_public_ok.svg' // 图标资源路径
},
{
value: 'menuItem2',
icon: 'resources/base/media/ic_public_ok.svg', // 图标资源路径
symbolIcon: new SymbolGlyphModifier($r('sys.symbol.ohos_folder_badge_plus')).fontColor([Color.Red, Color.Green])
.renderingStrategy(SymbolRenderingStrategy.MULTIPLE_COLOR),
},
{
value: 'menuItem3',
symbolIcon: new SymbolGlyphModifier($r('sys.symbol.ohos_lungs')),
},
];
@State toolItems: Array<ToolbarItem> = [
{
value: 'toolItem1',
icon: 'resources/base/media/ic_public_ok.svg', // 图标资源路径
symbolIcon: new SymbolGlyphModifier($r('sys.symbol.ohos_lungs')),
status: ToolbarItemStatus.ACTIVE,
activeSymbolIcon: new SymbolGlyphModifier($r('sys.symbol.ohos_folder_badge_plus')).fontColor([Color.Red,
Color.Green]).renderingStrategy(SymbolRenderingStrategy.MULTIPLE_COLOR),
action: () => {
}
},
{
value: 'toolItem2',
symbolIcon: new SymbolGlyphModifier($r('sys.symbol.ohos_star')),
status: ToolbarItemStatus.ACTIVE,
activeIcon: 'resources/base/media/ic_public_more.svg', // 图标资源路径
action: () => {
}
},
{
value: 'toolItem3',
symbolIcon: new SymbolGlyphModifier($r('sys.symbol.ohos_star')),
status: ToolbarItemStatus.ACTIVE,
activeSymbolIcon: new SymbolGlyphModifier($r('sys.symbol.ohos_lungs')),
action: () => {
}
}
];
@Builder
myRouter(name: string, param?: Object) {
if (name === 'NavigationMenu') {
NavigationMenu();
}
}
build() {
Navigation(this.navPathStack) {
Column() {
Button('跳转').onClick(() => {
this.navPathStack.pushPathByName('NavigationMenu', null);
})
}
}
.backButtonIcon(new SymbolGlyphModifier($r('sys.symbol.ohos_wifi')))
.titleMode(NavigationTitleMode.Mini)
.menus(this.menuItems)
.toolbarConfiguration(this.toolItems)
.title('一级页面')
.navDestination(this.myRouter)
}
}
@Component
export struct NavigationMenu {
@Consume('navPathStack') navPathStack: NavPathStack;
@State menuItems: Array<NavigationMenuItem> = [
{
value: 'menuItem1',
icon: 'resources/base/media/ic_public_ok.svg', // 图标资源路径
action: () => {
}
},
{
value: 'menuItem2',
symbolIcon: new SymbolGlyphModifier($r('sys.symbol.ohos_folder_badge_plus')).fontColor([Color.Red, Color.Green])
.renderingStrategy(SymbolRenderingStrategy.MULTIPLE_COLOR),
action: () => {
}
},
{
value: 'menuItem3',
symbolIcon: new SymbolGlyphModifier($r('sys.symbol.repeat_1')),
action: () => {
}
},
];
build() {
NavDestination() {
Row() {
Column() {
}
.width('100%')
}
.height('100%')
}
.hideTitleBar(false)
.title('NavDestination title')
.backgroundColor($r('sys.color.ohos_id_color_titlebar_sub_bg'))
.backButtonIcon(new SymbolGlyphModifier($r('sys.symbol.ohos_star')).fontColor([Color.Blue]))
.menus(this.menuItems)
}
}
用 router+Tabs 实现(替代方案)
如果非要用 router 实现,需要手动管理 Tabs 和路由的联动,代码量会多很多:
import { router } from '@kit.ArkUI';
@Entry
@Component
struct TabsExample {
@State fontColor: string = '#182431';
@State selectedFontColor: string = '#007DFF';
@State currentIndex: number = 0;
@State selectedIndex: number = 0;
private controller: TabsController = new TabsController();
@Builder
tabBuilder(index: number, name: string) {
Column() {
Text(name)
.fontColor(this.selectedIndex === index ? this.selectedFontColor : this.fontColor)
.fontSize(16)
.fontWeight(this.selectedIndex === index ? 500 : 400)
.lineHeight(22)
.margin({ top: 17, bottom: 7 })
Divider()
.strokeWidth(2)
.color('#007DFF')
.opacity(this.selectedIndex === index ? 1 : 0)
}.width('100%')
}
build() {
Column() {
Tabs({ barPosition: BarPosition.Start, index: this.currentIndex, controller: this.controller }) {
TabContent() {
Column().width('100%').height('100%').backgroundColor('#00CB87')
.onClick(() => {
// todo: 要跳转的地址和传递的参数
// name: '',
// params: {}
// router.pushUrl()
})
}.tabBar(this.tabBuilder(0, 'green'))
TabContent() {
Column().width('100%').height('100%').backgroundColor('#007DFF')
.onClick(() => {
// todo: 要跳转的地址和传递的参数
// name: '',
// params: {}
// router.pushUrl()
})
}.tabBar(this.tabBuilder(1, 'blue'))
TabContent() {
Column().width('100%').height('100%').backgroundColor('#FFBF00')
.onClick(() => {
// todo: 要跳转的地址和传递的参数
// name: '',
// params: {}
// router.pushUrl()
})
}.tabBar(this.tabBuilder(2, 'yellow'))
TabContent() {
Column().width('100%').height('100%').backgroundColor('#E67C92')
.onClick(() => {
// todo: 要跳转的地址和传递的参数
// name: '',
// params: {}
// router.pushUrl()
})
}.tabBar(this.tabBuilder(3, 'pink'))
}
.vertical(false)
.barMode(BarMode.Fixed)
.barWidth(360)
.barHeight(56)
.animationDuration(400)
.onChange((index: number) => {
// currentIndex控制TabContent显示页签
this.currentIndex = index;
this.selectedIndex = index;
})
.onAnimationStart((index: number, targetIndex: number, event: TabsAnimationEvent) => {
if (index === targetIndex) {
return;
}
// selectedIndex控制自定义TabBar内Image和Text颜色切换
this.selectedIndex = targetIndex;
})
.width(360)
.height(296)
.margin({ top: 52 })
.backgroundColor('#F1F3F5')
}.width('100%')
}
}
从代码量和维护成本就能看出,navigation 在实现标准化导航时的优势非常明显。
四、实战选型建议:结合项目需求做选择
根据我在多个鸿蒙项目中的实践经验,总结出以下选型原则:
优先选 navigation 的情况
-
项目周期紧张,需要快速搭建标准化导航界面
-
应用有明确的多标签页或侧边导航结构
-
要求所有页面导航风格统一,无需高度自定义
-
开发团队中新手较多,需要降低 UI 开发难度
优先选 router 的情况
-
只需简单的页面跳转,无复杂导航需求
-
导航 UI 需要高度定制,比如带渐变背景的标题栏、集成搜索框的导航栏
-
跨模块跳转场景多,需要灵活控制路由栈
-
开发纯逻辑层的页面跳转(如后台服务跳转页面)
混合使用的最佳实践
在实际项目中,混合使用二者往往能达到最佳效果。比如:
-
用 navigation 搭建 APP 的整体框架(底部标签栏 + 顶部导航栏)
-
在每个标签页内部,用 router 实现页面间的跳转
-
跨标签页的跳转用 router 处理,保持导航框架的稳定性
比如电商 APP 中,底部 “首页 - 分类 - 购物车 - 我的” 用 navigation 实现,首页内部从商品列表到详情页的跳转用 router 实现,既保证了整体导航的标准化,又兼顾了页面跳转的灵活性。
五、总结
router 和 navigation 不是对立关系,而是互补的工具。router 专注于 “跳转逻辑”,是鸿蒙开发中轻量跳转的首选;navigation 专注于 “完整导航体验”,能大幅降低标准化导航的开发成本。
在实际开发中,关键是根据具体需求判断:如果需要处理的是 “页面间的跳转关系”,用 router;如果需要搭建 “带 UI 的导航结构”,用 navigation。掌握二者的核心差异,合理搭配使用,才能高效开发出体验优秀的鸿蒙应用。
2万+

被折叠的 条评论
为什么被折叠?



