HarmonyOS应用开发实战:半天实现知乎日报项目( 五、组件导航Navigation使用详解)
在鸿蒙应用开发中,
Navigation
组件是实现界面间导航的重要工具。本文将介绍如何使用Navigation
组件实现页面跳转及参数传递,确保你能轻松构建具有良好用户体验的应用。
当前HarmonyOS支持两套路由机制(Navigation和Router),Navigation作为后续长期演进及推荐的路由选择方案,其与Router比较有不少优势。建议后续直接使用Navigation作为内部的路由方案。
Navigation介绍
Navigation组件通常作为页面的根容器,支持单页面、分栏和自适应三种显示模式。开发者可以使用Navigation组件提供的属性来设置页面的标题栏、工具栏、导航栏等。
Navigation和Router能力对标
Router路由的页面是一个@Entry修饰的Component,每一个页面都需要在main_page.json中声明。而基于Navigation的路由页面分为导航页和子页,导航页又叫Navbar,是Navigation包含的子组件,子页是NavDestination包含的子组件。
业务场景 | Navigation | Router |
---|---|---|
一多能力 | 支持,Auto模式自适应单栏跟双栏显示 | 不支持 |
跳转指定页面 | pushPath & pushDestination | pushUrl & pushNameRoute |
跳转HSP中页面 | 支持 | 支持 |
跳转HAR中页面 | 支持 | 支持 |
跳转传参 | 支持 | 支持 |
获取指定页面参数 | 支持 | 不支持 |
传参类型 | 传参为对象形式 | 传参为对象形式,对象中暂不支持方法变量 |
跳转结果回调 | 支持 | 支持 |
跳转单例页面 | 不支持 | 支持 |
页面返回 | 支持 | 支持 |
页面返回传参 | 支持 | 支持 |
返回指定路由 | 支持 | 支持 |
页面返回弹窗 | 支持,通过路由拦截实现 | showAlertBeforeBackPage |
路由替换 | replacePath & replacePathByName | replaceUrl & replaceNameRoute |
路由栈清理 | clear | clear |
清理指定路由 | removeByIndexes & removeByName | 不支持 |
转场动画 | 支持 | 支持 |
自定义转场动画 | 支持 | 支持,动画类型受限 |
屏蔽转场动画 | 支持全局和单次 | 支持 设置pageTransition方法duration为0 |
geometryTransition共享元素动画 | 支持(NavDestination之间共享) | 不支持 |
页面生命周期监听 | UIObserver.on('navDestinationUpdate') | UIObserver.on('routerPageUpdate') |
获取页面栈对象 | 支持 | 不支持 |
路由拦截 | 支持通过setInercption做路由拦截 | 不支持 |
路由栈信息查询 | 支持 | getState() & getLength() |
路由栈move操作 | moveToTop & moveIndexToTop | 不支持 |
沉浸式页面 | 支持 | 不支持,需通过window配置 |
设置页面标题栏(titlebar)和工具栏(toolbar) | 支持 | 不支持 |
模态嵌套路由 | 支持 | 不支持 |
Navigation简单示例
主页面(首页面)示例:
@Entry
@Component
struct NavigationExample {
build() {
Column() {
Navigation() {
// 中间主区域
}
.title("主标题") // 页面标题
.mode(NavigationMode.Auto) // 显示模式:Auto(自适应)、Stack(单页显示)、Split(分栏显示)、Full(强调型标题栏)、Mini(普通型标题栏)
.menus([
// 顶部菜单栏
])
.toolBar({items: [
// 底部工具栏
]})
}
.height('100%')
.width('100%')
.backgroundColor('#F1F3F5')
}
}
// index.ets
@Entry
@Component
struct Index {
pathStack: NavPathStack = new NavPathStack()
build() {
Navigation(this.pathStack) {
Column() {
Button('Push PageOne', { stateEffect: true, type: ButtonType.Capsule })
.width('80%')
.height(40)
.margin(20)
.onClick(() => {
this.pathStack.pushPathByName('pageOne', null)
})
}.width('100%').height('100%')
}
.title("Navigation")
}
}
子页示例:
// PageOne.ets
@Builder
export function PageOneBuilder() {
PageOne()
}
@Component
export struct PageOne {
pathStack: NavPathStack = new NavPathStack()
build() {
NavDestination() {
Column() {
Button('回到首页', { stateEffect: true, type: ButtonType.Capsule })
.width('80%')
.height(40)
.margin(20)
.onClick(() => {
this.pathStack.clear()
})
}.width('100%').height('100%')
}.title('PageOne')
.onReady((context: NavDestinationContext) => {
this.pathStack = context.pathStack
})
}
}
注意:每个子页也需要配置到系统配置文件route_map.json中(参考 系统路由配置 ):
// 工程配置文件module.json5中配置 {"routerMap": "$profile:route_map"}
// route_map.json
{
"routerMap": [
{
"name": "pageOne",
"pageSourceFile": "src/main/ets/pages/PageOne.ets",
"buildFunction": "PageOneBuilder",
"data": {
"description": "this is pageOne"
}
}
]
}
NavRouter介绍
NavRouter是Navigation组件中的特殊子组件,通常用于与Navigation组件配合使用,它默认提供了点击响应处理,不需要开发者自定义点击事件逻辑。NavRouter组件只有两个子组件,其中第二个子组件必须是NavDestination。而NavDestination则是Navigation组件中的特殊子组件,用于显示内容页的内容。当开发者点击NavRouter组件时,会跳转到对应的NavDestination内容区。
Navigation() {
TextInput({ placeholder: '请输入...' })
.width('90%')
.height(40)
.backgroundColor('#ffffff')
List({ space: 12 }) {
ForEach(this.arr, item => {
ListItem() {
NavRouter() {
Text("NavRouter" + item)
.width('100%')
.height(72)
.backgroundColor(Color.White)
.borderRadius(36)
.fontSize(16)
.fontWeight(500)
.textAlign(TextAlign.Center)
NavDestination() {
Text(`NavDestinationContent${item}`)
}
.title(`NavDestinationTitle${item}`)
}
}
})
}
}
.title('主标题')
.mode(NavigationMode.Stack)//导航模式
.titleMode(NavigationTitleMode.Mini)//标题模式
.menus([ //设置菜单
{ value: "", icon: './../../../resources/base/media/icon.png', action: () => {
} },
{ value: "", icon: './../../../resources/base/media/icon.png', action: () => {
} }
])
.toolBar({ items: [ //设置工具栏
{ value: 'func', icon: './../../../resources/base/media/icon.png', action: () => {
} },
{ value: 'func', icon: './../../../resources/base/media/icon.png', action: () => {
} }
] })
在鸿蒙开发中,NavRouter
和直接使用 this.pageStack.pushDestinationByName
来实现页面跳转的确可以达到相似的效果,但它们在设计理念和使用场景上有一些区别。以下是这两者之间的一些关键点和它们各自的用途:
1. NavRouter
的用途
- 封装与简化:
NavRouter
是一个封装了导航逻辑的组件,专注于处理页面间的跳转和路由管理。它提供了一个更简洁的接口以实现页面跳转,可以让开发者更容易进行组件化开发,从而提高代码的可读性和可维护性。 - 动态路由:在
NavRouter
中,可以方便地将导航逻辑和目标页面的内容结合在一起。通过NavRouter
,不必手动管理目标页面的具体情况,可以直接利用组件的声明式来定义导航内容。
2. this.pageStack.pushDestinationByName
- 底层控制:使用
this.pageStack.pushDestinationByName
更接近底层的 API,适合需要更细粒度控制的情况。它通常适用于需要直接处理路由堆栈的场合。 - 灵活性:虽然提供了更多的灵活性,但相应的也要求开发者自行管理跳转的状态、参数传递等,代码可能相对繁琐,尤其是在处理复杂的导航场景时。
3. 何时使用 NavRouter
- 如果你的应用需要在多个组件之间频繁进行导航,并且希望维护清晰的结构和可读性,使用
NavRouter
是一个不错的选择。 - 对于简单的情况,如果你的跳转逻辑不复杂,且需要直接控制页面堆栈的行为,使用
this.pageStack.pushDestinationByName
也完全可以。
总结而言,虽然两者都可以实现页面跳转,但 NavRouter
提供了更高层次的抽象,更适合构建组件化的、可维护的导航结构,而 this.pageStack.pushDestinationByName
更适合需要底层控制的场景。选择使用哪一种方式,主要依赖于具体的开发需求和代码结构设计。简言之,NavRouter
有点儿类似于折叠效果,所有内容已经拿到了,不需要再跳转到其他页面去请求,只需展开显示即可。则使用NavRouter
可以完成在一个页面里实现想要的效果。
Navigation使用步骤
一、基本使用
将Navigation
组件作为基础页面的根容器,它能够管理整个页面的布局和导航。定义NavPathStack 实例,后面需要用它实现路由跳转和传参。Navigation通过页面栈对象 NavPathStack 提供的方法来操作页面,需要创建一个栈对象并传入Navigation中。
以下是一个基本的 Navigation
组件结构示例:
import { getZhiHuNews } from '../../common/api/zhihu';
import { BaseResponse,ErrorResp } from '../../common/bean/ApiTypes';
import { Log } from '../../utils/logutil'
@Component
export default struct ZhiHu{
@State message: string = 'Hello World';
private arr: number[] = [1, 2, 3];
//重要,定义NavPathStack 实例,后面需要用它实现路由跳转和传参
pageStack: NavPathStack = new NavPathStack()
// 组件生命周期
aboutToAppear() {
Log.info('ZhiHu aboutToAppear');
getZhiHuNews("20241017").then((res) => {
Log.debug(res.data.message)
Log.debug("request","res.data.code:%{public}d",res.data.code)
}).catch((err:BaseResponse<ErrorResp>) => {
Log.debug("request","err.data.code:%d",err.data.code)
Log.debug("request",err.data.message)
});
let list: number[] = []
for (let i = 1; i <= 10; i++) {
list.push(i);
}
}
// 组件生命周期
aboutToDisappear() {
Log.info('ZhiHu aboutToDisappear');
}
build() {
Navigation(this.pageStack){
Row() {
Column({ space: 0 }) {
// 标题栏
Text("日报")
.size({ width: '100%', height: 50 })
.backgroundColor("#28bff1")
.fontColor("#ffffff")
.textAlign(TextAlign.Center)
.fontSize("18fp")
// 内容项
Swiper(this.swiperController) {
LazyForEach(this.data, (item: string) => {
Stack({ alignContent: Alignment.Center }) {
Text(item.toString())
.width('100%')
.height(160)
.backgroundColor(0xAFEEEE)
.textAlign(TextAlign.Center)
.fontSize(30)
.zIndex(1)
.onClick(() => {
//this.pageStack.pushPathByName("PageOne", item)
this.pageStack.pushDestinationByName("PageOne", { id:"9773231" }).catch((e:Error)=>{
// 跳转失败,会返回错误码及错误信息
console.log(`catch exception: ${JSON.stringify(e)}`)
}).then(()=>{
// 跳转成功
});
})
// 显示轮播图标题
Text("特立独行的猫哥")
.padding(5)
.margin({ top:60 })
.width('100%').height(50)
.textAlign(TextAlign.Center)
.maxLines(2)
.textOverflow({overflow:TextOverflow.Clip})
.fontSize(20)
.fontColor(Color.White)
.opacity(100) // 设置标题的透明度 不透明度设为100%,表示完全不透明
.backgroundColor('#808080AA') // 背景颜色设为透明
.zIndex(2)
}
}, (item: string) => item)
}
.cachedCount(2)
.index(1)
.autoPlay(true)
.interval(4000)
.loop(true)
.indicatorInteractive(true)
.duration(1000)
.itemSpace(0)
.curve(Curve.Linear)
.onChange((index: number) => {
console.info(index.toString())
})
.onGestureSwipe((index: number, extraInfo: SwiperAnimationEvent) => {
console.info("index: " + index)
console.info("current offset: " + extraInfo.currentOffset)
})
.height(160) // 设置高度
List({ space: 12 }) {
ForEach(this.arr, (item:string) => {
ListItem() {
NavRouter() {
Text("NavRouter" + item)
.width("100%")
.height(72)
.backgroundColor('#FFFFFF')
.borderRadius(24)
.fontSize(16)
.fontWeight(500)
.textAlign(TextAlign.Center)
NavDestination() {
Text("NavDestinationContent" + item)
}
.title("NavDestinationTitle" + item)
}
}
}, (item:string) => item)
}
}.size({ width: '100%', height: '100%' })
}
}
.mode(NavigationMode.Stack)
.width('100%').height('100%')
}
}
注意: 主页面使用 Navigation
组件作为根容器。而待跳转的子页面(目标页面),则是使用NavDestination作为根容器。
二、配置系统路由表
在进行页面跳转之前,需要在目录src/main/resources/base/profile
中,创建文件route_map.json
,手动注册跳转的页面。内容示例:
{
"routerMap": [
{
"name": "PageOne",
"pageSourceFile": "src/main/ets/pages/zhihu/detail/Detail.ets",
"buildFunction": "DetailPageBuilder",
"data": {
"description": "this is Page Detail"
}
}
]
}
上面这个配置其实是系统路由表,从API version 12版本开始,Navigation支持系统跨模块的路由表方案,整体设计是将路由表方案下沉到系统中管理,即在需要路由的各个业务模块(HSP/HAR)中独立配置router_map.json文件,在触发路由跳转时,应用只需要通过NavPactStack进行路由跳转,此时系统会自动完成路由模块的动态加载、组件构建,并完成路由跳转功能,从而实现了开发层面的模块解耦。
在这个示例中,name
定义了路由名称,pageSourceFile
指向页面的具体实现文件,buildFunction
则指定了构建函数。开发者需确保这些字段的正确性和一致性,以便系统能正确识别和加载目标页面。
路由表的基本概念
在新的设计中,每个业务模块(HSP / HAR)都可以独立配置 router_map.json
文件。当应用触发路由跳转时,使用 NavPathStack
内置的接口,系统将自动处理模块的动态加载和组件构建,从而实现路由跳转功能。
鸿蒙的系统路由表方案为应用开发提供了更加灵活和高效的路由管理方式。通过将路由表下沉到模块层,系统支持模块的独立配置和动态加载,不仅实现了开发层面的模块解耦,也大大简化了开发流程。对于正在进行鸿蒙应用开发来说,掌握这一机制将显著提升开发效率和应用性能。
配置路由表注意事项
- 注册正确:确保
name
和buildFunction
与要跳转的页面一致。 - 参数获取:在目标页面中,开发者可以使用
this.pageStack.getParamByName
方法获取传递的参数。
路由模块解耦的优势
- 独立性:每个业务模块可独立管理自己的路由信息,减少各模块间的相互依赖,提高开发效率。
- 动态加载:通过系统自动处理模块的加载和组件构建,提升了应用的性能和响应速度。
- 代码可维护性:在日后的维护中,开发者只需关注自身模块的路由配置和实现,对于系统的整体架构影响较小。
路由跳转的实现
使用 NavPathStack
进行路由跳转时,只需调用相应的方法并传递参数,系统会自动执行动态装载和跳转。例如:
this.pageStack.pushDestinationByName("DetailPage", { id: "9773231" })
.catch((error) => {
console.error(`路由跳转失败: ${JSON.stringify(error)}`);
})
.then(() => {
console.log("路由跳转成功");
});
在上述代码中,调用 pushDestinationByName
方法完成页面跳转,同时也可以传递必要的参数。其他一些操作还有:
@Entry
@Component
struct Index {
pathStack: NavPathStack = new NavPathStack()
build() {
// 设置NavPathStack并传入Navigation
Navigation(this.pathStack) {
...
}.width('100%').height('100%')
}
.title("Navigation")
}
// push page
this.pathStack.pushPath({ name: 'pageOne' })
// pop page
this.pathStack.pop()
this.pathStack.popToIndex(1)
this.pathStack.popToName('pageOne')
// replace page
this.pathStack.replacePath({ name: 'pageOne' })
// clear all page
this.pathStack.clear()
// 获取页面栈大小
let size = this.pathStack.size()
// 删除栈中name为PageOne的所有页面
this.pathStack.removeByName("pageOne")
// 删除指定索引的页面
this.pathStack.removeByIndexes([1,3,5])
// 获取栈中所有页面name集合
this.pathStack.getAllPathName()
// 获取索引为1的页面参数
this.pathStack.getParamByIndex(1)
// 获取PageOne页面的参数
this.pathStack.getParamByName("pageOne")
// 获取PageOne页面的索引集合
this.pathStack.getIndexByName("pageOne")
...
目标页面的实现
下面是目标页面 DetailPage
的一个简单示例:
import { Log } from '../../../utils/logutil';
@Builder
export function DetailPageBuilder() {
DetailPage()
}
@Component
export default struct DetailPage {
@State message: string = 'Hello World';
pageStack: NavPathStack = new NavPathStack();
private pathInfo: NavPathInfo | null = null;
// 组件生命周期
aboutToAppear() {
Log.info('Detail aboutToAppear');
}
// 组件生命周期
aboutToDisappear() {
Log.info('Detail aboutToDisappear');
}
build() {
NavDestination() {
Column({ space: 0 }) {
Text("内容").width('100%').height('100%').textAlign(TextAlign.Center).fontSize("25fp")
}
}
.title("日报详情")
.width('100%')
.height('100%')
.onReady(ctx => {
this.pageStack = ctx.pathStack;
const params = this.pageStack.getParamByName("PageOne");
Log.info('接收到的参数:', params);
});
}
}
组件生命周期与传递参数获取
在 DetailPage
的 onReady
方法中,开发者可以获取到传递的参数。这里使用 this.pageStack.getParamByName("PageOne")
获取指定页面的参数。这样可以灵活应对数据的传递和使用。
在导航页面中传递参数:
使用 this.pageStack.pushDestinationByName
方法跳转到目标页面,并传递参数。例如:
this.pageStack.pushDestinationByName("TargetPage", { id: "12345", name: "示例数据" })
.catch((error) => {
console.error(`路由跳转失败: ${JSON.stringify(error)}`);
});
import { Log } from '../../../utils/logutil';
@Builder
export function TargetPageBuilder() {
TargetPage();
}
@Component
export default struct TargetPage {
@State message: string = 'Hello from Target Page';
pageStack: NavPathStack = new NavPathStack();
private pathInfo: NavPathInfo | null = null;
// 组件生命周期方法,页面即将出现
aboutToAppear() {
Log.info('TargetPage aboutToAppear');
}
// 组件生命周期方法,页面即将消失
aboutToDisappear() {
Log.info('TargetPage aboutToDisappear');
}
build() {
NavDestination() {
Column({ space: 0 }) {
Text("内容").width('100%').height('100%').textAlign(TextAlign.Center).fontSize("25fp");
}
}
.title("目标页面")
.width('100%')
.height('100%')
.onReady(ctx => {
this.pageStack = ctx.pathStack; // 获取当前的路径栈
const params = this.pageStack.getParamByName("TargetPage"); // 获取传递的参数
Log.info('接收到的参数:', params);
// 例如,如果传递的数据为 { id: "12345", name: "示例数据" }
if (params) {
const id = params.id; // 获取具体的参数值
const name = params.name;
Log.info(`获取到的 ID: ${id}, 名称: ${name}`);
}
});
}
}
总结
通过使用 Navigation
组件及其相关接口,开发者可以方便地实现页面间的跳转和数据传递,从而构建丰富且流畅的用户界面。将页面逻辑与导航结构紧密结合,可以极大提升应用的可维护性和用户体验。希望本篇文章能帮助到您在鸿蒙应用开发中的导航实现。
写在最后
最后,推荐下笔者的业余开源app影视项目“爱影家”,推荐分享给与我一样喜欢免费观影的朋友。【注:该项目仅限于学习研究使用!请勿用于其他用途!】
开源地址: 爱影家app开源项目介绍及源码
https://gitee.com/yyz116/imovie
💖技术交流
作者:猫哥 (blog.csdn.net/qq8864),转载请注明出处。
团队:坚果派 团队介绍:坚果派由坚果等人创建,团队拥有12个华为HDE带领热爱HarmonyOS/OpenHarmony的开发者,以及若干其他领域的三十余位万粉博主运营。专注于分享HarmonyOS/OpenHarmony、ArkUI-X、元服务、仓颉。团队成员聚集在北京,上海,南京,深圳,广州,宁夏等地,目前已开发鸿蒙原生应用,三方库60+,欢迎交流。
其他资源
HarmonyOS:NavPathStack的详细使用说明以及示例-CSDN博客
HarmonyOS Next开发学习手册——组件导航 (Navigation) (推荐)_鸿蒙 navigation-CSDN博客
「HarmonyNextOS」页面路由跳转Router更换为Navigation_鸿蒙 routermap-CSDN博客
CommonAppDevelopment/common/routermodule/README_AUTO_GENERATE.md · HarmonyOS-Cases/Cases - Gitee.com
SystemRouterMap: 本项目提供系统路由的验证,运用系统路由表的方式,跳转到模块(HSP/HAR)的页面,可以不用配置不同跳转模块间的依赖。
HarmonyOS ArkUI实战开发-页面导航(Navigation)_arkui navigation-CSDN博客
【鸿蒙实战开发】基于Navigation的路由管理_navigation组件关联的路由栈提供了入栈方法-CSDN博客 OpenHarmony三方库中心仓--HMRouter
https://juejin.cn/post/7372488623944630281
如何开发一个OpenHarmony购物app导航页面-鸿蒙开发者社区-51CTO.COM
鸿蒙HarmonyOS实战-ArkUI组件(Navigation)_harmonyos navigation-CSDN博客
【HarmonyOS NEXT 】应用开发:ArkTS导航组件一(Navigation)_arkts navigation-CSDN博客
https://zhuanlan.zhihu.com/p/1076639693
- 3回答
- 2粉丝
- 3关注
- HarmonyOS应用开发实战:半天实现知乎日报项目(九、知乎日报项目接口使用指南)
- HarmonyOS应用开发实战:半天实现知乎日报项目(三、ArkUI页面底部导航TabBar的实现)
- HarmonyOS应用开发实战:半天实现知乎日报项目(八、知乎日报详情页的实现)
- HarmonyOS应用开发实战:半天实现知乎日报项目(四、仿知乎日报的首页轮播图实现)
- HarmonyOS应用开发实战:半天实现知乎日报项目(六、首页轮播图的完整实现)
- HarmonyOS 应用开发实战:半天实现知乎日报完整项目(一、开篇,环境准备)
- HarmonyOS应用开发实战:半天实现知乎日报项目(七、知乎日报List列表下拉刷新及上滑加载更多分页的实现)
- HarmonyOS NEXT应用开发 ( 应用的签名打包上架,各种证书详解)
- 使用 Flutter SDK 3.22.1构建HarmonyOS应用
- 鸿蒙Flutter实战:06-使用ArkTs开发Flutter鸿蒙插件
- 鸿蒙Flutter实战:12-使用模拟器开发调试
- HarmonyOS NEXT应用开发实战(封装比UniApp和小程序更简单好用的网络库)
- HarmonyOS 地图服务:深度解析其丰富功能与精准导航实力
- 『中工开发者』HarmonyOS应用开发者基础认证习题及答案
- Repeat:子组件复用