基于Navigation的路由管理(上篇)

科技   2024-08-29 18:04   广东  

Navigation介绍

1、Navigation简介

Navigation:路由导航的根视图容器,一般作为页面(@Entry)的根容器去使用,包括单页面(stack)、分栏(split)和自适应(auto)三种显示模式。Navigation组件适用于模块内和跨模块的路由切换,通过组件级路由能力实现更加自然流畅的转场体验,并提供多种标题栏样式来呈现更好的标题和内容联动效果。一次开发,多端部署场景下,Navigation组件能够自动适配窗口显示大小,在窗口较大的场景下自动切换分栏展示效果。

  • Title:标题栏,通过title属性对标题栏进行设置。通过menus配置菜单
  • NavContent:内容区域,默认首页显示导航内容(Navigation的子组件)或非首页显示(NavDestination的子组件),首页和非首页通过路由进行切换。

  • ToolBar:工具栏,通过toolbarConfiguration实现对工具栏的配置,如果不配置此属性,ToolBar不显示。竖屏最多支持显示5个图标,多余的图标会被放入自动生成的更多图标。

  • NavDestination:作为子页面的根容器,用于显示Navigation的内容区。具备两种类型:STANDARD(标准类型,NavDestination的生命周期跟随NavPathStack栈中标准NavDestination变化而改变),DIALOG(默认透明。不影响其他NavDestination的生命周期)。

  • NavPathStack:Navigation路由栈,由于管理NavDestination组件的路由跳转。推荐使用NavPathStack配合navDestination属性进行页面路由。

2、Navigation路由页面生命周期简介

Navigation由NavDestination组件组成页面其页面路由,在实现过程中NavDestination组件会被封装在一个自定义组件中,从而作为一个页面被路由栈使用。
当前支持的生命周期函数:aboutToAppear->onWillAppear->->onReady->onAppear->onWillShow->onShown->onWillHide->onHidden->onWillDisappear->onDisappear->aboutToDisappear

事件名称

描述

aboutToAppear

函数在创建自定义组件的新实例后,在执行其build()函数之前执行。

onWiilAppear

当NavDestination挂载之前触发此回调。在该回调中允许修改页面栈,当前帧生效。

onAppear

组件挂载显示时触发此回调。

onReady

当NavDestination即将构建子组件之前会触发此回调。

onWillShow

当该NavDestination显示之前触发此回调。

onShown

当该NavDestination页面显示时触发此回调。

onWillHide

当该NavDestination隐藏之前触发此回调。

onHidden

当该NavDestination页面隐藏时触发此回调。

onWillDisAppear

组件卸载消失时触发此回调。

onDisAppear

组件卸载消失时触发此回调。

aboutToDisappear

在自定义组件析构销毁之前执行。


Navigation VS Router

当前HarmonyOS支持两套路由机制(Navigation和Router),Navigation作为后续长期演进及推荐的路由选择方案,其与Router比较的优势如下:

  • 易用性层面:

    1. Navigation天然具备标题、内容、回退按钮的功能联动,开发者可以直接使用此能力。Router若要实现此能力,需要自行定义;

    2. Navigation的页面是由组件构成,易于实现共享元素的转场。

  • 功能层面:

    1. Navigation天然支持一多,Router不支持;

    2. Navigation没有路由数量限制,Router限制32个;

    3. Navigation可以获取到路由栈NavPathStack,并对路由栈进行操作;

    4. Navigation可以嵌套在模态对话框中,也就是说可以模态框中定义路由,Router不支持;

    5. Navigation的组件全量由开发者自行控制,开发者可以自定义复杂的动效和属性的设置(背景、模糊等),Router的page对象不对外暴露,开发者无法对page进行处理。

  • 性能层面

    1. Navigation传递参数性能更优,Navigation通过引用传递,Router通过深拷贝完成;

    2. Navigation可以配合动态加载,实现组件动态加载,Router页面使用@Entry进行修饰,当前模块加载时会生成全量页面。

1、Navigation & Router结构对比

  • Navigation中的每个页面,承载在一个page里,通过NavDestination容器实现基于组件的页面跳转。

  • Router的每一个页面配置在一个单独的page中,通过@Entry进行标识。

2、Navigation & Router能力对比

业务场景

Navigation能力

Router能力

跳转指定页面

pushPath & pushDestination

pushUrl & pushNameRouter

跳转HSP中页面

支持,需要先import页面

支持

跳转HAR中页面

支持,需要先import页面

支持

跳转传参

支持

支持

获取指定页面参数

支持

不支持

跳转结果回调

支持

支持

跳转单例页面

可通过判断栈内有没有此页面,调用moveToTop实现

支持

页面返回

pop

back

页面返回传参

支持

支持

返回指定路由

popToName&popToIndex

不支持

页面返回弹窗

通过路由拦截实现

showAlertBeforeBackPage

路由替换

replacePath & replacePathByName

replaceUrl & replaceNameRouter

路由栈清理

clear

clear

清理指定路由

removeByIndexes & removeByName

不支持

转场动画

支持

支持

自定义转场动画

支持

支持

屏蔽转场动画

pushDestination(info:  NavPathInfo, animated?: boolean) & pathStack.disableAnimation(true)

支持 duration属性设置为0

共享元素动画

支持

不支持

页面生命周期监听

UIObserver.on('navDestinationUpdate')

UIObserver.on('routerPageUpdate')

获取页面栈对象

支持

不支持

路由拦截

setInterception

不支持

路由栈信息查询

getAllPathName & getParamByIndex  & getParamByName&size

getState() & getLength()

路由栈操作

moveToTop & moveIndexToTop

不支持

沉浸式页面

支持

不支持,需通过window配置

设置页面属性(背景,模糊等)

支持,backgroundBlurStyle

不支持

设置页面标题栏(title)和工具栏(toolbar)

支持

不支持

模态嵌套路由

支持

不支持


Navigation 常见场景&解决方案

1、路由跳转场景

页面跳转是路由最常用的能力,Navigation通过NavPathStack提供了诸多方法,下文以pushDestination方法为例,介绍Navigation的路由跳转相关能力。

1.1页面间跳转

NavPathStack提供了路由管理的能力,通过NavPathStack进行页面跳转,主要适用于页面较多的应用。
Step1:创建NavPathStack对象pageStack,通常使用@Provide进行修饰,方便后续子组件通过@Comsumer获取,以实现子页面的路由跳转。
也可以将pageStack传入路由框架,以实现路由框架开发(后续路由框架章节会介绍)的开发

@Entry@Componentstruct mainPageView {  @Provide('pageStack') pageStack: NavPathStack = new NavPathStack()  ...  build() {   ...  }}

Step2:构建路由表pageMap,该方法通过@Builder进行修饰,通过传入的pageName属性,返回不同页面。

@Entry@Componentstruct mainPageView {  @Provide('pageStack') pageStack: NavPathStack = new NavPathStack()  @Builder  PageMap(pageName: string) {    if (pageName === 'loginPage') {      loginPageView()    } else if (pageName === 'mainPage') {      mainPageView()    }  }  build() {   ...  }}

Step3:在build创建Navigation组件(需要传入pageStack参数),通过navDestination属性传入路由表pageMap,并通过pageStack.pushPath()实现页面跳转。

@Entry@Componentstruct mainPageView {  @Provide('pageStack') pageStack: NavPathStack = new NavPathStack()  @Builder  pageMap(pageName: string) {    if (pageName === 'loginPage') {      loginPageView()    } else if (pageName === 'mainPage') {      mainPageView()    }  }  build() {    Navigation(this.pageStack){      ...      Button('login').onClick( ent => {        let pathInfo : NavPathInfo = new NavPathInfo('loginPage', null)        this.pageStack.pushDestination(pathInfo, true);      })    }.navDestination(this.pageMap)    ...  }}

1.2页面间参数传递

Navigation的页面间,通过NavPathInfo对象中的params属性,实现从发起页到目标页的数据传递;通过onPop回调参数,实现处理目标页面的返回。
Step1:构建NavPathInfo对象,输入需要传递给目标页面的参数。
params参数:将需要传递的数据封装起来进行传递。
onPop参数:目标页面触发pop时的返回,在回调中通过PopInfo.info.param获取到返回的对象。

// 发起页 mainPagelet loginParam : LoginParam = new LoginParam()// 构建pathInfo对象let pathInfo : NavPathInfo = new NavPathInfo('loginPage', loginParam     , (popInfo: PopInfo) => {        let loginParam : LoginParam = popInfo.info.param as LoginParam;        ...    })// 讲参数传递到目标页this.pageStack.pushDestination(pathInfo, true);    

Step2:目标页面获取发起页参数有两种方式:

方式一:目标页在NavDestination的onReady函数中获取传递过来的参数(推荐使用此方式)

  build() {    NavDestination(){      ...    }.hideTitleBar(true)    .onReady(cxt => {      this.loginParam = cxt.pathInfo.param as LoginParam;      ...    })  }

方式二:目标页通过“NavPathStack.getParamByIndex(this.pageStack.getAllPathName().length - 1)”获取到发起页传递过来的参数

@Componentexport struct loginPageView {  @Consume('pageInfo') pageStack : NavPathStack;   aboutToAppear(): void {    this.loginParam = this.pageStack.getParamByIndex(this.pageStack.getAllPathName().length - 1) as LoginParam;  }  ...} 

Step3:目标页通过NavPathStack.pop方法返回起始页,其result参数用来传递需要返回给起始页的对象。

@Componentexport struct loginPageView {  @Consume('pageInfo') pageStack : NavPathStack;  // 页面构建的对象  private loginParam! : LoginParam;  ...  build() {    NavDestination(){      ...      Button('login').onClick( ent => {        // 将对象返回给起始页        this.pageStack.pop(this.loginParam, true)      })    }  }}

1.3跨模块页面跳转

当应用模块较多,需要使用HSP(HAR)进行多模块开发,比如登录模块是一个独立团队开发,以HSP(HAR)的形式交付。
此时主页应当从mainPage跳转到HSP(HAR)中的页面,需要先导入模块的自定义组件,将组件添加到pageMap中,再通过pushDestination进行跳转。
Step1:从HSP(HAR)中完成自定义组件(需要跳转的目标页面)开发,将自定义组件申明为export。

@Componentexport struct loginPageInHSP {  @Consume('pageStack') pageStack: NavPathStack;  ...  build() {    NavDestination() {        ...    }  }}

Step2:在HSP(HAR)的index.ets中导出组件。

export { loginPageInHSP } from "./src/main/ets/pages/loginPageInHSP"

Step3:配置好HSP(HAR)的项目依赖后,在mainPage中导入自定义组件,并添加到pageMap中,即可正常调用。

// 导入模块目标页自定义组件import { loginPageInHSP } from 'library/src/main/ets/pages/loginPageInHSP'@Entry@Componentstruct mainPage {  @Provide('pageStack') pageStack: NavPathStack = new NavPathStack()  @Builder pageMap(name: string) {    if (name === 'loginPageInHSP') {      // 路由到hsp包中的登录页面      loginPageInHSP()    }  }  build() {    Navigation(this.pageStack) {      Button("login With HSP module")        .onClick(() => {           let loginParam : LoginParamInHSP = new LoginParamInHSP()           let pathInfo : NavPathInfo = new NavPathInfo('loginPageInHSP', loginParam, (popInfo: PopInfo) => {})           this.pageStack.pushDestination(pathInfo, true);      })    }    .navDestination(this.pageMap)  }}

2、页面转场

2.1默认转场动画

Navigation的pushXXX和pop方法中都带有一个参数animated,将animated设置成false则会取消转场动画,路由到Dialog模式页面或者路由出Dialog模式页面是,均无转场动画,如果需要转场动画,可以通过自定义转场动画实现。

2.2自定义转场动画

Navigation通过customNavContentTransition事件提供自定义转场动画的能力,当转场开始时,通过回调函数告知开发者,告知此次动画from(从哪来)、to(到哪去)、是Push、Pop亦或是Repalce。这里需要注意当为根视图时,NavContentInfo的name值为undefined。

2.3动画

开发者可以在customNavContentTransition的回调函数中进行动画处理,返回NavigationAnimatedTransition自定义转场协议已实现自定义转场。NavigationAnimatedTransition对象中包含三个参数,timeout(动画超时结束时间),transition(自定义动画执行回调),onTransitionEnd(转场完成回调),需要在transition方法中实现具体动画逻辑。由于自定义转场参数是在Navigation层级,但是每个页面都会有其特定的自定义转场效果,因此需要定义一套转场动画框架,已实现在Navigation层面对框架进行统一管理,各个页面通过实现框架提供的回调函数,将其特定的动画效果传递给Navigation。Step1:构建动画框架,通过一个Map管理各个页面自定义自定义动画对象CustomTransition,CustomTransition对象提供了Push、Pop、Replace各个动画阶段的回调函数给各个页面进行补充,此处将各个阶段细分为In和Out,从而实现页面进入和退出时不同的转场效果。自定义动画的构建需要结合in、out两个页面同时进行,因此案例针对不同路由方式均提供了in、out两个方法。

// 自定义动画对象,定义了Push、Pop、Replace各个动画阶段的回调函数export class CustomTransition {  pageID : number = -1;  onPushInStart: () => void = () => {};  onPushInEnd: () => void = () => {};  onPushInFinish: () => void = () => {};  onPopInStart: () => void = () => {};  onPopInEnd: () => void = () => {};  onPopInFinish: () => void = () => {};  onReplaceInStart: () => void = () => {};  onReplaceInEnd: () => void = () => {};  onReplaceInFinish: () => void = () => {};  onPushOutStart: () => void = () => {};  onPushOutEnd: () => void = () => {};  onPushOutFinish: () => void = () => {};  onPopOutStart: () => void = () => {};  onPopOutEnd: () => void = () => {};  onPopOutFinish: () => void = () => {};  onReplaceOutStart: () => void = () => {};  onReplaceOutEnd: () => void = () => {};  onReplaceOutFinish: () => void = () => {};  ...    // 获取启动阶段参数回调  public getStart(operation : NavigationOperation, isInPage : boolean) : () => void {    if (operation == NavigationOperation.PUSH) {      if (isInPage) {        return this.onPushInStart;      } else {        return this.onPushOutStart;      }    } else if (operation == NavigationOperation.POP) {      if (isInPage) {        return this.onPopInStart;      } else {        return this.onPopOutStart;      }    } else {      if (isInPage) {        return this.onReplaceInStart;      } else {        return this.onReplaceOutStart;      }    }  }  // 获取动画结束阶段参数回调  public getEnd(operation : NavigationOperation, isInPage : boolean) : () => void {   ...  }  // 获取动画结束后参数回调  public getFinished(operation : NavigationOperation, isInPage : boolean) : () => void {   ...  }} // 自定义动画对象框架export class CustomTransitionFW {  // 各个页面自定义动画对象映射表  private customTransitionMap: Map<number, CustomTransition> = new Map<number, CustomTransition>()  ...  registerNavParam(ct : CustomTransition): void {    ...    this.customTransitionMap.set(ct.pageID, ct);  }   unRegisterNavParam(pageId: number): void {    ...    this.customTransitionMap.delete(pageId);  }   getAnimateParam(pageId: number): CustomTransition {    ...    return this.customTransitionMap.get(pageId) as CustomTransition;  }}

Step2:配置Navigation的customNavContentTransition属性,当返回undefined时,使用系统默认动画。

build() {  Navigation(this.pageStack){    ...  }.hideTitleBar(true)  .hideToolBar(true)  .navDestination(this.pageMap)  .customNavContentTransition((from: NavContentInfo, to: NavContentInfo, operation: NavigationOperation) => {    // 对于Dialog型的页面,此处统一做了自定义动画的屏蔽,若需要动画,可以不做此判断。    if (from.mode == NavDestinationMode.DIALOG || to.mode == NavDestinationMode.DIALOG) {      console.error(`==== no transition because Dialog`);      return undefined;    }    let pageIn : CustomTransition | undefined;    let pageOut : CustomTransition | undefined;    pageIn = CustomTransitionFW.getInstance().getAnimateParam(to.index)    pageOut = CustomTransitionFW.getInstance().getAnimateParam(from.index)   // 业务首页跳转时若没有自定义动画诉求,此处可以通过判断页面id是否为-1(-1表示Navigation根视图)进行跳出。    if (from.index === -1 || to.index === -1) {      return undefined;    }    // 创建自定义转场协议,各个页面都会根据协议中的配置进行转场,当返回undefined时,使用系统默认动画。    let customAnimation: NavigationAnimatedTransition = {      onTransitionEnd: (isSuccess: boolean)=>{        ...      },      transition: (transitionProxy: NavigationTransitionProxy)=>{       ...      },      timeout: 100,    };    return customAnimation;  })}

Step3:customNavContentTransition事件需要返回NavigationAnimatedTransition对象,具体的动画实现需要在NavigationAnimatedTransition的transition属性中实现。transition中通过各个页面在框架中注册的回调函数,配置框架需要的动画属性。案例中各个页面注册了PUSH\POP\REPLACE的各个阶段动画参数。此处需要注意由于Navigation根页面不在栈中,因此无法与NavDestination无法产生跳转联动,因此如果第一个入栈的页面也需要自定义动画,那么就需要判断pageId是否为-1(-1及表示为根视图),如果是-1则不就行动画设置。

let customAnimation: NavigationAnimatedTransition = {  ...  transition: (transitionProxy: NavigationTransitionProxy)=>{    // 配置起始参数    if (pageOut != undefined && pageOut.pageID != -1) {      pageOut.getStart(operation, false)();    }    if (pageIn != undefined && pageIn.pageID != -1) {      pageIn.getStart(operation, true)();    }    // 执行动画    animateTo({      duration: 1000,      curve: Curve.EaseInOut,      onFinish: ()=>{        if (pageOut != undefined && pageOut.pageID != -1) {          pageOut.getFinished(operation, false)();        }        if (pageIn != undefined && pageIn.pageID != -1) {          pageIn.getFinished(operation, true)();        }        transitionProxy.finishTransition();      }}, ()=>{        if (pageOut != undefined && pageOut.pageID != -1) {          pageOut.getEnd(operation, false)();        }        if (pageIn != undefined && pageIn.pageID != -1) {          pageIn.getEnd(operation, true)();        }    })  }}

Step4:在各个页面中定义动画回调,并往自定义动画框架中注册。并在组件onDisAppear生命周期中注销框架中的页面动画回调。
Step5:定义NavDestination的translate属性,已实现动画效果。

@Componentexport struct loginPageView {  ...  private pageId: number = 0;  @State transX: number = 0;  @State transY: number = 0;   aboutToAppear(): void {    this.pageId = this.pageStack.getAllPathName().length - 1;    let ct : CustomTransition = new CustomTransition();    ct.pageID = this.pageId;    ct.onPushInStart = ct.onPushOutEnd = ct.onPopInStart = ct.onPopOutEnd = ct.onReplaceInStart = ct.onReplaceOutEnd = () => {      this.transX = -300;    }    ct.onPushInEnd = ct.onPushOutStart = ct.onPopInEnd = ct.onPopOutStart = ct.onReplaceInEnd = ct.onReplaceOutStart = () => {      this.transX = 0;    }    ct.onPushInFinish = ct.onPopInFinish  = ct.onReplaceInFinish = () => {      this.transX = 0;    }    ct.onPushOutFinish = ct.onPopOutFinish  = ct.onReplaceOutFinish = () => {      this.transX = -300;    }    // 将页面的动画效果注册到动画框架中    CustomTransitionFW.getInstance().registerNavParam(ct)  }   build() {    NavDestination(){      ...    }.hideTitleBar(true)    .onDisAppear(()=>{    // 组件销毁的时候,需要将页面的动画效果从框架中删除      CustomTransitionFW.getInstance().unRegisterNavParam(this.pageId)    })    // 定义translate,已实现动画    .translate({x: this.transX, y: this.transY, z: 0})  }} 

更多HarmonyOS示例代码详情见官网:

https://developer.huawei.com/consumer/cn/samples/

最佳实践:

https://developer.huawei.com/consumer/cn/doc/best-practices-V5/bpta-harmonyos-features-V5?catalogVersion=V5

Codelabs:

https://developer.huawei.com/consumer/cn/codelabsPortal/serviceTypes?serviceId=43

最佳实践官网链接:

https://developer.huawei.com/consumer/cn/doc/best-practices-V5/bpta-harmonyos-features-V5?catalogVersion=V5

FAQ链接:

https://developer.huawei.com/consumer/cn/doc/harmonyos-faqs-V5/faqs-business-scenarios-and-solutions-V5

开发指南链接:

https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/application-dev-guide-V5?catalogVersion=V5

API参考链接:

https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/development-intro-api-V5?catalogVersion=V5

更多推荐
点击下方图片链接,查看更多栏目内容

HarmonyOS开发者技术
HarmonyOS开发者提供HarmonyOS关键技术解析、版本更新、Codelabs实践和活动资讯,欢迎各位开发者加入鸿蒙生态,一起创造无限可能!
 最新文章