HarmonyOS开发实战:Navigation路由管理规范指南

22 篇文章 0 订阅
17 篇文章 0 订阅

Navigation简介

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

image.png

  1. Title:标题栏,通过title属性对标题栏进行设置。通过menus配置菜单
  2. NavContent:内容区域,默认首页显示导航内容(Navigation的子组件)或非首页显示(NavDestination的子组件),首页和非首页通过路由进行切换。
  3. ToolBar:工具栏,通过toolbarConfiguration实现对工具栏的配置,如果不配置此属性,ToolBar不显示。竖屏最多支持显示5个图标,多余的图标会被放入自动生成的更多图标。
  4. NavDestination:作为子页面的根容器,用于显示Navigation的内容区。具备两种类型:STANDARD(标准类型,NavDestination的生命周期跟随NavPathStack栈中标准NavDestination变化而改变),DIALOG(默认透明。不影响其他NavDestination的生命周期)。
  5. NavPathStack:Navigation路由栈,由于管理NavDestination组件的路由跳转。推荐使用NavPathStack配合navDestination属性进行页面路由。

Navigation路由页面生命周期简介

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

image.png

事件名称

描述

aboutToAppear

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

onAppear

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

onReady

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

onShown

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

onHidden

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

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进行修饰,当前模块加载时会生成全量页面。

Navigation & Router结构对比

  1. Navigation中的每个页面,承载在一个page里,通过NavDestination容器实现基于组件的页面跳转。
  2. Router的每一个页面配置在一个单独的page中,通过@Entry进行标识。

image.png

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) & patshStack.disableAnimation(true)

支持 duration属性设置为0

共享元素动画

支持

不支持

页面生命周期监听

UIObserver.on('navDestinationUpdate')

UIObserver.on('routerPageUpdate')

获取页面栈对象

支持

不支持

路由拦截

setInterception

不支持

路由栈信息查询

getAllPathName & getParamByIndex & getParamByName&size

getState() & getLength()

路由栈操作

moveToTop & moveIndexToTop

不支持

沉浸式页面

支持

不支持,需通过window配置

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

支持,backgroundBlurStyle

不支持

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

支持

不支持

模态嵌套路由

支持

不支持

Navigation常见场景&解决方案

路由跳转场景

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

页面间跳转

NavPathStack提供了路由管理的能力,通过NavPathStack进行页面跳转,主要适用于页面较多的应用。

Step1:创建NavPathStack对象pageStack,通常使用@Provide进行修饰,方便后续子组件通过@Consumer获取,以实现子页面的路由跳转。也可以将pageStack传入路由框架,以实现路由框架开发(后续路由框架章节会介绍)的开发

@Entry

@Component

struct mainPageView {

  @Provide('pageStack') pageStack: NavPathStack = new NavPathStack()

  ...

  build() {

    ...

  }

}

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

@Entry

@Component

struct 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

@Component

struct 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)

    ...

  }

}

页面间参数传递

Navigation的页面间,通过NavPathInfo对象中的params属性,实现从发起页到目标页的数据传递;通过onPop回调参数,实现处理目标页面的返回。

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

// 发起页 mainPage

let 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函数中,通过通过cxt.pathInfo.param,获取传递过来的参数。

build() {

  NavDestination(){

    ...

  }.hideTitleBar(true)

  .onReady(cxt => {

    this.loginParam = cxt.pathInfo.param as LoginParam;

    ...

  })

}

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

@Component

export struct loginPageView {

  @Consume('pageInfo') pageStack : NavPathStack;

  // 页面构建的对象

  private loginParam! : LoginParam;

  ...

  build() {

    NavDestination(){

      ...

      Button('login').onClick( ent => {

        // 将对象返回给起始页

        this.pageStack.pop(this.loginParam, true)

      })

    }

  }

}

跨模块页面跳转

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

Step1:从HSP(HAR)中完成自定义组件(需要跳转的目标页面)开发,将自定义组件申明为export。

@Component

export 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

@Component

struct 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)

  }

}

页面转场

默认转场动画

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

自定义转场动画

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

image.png

开发者可以在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属性,已实现动画效果。

@Component

export 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})

  }

}

共享元素转场

NavDestination之间可以通过geometryTransition实现共享元素转场。

Step1:起始页为需要实现共享元素转场的元素添加geometryTransition属性,id参数必须在两个NavDestination之间保持一致。

起始页代码

Column() {

  Image($r('app.media.startIcon'))

    .geometryTransition('1')

  Text("起始页共享的图片")

}

.width(100)

.height(100)

目的页代码

Column() {

  Image($r('app.media.startIcon'))

    .geometryTransition('1')

  Text("目的页共享的图片")

}

.width(200)

.height(200)

Step2:animateTo方法发起页面跳转(push 或者 pop),触发共享元素转场动画执行。注意此处需要关闭页面默认的跳转动画

Button('跳转目的页')

  .width('80%')

  .height(40)

  .margin(20)

  .onClick(() => {

    animateTo({ duration: 1000 }, () => {

      this.pageStack.pushPath({ name: 'DestinationPage' }, false)

    })

  })

其他常见业务功能场景

基于Dialog类型NavDestination,实现弹窗页面跳转返回后弹窗不关闭

image.png

NavDestination有两个类型,通过mode属性进行配置,前文介绍的NavDestination均是STANDARD类型。

名称

描述

STANDARD

标准类型NavDestination的生命周期跟随NavPathStack栈中标准Destination变化而改变。

DIALOG

默认透明。不影响其他NavDestination的生命周期

DIALOG类型的NavDestination背景透明,且不会影响其他NavDestination生命周期,也就是说前面的页面不会隐藏,因此比较适合开发类似“高德地图”的应用,此类应用特点是:底层一个固定的页面,其余页面都是覆盖在底层页面之上,但是底层页面始终可见。mode为DIALOG的NavDestination在转入和转出时,默认不支持动画,可以通过自定义动画的方式配置动画。在全局弹窗需求交付之前,可以通过Dialog来实现弹窗,Dialog实现的弹窗可以实现解耦,还可以实现弹窗跳转页面返回,弹窗不关闭等效果(比如隐私弹窗,该弹窗还可以接续跳转到隐私条款页)。构建Dialog类型的NavDestination,因为Dialog类型页面背景是透明的,为了更好的效果,可以增加一层蒙层。如果要对弹窗组件增加类似移动出现效果,需要在组件中自行实现。

@Component

export struct PrivacyDialog {

  @Consume('pageInfo') pageStack : NavPathStack;

  @State isAgree: string = "Not Agree";



  build() {

    NavDestination(){

      Stack({ alignContent: Alignment.Center }){

        // 蒙层

        Column() {

        }

        .width("100%")

        .height("100%")

        .backgroundColor('rgba(0,0,0,0.5)')

        // 隐私弹窗

        Column() {

          Text("注册应用账号").fontSize(30).height('20%')

          Text("请您仔细阅读一下协议并同意,我们将全力保护您的个人信息安全,您可以使用账号登录APP。").height('40%')

          Divider()

          Row(){

            // 点击隐私条款,跳转到隐私条款页面,并接受隐私条款的返回值,用来刷新页面的同意状态。

            Text("《应用隐私政策》").onClick(ent => {

              let pathInfo : NavPathInfo = new NavPathInfo('PrivacyItem', null

                , (popInfo: PopInfo) => {

                  this.isAgree = popInfo.result.toString();

                })

              this.pageStack.pushDestination(pathInfo, true)

            })

            Text(this.isAgree)

          }.height('20%')

          Divider()

          // 点击同意\不同意按钮,将状态返回登录页

          Row(){

            Button("不同意").onClick(ent => {

              this.pageStack.pop("Not Agree", true)

            }).width('30%')

            Button("同意").onClick(ent => {

              this.pageStack.pop("Agree", true)

            }).width('30%')

          }.height('20%')

        }.backgroundColor(Color.White)

        .height('50%')

        .width('80%')

      }

    }.hideTitleBar(true)

    // 设置Dialog类型

    .mode(NavDestinationMode.DIALOG)

  }

}

基于页面生命周期监听,实现页面埋点

在应用运维时,需要了解哪些业务功能用户使用频率较高,为了实现此能力,可以在页面显示\隐藏的的时候进行相应的埋点操作,以方便后台进行统计分析。此类诉求可以通过DevNavigation的状态监听实现。可以在Ability中的onWindowStageCreate方法中通过uiObserver.on("navDestinationUpdate",(info) => {})方法注册DevNavigation的状态监听,样例代码通过打样日志的方式记录各个DevNavigation显示\隐藏时的状态,在真实业务中可以相应进行替换。

export default class EntryAbility extends UIAbility {

  ...

  onWindowStageCreate(windowStage: window.WindowStage): void {

    ...

    windowStage.getMainWindow((err: BusinessError, data) => {

      ...

      windowClass = data;

      // 获取UIContext实例。

      let uiContext: UIContext = windowClass.getUIContext();

      // 获取UIObserver实例。

      let uiObserver : UIObserver = uiContext.getUIObserver();

      // 注册DevNavigation的状态监听.

      uiObserver.on("navDestinationUpdate",(info) => {

        // NavDestinationState.ON_SHOWN = 0, NavDestinationState.ON_HIDE = 1

        if (info.state == 0) {

          // NavDestination组件显示时操作

          console.info('page ON_SHOWN:' + info.name.toString());

        } else if (info.state == 1){

          // NavDestination组件隐藏时操作

          console.info('page ON_HIDE' + info.name.toString());

        } else {

          // NavDestination组件其他操作

          console.info('page state:' + info.state);

        }

      })

    })

  }

}

基于路由拦截实现页面返回弹窗确认

业务场景:要针对页面进行统一的逻辑处理,比如在页面返回时根据页面参数配置规则匹配是否需要给出弹窗提示,若规则匹配到就弹框,否则默认走返回流程。

image.png

为了实现此功能,需要依赖路由拦截能力,通过路由跳转拦截,实现控制路由跳转,弹窗或阻止路由跳转等操作。 为此NavPathStack提供了setInterception方法,用于设置Navigation页面跳转拦截回调。该方法需要一个NavigationInterception对象,该对象包含三个回调函数:

名称

描述

willShow

页面跳转前拦截,允许操作栈,在当前跳转中生效。

didShow

页面跳转后回调。在该回调中操作栈在下一次跳转中刷新。

modeChange

Navigation单双栏显示状态发生变更时触发该回调。

简单理解就是willShow会在from页面动效完成之前回调;didiShow会在from页面动效完成之后回调。此处需要注意的是无论哪个回调,在进入回调时页面栈都已经发生了变化。案例中,由于在弹出确认框时,不能出现登录页面返回的转场动画,因此需要在willShow回调中执行相关业务。

    1. 代码判断srcPage为登录页面loginPageView,targetPage为主页面MainPage是执行弹窗逻辑。
    2. 由于loginPageView此时已经出栈,需要重新将loginPageView重新入栈。此处需要注意需要将参数重新传递给loginPageView,否则无法顺利完成页面初始化。如果需要保持页面之前的输入值,则需要为组件独立注册LocalStorage以保持状态。
    3. 弹窗ConfirmDialog通过Dialog类型的NavDestination实现,启动ComfirmDialog时,将点击OK和Cancel的回调一并传入,从而可以提高ComfirmDialog的独立封装能力。
    4. 当点击‘OK’确认返回时,需要将ConfirmDialog和loginPageView同时出栈。此时需要注意这一帧的起点栈为Dialog类型,因此不会有转场动画,如果需要转场动画,需要通过自定义动画实现。
export function registerInterception() {

  RouterManager.setInterception({

    willShow: (from: NavDestinationContext | "navBar", to: NavDestinationContext | "navBar",

      operation: NavigationOperation, animated: boolean) => {

      if (typeof to === "string") {

        console.log("target page is navigation home");

        return;

      }

      if (typeof from === "string") {

        console.log("target page is navigation home");

        return;

      }

      // redirect target page.Change pageTwo to pageOne.

      let target: NavDestinationContext = to as NavDestinationContext;

      let srcPage: NavDestinationContext = from as NavDestinationContext;

      console.log("==== setInterception target.pathInfo.name = " + target.pathInfo.name)

      if (target.pathInfo.name === 'MainPage'&& srcPage.pathInfo.name === 'loginPage') {

        RouterManager.pushPath('loginPage', srcPage.pathInfo.param, srcPage.pathInfo.onPop, true)

        let cdd = new ConfirmDialogData();

        cdd.onCancelClick = () => {

          RouterManager.popWithoutParam(false)

        }

        cdd.onConfirmClick = () => {

          // RouterManager.removeByName(RouterInfo.LOGIN_PAGE)

          RouterManager.popWithoutParam(true)

          RouterManager.popWithoutParam(true)

        }

        RouterManager.pushPath("confirmDialog", cdd, () => {}, false)

      }

    },

    didShow: (from: NavDestinationContext | "navBar", to: NavDestinationContext | "navBar",

      operation: NavigationOperation, isAnimated: boolean) => {

    },

    modeChange: (mode: NavigationMode) => {

    }

  })

}

补充一些有用的知识

从案例中可以看出,loginPage经历了出栈再入栈两个动作,即使再次入栈时,将初始的入参重新传递了回去,但是页面表单之前填入的信息任然无法恢复,也就是如果之前表单填入了'abc',再次入栈时显示的时初始值,之前填入的'abc'无法恢复。为了解决这个问题,可以采用组件级别的localStorage来解决。

let localStorageLoginPage: LocalStorage = new LocalStorage();

localStorageLoginPage.setOrCreate('userName', '');



@Component

export struct loginPageView {

  @LocalStorageLink('userName') userName : string = ''

  @State initUserName : string = ''



  build() {

    NavDestination(){

      TextInput({placeholder: 'User Name', text: this.initUserName})

        .onChange((value: string) => {

          this.userName = value;

        }).width('80%')

      ...

    }

  }.hideTitleBar(true)

  .onReady((ctx)=>{

    this.loginParam = ctx.pathInfo.param as LoginParamInHAR;

    // 判断页面初始输入框初始阶段显示的指是来自传参还是localStorage

    if (this.userName == '') {

      this.initUserName = this.loginParam.userName == 'not login' ? '' : this.loginParam.userName

    } else {

       this.initUserName = this.userName;

    }

  })

}



@Builder

export function getLoginPage() : void {

  loginPageView({}, localStorageLoginPage);

}

路由框架

大型应用为了实现更好的模块间解耦,往往会设计一套路由框架,用于解耦各个模块间的路由关系(A模块不感知路由具体通过哪个模块实现、如何实现,只通过路由名称实现路由跳转,闭环业务功能)

系统跨模块路由框架

系统支持跨模块的路由表方案,整体设计是基于模块路由表的思想。即在需要路由的各个业务模块(HSP/HAR)中独立配置router_map.json文件,在触发路由跳转时,应用只需要通过NavPactStack进行路由跳转。此时系统会完成路由模块的动态加载、组件构建,并完成路由跳转功能,从而实现了开发层面的模块接口。

Step1:在需要配置路由表的模块的module.json5中添加路由表配置

{

  "module" : {

  ...

  "routerMap": "$profile:router_map"

}

}

Step2:在src\main\resources\base\profile(若之前没有profile目录,则需要新建)目录下新建router_map.json文件,此处文件名称需要与module.json5中配置的文件名称一致。

配置项

说明

name

跳转页面名称,用于路由跳转时使用,如this.pageStack.pushPathByName("MainPage", null, false);

pageSourceFile

跳转目标页在包内的路径,相对src目录的相对路径。

buildFunction

跳转目标页的入口函数名称,必须以@Builder修饰。

data

应用自定义字段。可以通过配置项读取接口getConfigInRouteMap获取。

{

  "routerMap": [

    {

      "name": "MainPage",

      "pageSourceFile": "src/main/ets/components/mainpage/MainPage.ets",

      "buildFunction": "getMainPageRouterMap",

      "data": {

        "description" : "this is mainPage"

      }

    },

    {

      "name": "PersonDetail",

      "pageSourceFile": "src/main/ets/components/mainpage/PersonDetail.ets",

      "buildFunction": "getPersonDetailRouterMap",

      "data": {

        "description" : "this is PersonDetailPage"

      }

    }

  ]

}

Step3:Navigation主页开发:使用系统路由表方案可以不配置.navDestination()属性,如若配置.navDestination()属性,则会先查询.navDestination()中配置的路由,再查询router_map中的路由。

@Entry

@Component

struct IndexRouterMap {

  @Provide('pageInfo') pageStack : NavPathStack = new NavPathStack();



  build() {

    Navigation(this.pageStack){

    }.onAppear(() => {

      this.pageStack.pushPathByName("MainPage", null, false);

    })

    .hideNavBar(true)

  }

}

更近一步,我们可以稍微做一些封装,将pageStack封装到路由管理对象中,这样就可以避免通过Provide的方式来传递pageStack

@Entry

@Component

struct IndexRouterMap {

  private pageStack : NavPathStack = new NavPathStack();



  aboutToAppear(): void {

    RouterManager.createNavPathStack(this.pageStack)

    ...

  }



  build() {

    Navigation(this.pageStack){

    }.onAppear(() => {

      RouterManager.pushPath("MainPage", null, ()=>{}, false);

    })

    .hideNavBar(true)

  }

}

Step4:NavDestination页面开发:与不使用路由框架对比,采用路由框架后,需要注意一下两点:

    1. 页面需要增加@Builder函数,函数名称与router_map.json中的buildFunction属性必须一致
    2. 在router_map.json中配置的data信息,通过onReady回调中执行ctx.getConfigInRouteMap()获取。
@Component

export struct MainPage {

  ...

  build() {

    NavDestination() {

      ...

    }.hideTitleBar(true)

    .onReady(ctx => {

      let config : RouteMapConfig | undefined = ctx.getConfigInRouteMap()

      if (config) {

        let value : string = config.data['description'];

        ...

      }

    })

  }

}



@Builder

export function getMainPageRouterMap() : void {

  MainPage();

}

特别注意

由于路由表框架涉及SDK和编译构建多个阶段,在工程整体迁移到新SDK版本后,需要手动更新oh-package.json5的devDependencies项下下依赖信息,具体版本号建议通过新建工程的方法确认。

{

  ...

  "devDependencies": {

  "@ohos/hypium": "1.0.18-rc2",

  "@ohos/hamock": "1.0.1-rc2"

}

}

自定义跨模块路由框架

应用可以自定义实现路由框架,整体方案如下:

    1. 定义个路由管理模块,各个提供路由页面的模块均依赖此模块
    2. 构建Navgation组件时,将NavPactStack注入路由管理模块,路由管理模块对NavPactStack进行封装,对外提供路由能力。
    3. 各个路由页面不再提供组件,转为提供@build封装的构建函数,并再通过WrappedBuilder封装后,实现全局封装。
    4. 各个路由页面将模块名称、路由名称、WrappedBuilder封装后构建函数注册如路由模块。
    5. 当路由需要跳转到指定路由时,路由模块完成对指定路由模块的动态导入,并完成路由跳转。

Router切换Navigation

对于原先使用Router的应用,建议尽快切换Navigation,避免由于页面的持续增加导致迁移工作量变大。当前美团、高德地图是基于Navigation进行应用开发,微博已经完成Router到Navigation切换。

Navigation基础容器构建

在应用路由根节点新增Navigation节点,定义好pageStack,pageMap及Navigation,将页面原先的内容封装入Navigation容器。若不使用路由框架,则此时需要将全量路由在pageMap中申明,也可以在切换时新增使用【路由框架】)具体步骤可以参考【页面间跳转】

image.png

页面切换为NavDestination组件

业务模块页面新增NavDestination组件,删除@Entry装饰器,并export导出。比较快速的查找方式就是工程内搜索@Entry关键字,这样就可以快速找到页面。如果切换时新增使用路由框架,此时需要同时注册路由,参考【路由框架】)

image.png

转场动画切换

ruter通过pageTransition设置页面间转场动画,Navigation中的页面是组件级的,都隶属于一个页面,因此需要通过Navigation的customNavContentTransition事件实现自定义转场。

Router相关路由能力替换

Router 能力

Navigation 能力

pushUrl

pushDestination

pushNamedRoute

pushDestination

replaceUrl

replacePath

replaceNamedRoute

replacePath

back

pop

showAlertBeforeBackPage\hideAlertBeforeBackPage

可自定义 ,参考生命周期监听

clear

clear

getLength

size

getState

getAllPathName

自定义转场动画

参考【自定义转场动画】

共享元素动画

参考【共享元素动画】

Router路由配置信息从main_pages.json中移除

模块中的main_pages.json文件删除转换为NavDestination的页面路由信息。此处初始页面需要保留,计划任然使用router的页面也需要保留(并不推荐这样做)。

image.png

生命周期切换

router路由页面切换为NavDestination路由后,之前每个页面的onPageShow\onPageHide生命周期函数不在生效,需要替换为NavDestination路由相关生命周期onShow\onHide。

路由框架切换

使用部分应用如果之前基于router构建了路由框架,则需要对路由框架就行Navigation化处理。具体过程可以参考【系统跨模块路由框架】

Step1:完成各个页面的Navigation/NavDestination改造。

Step2:页面切换为@build构建函数。

Step3:完成系统路由配置,并将NavPathSatck传入路由模块。

Step4:路由框架router相关方法通过NavPathSatck相关方法进行替换。

Step5:给涉及路由的模块配置路由表router_map.json

Navigation常见问题

Q:Router和Navigation是否可以混用?A:可以混用,但是推荐使用Navigation进行开发。

Q:Navigation页面层级是否有限制?A:无限制。

Q:Navigation传递参数时,参数对象里面是否可以有方法?A:参数对象可以包含方法,目标页面可以调用对象方法。

  • 14
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值