前言
如果你还在使用router做为页面跳转,建议切换Navigation组件作为应用路由框架,不为别的,因为官方目前针对router已不在推荐。
需要说明的是,Navigation它是一个组件,并不是像router那样可调用的方法,一般作为首页的根容器使用。
简单使用
简单实现一个小案例,从页面A跳转到页面B。按照官方案例,大致三步即可。
第一步,使用Navigation替换主入口页面,并设置NavPathStack,因为要使用NavPathStack执行跳转的逻辑。
@Entry @Component struct Index { pageStack: NavPathStack = new NavPathStack() build() { Navigation(this.pageStack) { RelativeContainer() { Button("点击") .onClick(() => { this.pageStack.pushPath({ name: "TestPage" }) }) .alignRules({ center: { anchor: '__container__', align: VerticalAlign.Center }, middle: { anchor: '__container__', align: HorizontalAlign.Center } }) } .height('100%') .width('100%') } } }
第二步,跳转目标页面使用NavDestination做为根布局,并且声明一个跳转页面入口函数。
// 跳转页面入口函数 @Builder export function TestPageBuilder() { TestPage() } @Component struct TestPage { @State message: string = 'Hello TestPage'; build() { NavDestination() { RelativeContainer() { Text(this.message) .id('TestPageHelloWorld') .fontSize(50) .fontWeight(FontWeight.Bold) .alignRules({ center: { anchor: '__container__', align: VerticalAlign.Center }, middle: { anchor: '__container__', align: HorizontalAlign.Center } }) } .height('100%') .width('100%') } } }
第三步,添加路由表配置,在跳转目标模块的配置文件module.json5进行添加:
{ "module" : { "routerMap": "$profile:route_map" } }
route_map.json文件配置如下:
{ "routerMap": [ { "name": "TestPage", "pageSourceFile": "src/main/ets/pages/TestPage.ets", "buildFunction": "TestPageBuilder" } ] }
以上三步执行完,我们就简单的实现了页面的跳转,当然了,Navigation作为路由容器,有着自身的生命周期还有很多可用的属性或方法,我们挑其中常见的举例说一下。
例一个简单的大纲,方便我们直观的了解:
1、了解Navigation的生命周期。
2、Navigation常见属性方法。
3、NavPathStack常见属性方法。
4、如何使用代码动态配置路由表。
5、常见使用方法。
一、了解Navigation的生命周期
大家可以验证一个问题,当使用了Navigation组件之后,会有一个常见的问题,那就是,子页面的onPageShow,onPageHide声明周期是不会走的。
aboutToAppear(): void { console.log("===声明周期:aboutToAppear") } onPageShow(): void { console.log("===声明周期:onPageShow") } onPageHide(): void { console.log("===声明周期:onPageHide") }
当跳转到目标页面时,控制台只会打印aboutToAppear:
其主要原因就是,Navigation作为一个路由容器,生命周期会承载在NavDestination组件上,以组件事件的形式进行开放。
如何得知页面的显示或隐藏,或者切换了前台和后台,需要通过NavDestination的生命周期方法进行获取:
onWillShow:NavDestination组件布局显示之前执行,此时页面不可见(应用切换到前台不会触发)。 onShown:NavDestination组件布局显示之后执行,此时页面已完成布局。 onWillHide:NavDestination组件触发隐藏之前执行(应用切换到后台不会触发)。 onHidden:NavDestination组件触发隐藏后执行(非栈顶页面push进栈,栈顶页面pop出栈或应用切换到后台)
二、Navigation常见属性方法
需要说明的是,Navigation虽然提供了很多方法,而在实际的开发中,用到的确屈指可数,因为一般情况下,什么导航栏,标题栏,我们都是不需要的,毕竟系统的是不符合我们UI设计的,只需要针对性隐藏即可。
比如提供的标题栏如下所示,能和实际中的UI匹配度,可以说是很低的,当然了,如果你们的设计是类似的,那么完全可以使用系统的。
1、title,设置页面标题
也就是上面图中的主标题,直接设置如下:
.title("我是一个标题")
由于subTitle副标题已经过时了,官方替代方案是使用title来代替:
.title(this.NavigationTitle)
通过@Builder自定义布局。
@Builder NavigationTitle() { Column() { Text('Title') .fontColor('#182431') .fontSize(30) .lineHeight(41) .fontWeight(700) Text('subtitle') .fontColor('#182431') .fontSize(14) .lineHeight(19) .opacity(0.4) .margin({ top: 2, bottom: 20 }) }.alignItems(HorizontalAlign.Start) }
效果:
2、menus,设置页面右上角菜单
.menus(this.NavigationMenus)
通过@Builder自定义布局。
@Builder NavigationMenus() { Row() { Image($r("app.media.app_icon")) .width(24) .height(24) Image($r("app.media.app_icon")) .width(24) .height(24) .margin({ left: 24 }) } }
效果:
3、titleMode,设置页面标题栏显示模式
目前官方提供了有三种模式,Full:固定为大标题模式,Mini:固定为小标题模式,Free:当内容为满一屏的可滚动组件时,标题随着内容向上滚动而缩小(子标题的大小不变、淡出)。向下滚动内容到顶时则恢复原样。
4、backButtonIcon,设置标题栏中返回键图标
.backButtonIcon(new SymbolGlyphModifier($r('app.media.app_icon')))
5、mode,导航栏的显示模式
设置导航栏的显示模式。支持Stack、Split与Auto模式。
当然了,还有很多的属性,如果使用系统提供的标题栏的话,尽量去官方多熟悉熟悉,在实际的开发中,其实这些都是不需要的,直接隐藏即可。
当我们什么也不设置的时候,会发现,内容区域是无法覆盖完整的,这是由于标题栏导致的,我们只需要隐藏即可。
未隐藏前:
隐藏后:
.hideTitleBar(true)
当然了,NavDestination中也有hideTitleBar属性,如果采用自己的UI标题栏,也需要设置为true。
三、NavPathStack常见属性方法
NavPathStack是Navigation路由栈,用于管理路由,比如跳转,移除等等,非常的重要,针对常见的几个属性,我们简单的说一下。
1、pushPath
页面跳转,将info指定的NavDestination页面信息入栈。
this.pageStack.pushPath({ name: "TestPage" })
两个参数,一个是NavPathInfo,一个参数是NavigationOptions,这是api12+的版本,如果是以下的版本,第二个参数是boolean类型,意思是是否支持转场动画。
NavPathInfo对象,name指的是NavDestination页面名称,param是传递的参数,onPop是NavDestination页面触发pop时返回的回调。
比如传递参数
this.pageStack.pushPath({ name: "TestPage",param:"我是一个参数"})
第二个参数NavigationOptions,可以设置页面栈的操作模式和转场动画,比如设置从栈底向栈顶查找,支持转场动画:
this.pageStack.pushPath({ name: "TestPage",param:"我是一个参数"},{ launchMode:LaunchMode.POP_TO_SINGLETON, animated:true })
2、pushPathByName
将name指定的NavDestination页面信息入栈。
this.pageStack.pushPathByName("TestPage","我是一个参数")
三个参数,第一个指的是NavDestination页面名称,第二个是传递的参数,最后一个是boolean类型,是否支持转场动画。
3、pushDestination
和pushPath一致,将info指定的NavDestination页面信息入栈,使用Promise异步回调返回接口调用结果。
4、pushDestinationByName
和pushPathByName一致,将name指定的NavDestination页面信息入栈,使用Promise异步回调返回接口调用结果。
5、replacePath
和pushPath的参数使用一致,主要是用于替换页面栈操作。
6、replacePathByName
和pushPathByName的参数一致,将当前页面栈栈顶退出,将name指定的页面入栈。
this.pageStack.replacePathByName("TestPage","我是一个参数",true)
7、pop
弹出路由栈栈顶元素,也就是销毁当前页面,并触发onPop回调传入页面处理结果。
this.pageStack.pop("返回结果")
8、popToName
回退路由栈到由栈底开始第一个名为name的NavDestination页面,和pop,使用方式一致,不过第一个参数为NavDestination页面名称。
9、popToIndex
回退路由栈到index指定的NavDestination页面,和pop,使用方式一致,不过第一个参数为页面栈的索引。
10、moveToTop
将由栈底开始第一个名为name的NavDestination页面移到栈顶。
11、moveIndexToTop
将index指定的NavDestination页面移到栈顶。
12、clear
清除栈中所有页面。
四、如何使用代码动态配置路由表
开篇我们是用的静态配置的路由实现的跳转,每个页面都需要在路由json文件里进行配置,十分的不便,针对此问题,官方也给我们提供了自定义路由表的方式来实现跨包动态路由。
按照官方解读,具体的实现方案如下:
1、定义页面跳转配置项。
使用资源文件进行定义,通过资源管理@ohos.resourceManager在运行时对资源文件解析。
在ets文件中配置路由加载配置项,一般包括路由页面名称(即pushPath等接口中页面的别名),文件所在模块名称(hsp/har的模块名),加载页面在模块内的路径(相对src目录的路径)。
2、加载目标跳转页面,通过动态import将跳转目标页面所在的模块在运行时加载, 在模块加载完成后,调用模块中的方法,通过import在模块的方法中加载模块中显示的目标页面,并返回页面加载完成后定义的Builder函数。
3、触发页面跳转,在Navigation的navDestination属性执行步骤2中加载的Builder函数,即可跳转到目标页面。
我们简单实现一个小案例:
第一步,创建静态路由模块,此模块,用于存放路由相关的工具类,配置路由加载配置项,并且需要被使用到的模块进行依赖,也就是,牵扯到路由跳转的所有模块都需要依赖此模块。
路由工具类:
export class RouterModule { static builderMap: Map<string, WrappedBuilder<[object]>> = new Map<string, WrappedBuilder<[object]>>(); static routerMap: Map<string, NavPathStack> = new Map<string, NavPathStack>(); // Registering a builder by name. public static registerBuilder(builderName: string, builder: WrappedBuilder<[object]>): void { RouterModule.builderMap.set(builderName, builder); } // Get builder by name. public static getBuilder(builderName: string): WrappedBuilder<[object]> { const builder = RouterModule.builderMap.get(builderName); return builder as WrappedBuilder<[object]>; } // Registering a router by name. public static createRouter(routerName: string, router: NavPathStack): void { RouterModule.routerMap.set(routerName, router); } // Get router by name. public static getRouter(routerName: string): NavPathStack { return RouterModule.routerMap.get(routerName) as NavPathStack; } // Jumping to a Specified Page by Obtaining the Page Stack. public static async push(router: RouterModel): Promise<void> { const harName = router.builderName.split('_')[0]; await import(harName).then((ns: ESObject): Promise<void> => ns.harInit(router.builderName)) RouterModule.getRouter(router.routerName).pushPath({ name: router.builderName, param: router.param }); } // Obtain the page stack and pop it. public static pop(routerName: string): void { // Find the corresponding route stack for pop. RouterModule.getRouter(routerName).pop(); } // Get the page stack and clear it. public static clear(routerName: string): void { // Find the corresponding route stack for pop. RouterModule.getRouter(routerName).clear(); } // Directly jump to the specified route. public static popToName(routerName: string, builderName: string): void { RouterModule.getRouter(routerName).popToName(builderName); } }
路由静态变量
export class BuilderNameConstants { static readonly Test: string = 'Test'; } // Indicates the key of the routerMap table in the RouterModule. export class RouterNameConstants { static readonly ENTRY_HAP: string = 'EntryHap_Router'; }
路由模型对象
export class RouterModel { // Route page alias, in the form:${bundleName}_${pageName}. builderName: string = ""; // Routing Stack Name. routerName: string = ""; // Parameters that need to be transferred to the page. param?: object = new Object(); }
第二步,主页面配置
@Entry @Component struct Index { private pageStack: NavPathStack = new NavPathStack() aboutToAppear() { RouterModule.createRouter(RouterNameConstants.ENTRY_HAP, this.pageStack); } @Builder routerMap(builderName: string, param: object) { RouterModule.getBuilder(builderName).builder(param); } build() { Navigation(this.pageStack) { RelativeContainer() { Button("点击") .onClick(() => { RouterModule.getRouter(RouterNameConstants.ENTRY_HAP) .pushPath({ name: BuilderNameConstants.Test }) }) .alignRules({ center: { anchor: '__container__', align: VerticalAlign.Center }, middle: { anchor: '__container__', align: HorizontalAlign.Center } }) } .width('100%') .height('100%') .backgroundColor(Color.Pink) }.width('100%') .height('100%') .hideTitleBar(true) .navDestination(this.routerMap); } }
第三步,子页面配置
@Component struct TestPage { build() { Column() { Text("子页面") } .width('100%') .height('100%') } } @Builder export function TestBuilder(value: object) { NavDestination() { TestPage() } .hideTitleBar(true) } const builderName = BuilderNameConstants.Test; if (!RouterModule.getBuilder(builderName)) { const builder: WrappedBuilder<[object]> = wrapBuilder(TestBuilder); RouterModule.registerBuilder(builderName, builder); }
第四步,初始化,动态导包,可以在Ability里或者AbilityStage里进行初始化。
export function importPages() { import('../pages/TestPage') }
以上四步,我们就实现了一个动态路由配置,是不是也是有点复杂,毕竟,动态导包是需要手动配置的,下篇文章,我们就把以上的程序进行简单化。
五、常见使用问题汇总
1、子页面如何拿到NavPathStack
所有的动作都是通过NavPathStack对象进行执行的,主页面我们声明了Navigation,传递了NavPathStack,那么子页面是没有NavPathStack对象的,如何进行跳转操作呢?很简单,只需要传递到子页面即可。
这里用到了@Provide装饰器和@Consume装饰器,也就是与后代组件双向同步。
在主页面声明的时候
@Provide('pageStack') pageStack: NavPathStack = new NavPathStack()
子页面接收
@Consume('pageStack') pageStack: NavPathStack;
2、页面如何拿到传递的参数
根据页面Name来获取传递的数据。
this.pageStack.getParamByName("TestPage")
除此之外,还可以根据索引
this.pageStack.getParamByIndex(1)
3、页面如何接收返回的参数
比如返回,我们随便传递一个数据:
this.pageStack.pop("返回一个数据")
接收返回的数据
this.pageStack.pushPath({ name: "TestPage", onPop: (info) => { //接收返回的数据 console.log("==========" + info.result) } })
4、如何对路由进行拦截
this.pageStack.setInterception({ willShow: (from: NavDestinationContext | "navBar", to: NavDestinationContext | "navBar", operation: NavigationOperation, animated: boolean) => { //做一些逻辑处理,比如重定向等等 } })