鸿蒙开发:一文探究Navigation路由组件

简介: 如果你还在使用router做为页面跳转,建议切换Navigation组件作为应用路由框架,不为别的,因为官方目前针对router已不在推荐。

前言


如果你还在使用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) => {
       //做一些逻辑处理,比如重定向等等
      }
    })
相关文章
|
2天前
鸿蒙开发:了解@Builder装饰器
@Builder装饰是鸿蒙UI开发中,非常重要的一个装饰器,在实际的开发中,合理且正确的使用,能够让我们的代码更加的简洁,有两点需要注意,一是,是用私有还是全局,取决于当前的组件的复用机制,如果多个页面都使用了,建议以全局为主;二是传参的动态更新,有更新就使用引用参数传递,没有更新按值传递即可。
49 28
|
1月前
|
存储 人工智能 JavaScript
Harmony OS开发-ArkTS语言速成二
本文介绍了ArkTS基础语法,包括三种基本数据类型(string、number、boolean)和变量的使用。重点讲解了let、const和var的区别,涵盖作用域、变量提升、重新赋值及初始化等方面。期待与你共同进步!
109 47
Harmony OS开发-ArkTS语言速成二
|
2月前
|
前端开发 API 数据库
鸿蒙开发:异步并发操作
在结合async/await进行使用的时候,有一点需要注意,await关键字必须结合async,这两个是搭配使用的,缺一不可,同步风格在使用的时候,如何获取到错误呢,毕竟没有catch方法,其实,我们可以自己创建try/catch来捕获异常。
105 3
鸿蒙开发:异步并发操作
|
2月前
|
API
鸿蒙开发:实现popup弹窗
目前提供了两种方式实现popup弹窗,主推系统实现的方式,几乎能满足我们常见的所有场景,当然了,文章毕竟有限,尽量还是以官网为主。
106 2
鸿蒙开发:实现popup弹窗
|
1月前
|
存储 JSON 区块链
【HarmonyOS NEXT开发——ArkTS语言】购物商城的实现【合集】
HarmonyOS应用开发使用@Component装饰器将Home结构体标记为一个组件,意味着它可以在界面构建中被当作一个独立的UI单元来使用,并且按照其内部定义的build方法来渲染具体的界面内容。txt:string定义了一个名为Data的接口,用于规范表示产品数据的结构。src:类型为,推测是用于引用资源(可能是图片资源等)的一种特定类型,用于指定产品对应的图片资源。txt:字符串类型,用于存放产品的文字描述,比如产品名称等相关信息。price:数值类型,用于表示产品的价格信息。
58 5
|
1月前
|
开发工具 开发者 容器
【HarmonyOS NEXT开发——ArkTS语言】欢迎界面(启动加载页)的实现【合集】
从ArkTS代码架构层面而言,@Entry指明入口、@Component助力复用、@Preview便于预览,只是初窥门径,为开发流程带来些许便利。尤其动画回调与Blank组件,细节粗糙,后续定当潜心钻研,力求精进。”,字体颜色为白色,字体大小等设置与之前类似,不过动画配置有所不同,时长为。,不过这里没有看到额外的动画效果添加到这个特定的图片元素上(与前面带动画的元素对比而言)。这是一个显示文本的视图,文本内容为“奇怪的知识”,设置了字体颜色为灰色(的结构体,它代表了整个界面组件的逻辑和视图结构。
53 1
|
2月前
|
开发框架 物联网 API
HarmonyOS开发:串行通信开发详解
在电子设备和智能系统的设计中,数据通信是连接各个组件和设备的核心,串行通信作为一种基础且广泛应用的数据传输方式,因其简单、高效和成本效益高而被广泛采用。HarmonyOS作为一个全场景智能终端操作系统,不仅支持多种设备和场景,还提供了强大的开发框架和API,使得开发者能够轻松实现串行通信功能。随着技术的不断进步,串行通信技术也在不断发展。在HarmonyOS中,串行通信的开发不仅涉及到基本的数据发送和接收,还包括设备配置、错误处理和性能优化等多个方面。那么本文就来深入探讨在HarmonyOS中如何开发串行通信应用,包括串行通信的基础知识、HarmonyOS提供的API、开发步骤和实际代码示例
60 2
HarmonyOS实战—组件的外边距和内边距
HarmonyOS实战—组件的外边距和内边距
303 0
HarmonyOS实战—组件的外边距和内边距
|
2月前
|
API 索引
鸿蒙开发:实现一个超简单的网格拖拽
实现拖拽,最重要的三个方法就是,打开编辑状态editMode,实现onItemDragStart和onItemDrop,设置拖拽移动动画和交换数据,如果想到开启补位动画,还需要实现supportAnimation方法。
86 13
鸿蒙开发:实现一个超简单的网格拖拽
|
2月前
|
索引 API
鸿蒙开发:自定义一个股票代码选择键盘
金融类的软件,特别是股票基金类的应用,在查找股票的时候,都会有一个区别于正常键盘的键盘,也就是股票代码键盘,和普通键盘的区别就是,除了常见的数字之外,也有一些常见的股票代码前缀按钮,方便在查找股票的时候,更加方便的进行检索。
鸿蒙开发:自定义一个股票代码选择键盘

热门文章

最新文章