系列文章《鸿蒙开发从入门到专家》第 4 篇(中级篇)
前言
前三篇我们把界面搭出来了,但一个真实的应用不可能只有一个页面。页面之间怎么跳转?参数怎么传递?返回时数据怎么回传?这些问题的答案取决于你选哪种导航方案。
鸿蒙目前提供两套路由机制:
Router:从早期版本继承的页面路由 API,类似 Android 的 Intent
Navigation:HarmonyOS NEXT 主推的新一代导航组件,基于组件化的栈管理
很多开发者的困惑在于:网上搜到的教程混用两套 API,有的用 router.pushUrl(),有的用 Navigation() 组件,到底该学哪个?本文直接给结论:新项目一律用 Navigation,然后告诉你为什么。
一、Router:简单但有限的传统方案
1.1 基本用法
import { router } from '@kit.ArkUI'
// 跳转到新页面
router.pushUrl({
url: 'pages/DetailPage',
params: { id: '123', title: '新闻标题' }
})
// 返回上一页
router.back()
// 返回并携带结果
router.back({
url: 'pages/Index',
params: { updated: true }
})
1.2 接收参数
import { router } from '@kit.ArkUI'
@Entry
@Component
struct DetailPage {
@State id: string = ''
@State title: string = ''
aboutToAppear() {
let params = router.getParams() as Record<string, string>
this.id = params['id'] || ''
this.title = params['title'] || ''
}
build() {
Column() {
Text(this.title).fontSize(20)
Text(`ID: ${this.id}`).fontSize(14).fontColor('#999')
}
}
}
1.3 Router 的局限性
Router 适合快速原型或只有一个入口的简单页面,但企业级应用肯定会踩到上面的坑。
二、Navigation:鸿蒙主推的现代导航方案
Navigation 是 ArkUI 提供的声明式导航组件,它将页面栈管理内嵌到组件树中,配合 NavPathStack 实现完整的导航能力。
2.1 基础结构
import { NavPathStack } from '@kit.ArkUI'
@Entry
@Component
struct RootPage {
@Provide('navStack') navStack: NavPathStack = new NavPathStack()
@Builder
PageMap(name: string) {
if (name === 'HomePage') {
HomePage()
} else if (name === 'DetailPage') {
DetailPage()
} else if (name === 'SettingsPage') {
SettingsPage()
}
}
build() {
Navigation(this.navStack) {
// 默认显示的首页
HomePage()
}
.hideTitleBar(true) // 隐藏默认标题栏(我们自定义)
.navDestination(this.PageMap) // 页面映射表
.width('100%')
.height('100%')
}
}
2.2 页面跳转与参数传递
// HomePage.ets
@Component
struct HomePage {
@Consume('navStack') navStack: NavPathStack
build() {
Column() {
List() {
ListItem() {
Text('点击跳转到详情页')
.onClick(() => {
this.navStack.pushPathByName('DetailPage', {
id: 'news-001',
title: 'HarmonyOS NEXT 5.0 发布'
})
})
}
}
}
}
}
// DetailPage.ets
@Component
struct DetailPage {
@Consume('navStack') navStack: NavPathStack
// 通过 onReady 获取导航参数
onReady?: (context: NavDestinationContext) => void
build() {
NavDestination() {
Column() {
Text('详情页内容')
Button('返回')
.onClick(() => {
this.navStack.pop()
})
}
}
.onReady((context: NavDestinationContext) => {
let params = context.pathInfo.param as Record<string, string>
console.info(`收到参数: ${JSON.stringify(params)}`)
})
.hideTitleBar(true)
}
}
2.3 页面生命周期:Navigation 的一大优势
API 26 为 Navigation 新增了 onWillShow 和 onWillHide 生命周期,这对数据刷新场景极其有用:
NavDestination() {
Column() {
Text(this.pageTitle)
}
}
.onReady((context) => {
// 页面首次创建时触发
this.pageTitle = context.pathInfo.param?.title || ''
})
.onWillShow(() => {
// 页面即将显示时触发(包括从其他页面返回时)
console.info('DetailPage will show')
this.refreshData()
})
.onWillHide(() => {
// 页面即将隐藏时触发
console.info('DetailPage will hide')
})
.onDisappear(() => {
// 页面销毁时触发
console.info('DetailPage disappeared')
})
对比 Router:Router 只有页面的 aboutToAppear 和 aboutToDisappear,无法区分"首次创建"和"从其他页面返回"。Navigation 的 onWillShow 在返回时也会触发,这是实现"返回刷新"的关键。
三、Navigation 高阶能力
3.1 转场动画自定义
Navigation 支持页面转场动画的自定义,API 26 还新增了动画打断和接续能力:
Navigation(this.navStack) {
HomePage()
}
.navDestination(this.PageMap)
// 自定义转场动画
.transition(TransitionEffect.OPACITY.animation({ duration: 300 }))
更精细的控制可以在每个 NavDestination 上单独设置:
NavDestination() {
// 页面内容
}
// 入场动画:从右侧滑入
.transition(TransitionEffect.translate({ x: '100%' }).animation({ duration: 250 }))
// 出场动画:向右侧滑出
.exitTransition(TransitionEffect.translate({ x: '100%' }).animation({ duration: 250 }))
3.2 页面内容扩展到状态栏
现代 App 普遍要求页面内容延伸到状态栏下方(沉浸式效果),Navigation 原生支持:
NavDestination() {
Column() {
// 顶部安全区域占位
Row().height(this.statusBarHeight)
// 真正的内容区域
Text('标题').fontSize(20)
// ...
}
}
.hideTitleBar(true)
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP]) // 扩展到状态栏
3.3 单例模式跳转
有些页面(如登录页、设置页)在栈中应该只有一个实例,重复跳转时不创建新页面:
// 单例跳转:如果栈中已存在该页面,直接回到它
this.navStack.pushPathByName('SettingsPage', {}, { launchMode: LaunchMode.SINGLETON })
3.4 跨模块路由(HSP/HAR 动态路由)
API 26 新增了一个重要能力:业务模块(HSP/HAR)支持配置 router_map.json 路由表,实现模块间动态路由解耦。
// 模块内的 router_map.json
{
"routerMap": [
{
"name": "FeatureDetailPage",
"pageSourceFile": "src/main/ets/pages/FeatureDetailPage.ets",
"buildFunction": "FeatureDetailPageBuilder"
}
]
}
主模块在 module.json5 中引入该路由表后,就可以通过名称跳转到子模块的页面,无需硬编码路径。这是模块化架构的关键基础设施。
四、Router vs Navigation 选型决策表
结论:除非你的应用只有一个页面,否则请直接上 Navigation。前期多花 30 分钟学习,后期能省掉大量路由相关的 bug 和重构工作。
五、实战:搭建带导航的完整应用骨架
// AppEntry.ets(应用入口)
import { NavPathStack } from '@kit.ArkUI'
@Entry
@Component
struct AppEntry {
@Provide('navStack') navStack: NavPathStack = new NavPathStack()
@Builder
PageMap(name: string) {
if (name === 'MainTabs') {
MainTabsPage()
} else if (name === 'SearchPage') {
SearchPage()
} else if (name === 'DetailPage') {
DetailPage()
}
}
build() {
Navigation(this.navStack) {
MainTabsPage()
}
.hideTitleBar(true)
.navDestination(this.PageMap)
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
}
// MainTabsPage.ets(底部 Tab 容器)
@Component
struct MainTabsPage {
@Consume('navStack') navStack: NavPathStack
@State currentTab: number = 0
build() {
Tabs({ barPosition: BarPosition.End }) {
TabContent() { HomeTabContent() }
.tabBar(this.buildTabBar('首页', 0))
TabContent() { CategoryTabContent() }
.tabBar(this.buildTabBar('分类', 1))
TabContent() { CartTabContent() }
.tabBar(this.buildTabBar('购物车', 2))
TabContent() { ProfileTabContent() }
.tabBar(this.buildTabBar('我的', 3))
}
.onChange((index: number) => this.currentTab = index)
}
@Builder
buildTabBar(title: string, index: number) {
Column() {
Text(title)
.fontSize(12)
.fontColor(this.currentTab === index ? '#317AF7' : '#999999')
}
.height(56)
.justifyContent(FlexAlign.Center)
}
}
// HomeTabContent.ets(首页内容)
@Component
struct HomeTabContent {
@Consume('navStack') navStack: NavPathStack
build() {
Column() {
// 顶部搜索栏(点击跳转到搜索页)
Row() {
Text('搜索商品')
.fontColor('#999999')
.layoutWeight(1)
}
.width('100%')
.height(40)
.padding({ left: 16, right: 16 })
.backgroundColor('#F0F0F0')
.borderRadius(20)
.onClick(() => {
this.navStack.pushPathByName('SearchPage', {})
})
// 商品列表(点击跳转到详情页)
List() {
LazyForEach(this.productList, (item: Product) => {
ListItem() {
// ... 商品卡片布局
}
.onClick(() => {
this.navStack.pushPathByName('DetailPage', { productId: item.id })
})
})
}
}
}
}
这个骨架的特点:
Navigation 作为根容器,管理整个应用的页面栈
Tabs 嵌套在 Navigation 内部,管理底部导航
任何深层页面都可以通过
this.navStack.pushPathByName()跳转,无需层层传递返回时
onWillShow自动触发,适合实现"返回刷新列表"
小结与下篇预告
本文的核心结论很简单:新项目用 Navigation,别用 Router。
Navigation 的优势不是某一项功能,而是完整的导航体验:可控的转场动画、完整的生命周期、安全区域适配、模块化路由支持。这些是 Router 无法弥补的架构级差距。
下一篇(第 5 篇)我们进入数据层:鸿蒙的三种数据持久化方案——Preferences(轻量键值)、RelationalStore(关系型数据库)和分布式数据管理。你会学到什么时候该用哪种,以及它们各自的性能边界。