首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【鸿蒙基于API 13实战开发】—— ArkUI 组件:Router切换Navigation

【鸿蒙基于API 13实战开发】—— ArkUI 组件:Router切换Navigation

原创
作者头像
小帅聊鸿蒙
发布2025-01-17 17:27:17
发布2025-01-17 17:27:17
4500
举报
文章被收录于专栏:鸿蒙开发笔记鸿蒙开发笔记

架构差异

从ArkUI组件树层级上来看,原先由Router管理的page在页面栈管理节点stage的下面。Navigation作为导航容器组件,可以挂载在单个page节点下,也可以叠加、嵌套。Navigation管理了标题栏、内容区和工具栏,内容区用于显示用户自定义页面的内容,并支持页面的路由能力。Navigation的这种设计上有如下优势:

  1. 接口上显式区分标题栏、内容区和工具栏,实现更加灵活的管理和UX动效能力;
  2. 显式提供路由容器概念,由开发者决定路由容器的位置,支持在全模态、半模态、弹窗中显示;
  3. 整合UX设计和一多能力,默认提供统一的标题显示、页面切换和单双栏适配能力;
  4. 基于通用 UIBuilder 能力,由开发者决定页面别名和页面UI对应关系,提供更加灵活的页面配置能力;
  5. 基于组件属性动效和共享元素动效能力,将页面切换动效转换为组件属性动效实现,提供更加丰富和灵活的切换动效;
  6. 开放了页面栈对象,开发者可以继承,能更好的管理页面显示。

能力对比

业务场景

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)

支持

不支持

模态嵌套路由

支持

不支持

切换指导

页面结构

Router路由的页面是一个@Entry修饰的Component,每一个页面都需要在main_page.json中声明。

代码语言:ts
复制
// main_page.json
{
  "src": [
    "pages/Index",
    "pages/pageOne",
    "pages/pageTwo"
  ]
}

以下为Router页面的示例。

代码语言:ts
复制
// index.ets
import { router } from '@kit.ArkUI';

@Entry
@Component
struct Index {
  @State message: string = 'Hello World';

  build() {
    Row() {
      Column() {
        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
        Button('router to pageOne', { stateEffect: true, type: ButtonType.Capsule })
          .width('80%')
          .height(40)
          .margin(20)
          .onClick(() => {
            router.pushUrl({
              url: 'pages/pageOne' // 目标url
            }, router.RouterMode.Standard, (err) => {
              if (err) {
                console.error(`Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`);
                return;
              }
              console.info('Invoke pushUrl succeeded.');
            })
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

代码语言:ts
复制
// pageOne.ets
import { router } from '@kit.ArkUI';

@Entry
@Component
struct pageOne {
  @State message: string = 'This is pageOne';

  build() {
    Row() {
      Column() {
        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
        Button('router back to Index', { stateEffect: true, type: ButtonType.Capsule })
          .width('80%')
          .height(40)
          .margin(20)
          .onClick(() => {
            router.back();
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

而基于Navigation的路由页面分为导航页和子页,导航页又叫Navbar,是Navigation包含的子组件,子页是NavDestination包含的子组件。

以下为Navigation导航页的示例。

代码语言:ts
复制
// 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")
    .mode(NavigationMode.Stack)
  }
}

以下为Navigation子页的示例。

代码语言:ts
复制
// 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中(参考 系统路由表 )。

代码语言:ts
复制
// 工程配置文件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"
      }
    }
  ]
}

路由操作

Router通过@ohos.router模块提供的方法来操作页面,使用前需要先import。

代码语言:ts
复制
import { router } from '@kit.ArkUI';

// push page
router.pushUrl({ url:"pages/pageOne", params: null })

// pop page
router.back({ url: "pages/pageOne" })

// replace page
router.replaceUrl({ url: "pages/pageOne" })

// clear all page
router.clear()

// 获取页面栈大小
let size = router.getLength()

// 获取页面状态
let pageState = router.getState()

Navigation通过页面栈对象 NavPathStack 提供的方法来操作页面,需要创建一个栈对象并传入Navigation中。

代码语言:ts
复制
@Entry
@Component
struct Index {
  pathStack: NavPathStack = new NavPathStack()

  build() {
    // 设置NavPathStack并传入Navigation
    Navigation(this.pathStack) {
      // ...
    }.width('100%').height('100%')
    .title("Navigation")
    .mode(NavigationMode.Stack)
  }
}



// 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: number = 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")
// ...

Router作为全局通用模块,可以在任意页面中调用,Navigation作为组件,子页面想要做路由需要拿到Navigation持有的页面栈对象NavPathStack,可以通过如下几种方式获取:

方式一:通过@Provide和@Consume传递给子页面(有耦合,不推荐)。

代码语言:ts
复制
// Navigation根容器
@Entry
@Component
struct Index {
  // Navigation创建一个Provide修饰的NavPathStack
 @Provide('pathStack') pathStack: NavPathStack = new NavPathStack()

  build() {
    Navigation(this.pathStack) {
        // ...
    }
    .title("Navigation")
    .mode(NavigationMode.Stack)
  }
}

// Navigation子页面
@Component
export struct PageOne {
  // NavDestination通过Consume获取到
  @Consume('pathStack') pathStack: NavPathStack;

  build() {
    NavDestination() {
      // ...
    }
    .title("PageOne")
  }
}

方式二:子页面通过OnReady回调获取。

代码语言:ts
复制
@Component
export struct PageOne {
  pathStack: NavPathStack = new NavPathStack()

  build() {
    NavDestination() {
      // ...
    }.title('PageOne')
    .onReady((context: NavDestinationContext) => {
      this.pathStack = context.pathStack
    })
  }
}

方式三: 通过全局的AppStorage接口设置获取。

代码语言:ts
复制
@Entry
@Component
struct Index {
  pathStack: NavPathStack = new NavPathStack()

  // 全局设置一个NavPathStack
  aboutToAppear(): void {
     AppStorage.setOrCreate("PathStack", this.pathStack)
   }

  build() {
    Navigation(this.pathStack) {
      // ...
    }.title("Navigation")
    .mode(NavigationMode.Stack)
  }
}

// Navigation子页面
@Component
export struct PageOne {
  // 子页面中获取全局的NavPathStack
  pathStack: NavPathStack = AppStorage.get("PathStack") as NavPathStack

  build() {
    NavDestination() {
      // ...
    }
    .title("PageOne")
  }
}

方式四:通过自定义组件查询接口获取,参考 queryNavigationInfo 。

代码语言:ts
复制
// 子页面中的自定义组件
@Component
struct CustomNode {
  pathStack: NavPathStack = new NavPathStack()

  aboutToAppear() {
    // query navigation info
    let navigationInfo: NavigationInfo = this.queryNavigationInfo() as NavigationInfo
    this.pathStack = navigationInfo.pathStack;
  }

  build() {
    Row() {
      Button('跳转到PageTwo')
        .onClick(() => {
          this.pathStack.pushPath({ name: 'pageTwo' })
        })
    }
  }
}
DD一下:欢迎大家关注公众号<程序猿百晓生>,可以了解到一下知识点。
代码语言:erlang
复制
1.OpenHarmony开发基础
2.OpenHarmony北向开发环境搭建
3.鸿蒙南向开发环境的搭建
4.鸿蒙生态应用开发白皮书V2.0 & V3.0
5.鸿蒙开发面试真题(含参考答案) 
6.TypeScript入门学习手册
7.OpenHarmony 经典面试题(含参考答案)
8.OpenHarmony设备开发入门【最新版】
9.沉浸式剖析OpenHarmony源代码
10.系统定制指南
11.【OpenHarmony】Uboot 驱动加载流程
12.OpenHarmony构建系统--GN与子系统、部件、模块详解
13.ohos开机init启动流程
14.鸿蒙版性能优化指南
.......

生命周期

Router页面生命周期为@Entry页面中的通用方法,主要有如下四个生命周期:

代码语言:ts
复制
// 页面创建后挂树的回调
aboutToAppear(): void {
}

// 页面销毁前下树的回调  
aboutToDisappear(): void {
}

// 页面显示时的回调  
onPageShow(): void {
}

// 页面隐藏时的回调  
onPageHide(): void {
}

其生命周期时序如下图所示:

Navigation作为路由容器,其生命周期承载在NavDestination组件上,以组件事件的形式开放。

具体生命周期描述请参考Navigation 页面生命周期 。

代码语言:ts
复制
@Component
struct PageOne {
  aboutToDisappear() {
  }

  aboutToAppear() {
  }

  build() {
    NavDestination() {
      // ...
    }
    .onWillAppear(() => {
    })
    .onAppear(() => {
    })
    .onWillShow(() => {
    })
    .onShown(() => {
    })
    .onWillHide(() => {
    })
    .onHidden(() => {
    })
    .onWillDisappear(() => {
    })
    .onDisAppear(() => {
    })
  }
}

转场动画

Router和Navigation都提供了系统的转场动画也提供了自定义转场的能力。

其中Router自定义页面转场通过通用方法pageTransition()实现,具体可参考Router 页面转场动画 。

Navigation作为路由容器组件,其内部的页面切换动画本质上属于组件跟组件之间的属性动画,可以通过Navigation中的 customNavContentTransition 事件提供自定义转场动画的能力,具体实现可以参考Navigation 自定义转场 。(注意:Dialog类型的页面当前没有转场动画)

共享元素转场

页面和页面之间跳转的时候需要进行共享元素过渡动画,Router可以通过通用属性sharedTransition来实现共享元素转场。

Navigation也提供了共享元素一镜到底的转场能力,需要配合geometryTransition属性,在子页面(NavDestination)之间切换时,可以实现共享元素转场,具体可参考 Navigation共享元素转场动画 。

跨包路由

Router可以通过命名路由的方式实现跨包跳转。

  1. 在想要跳转到的共享包 HAR 或者 HSP 页面里,给@Entry修饰的自定义组件 EntryOptions 命名。
代码语言:ts
复制
// library/src/main/ets/pages/Index.ets
// library为新建共享包自定义的名字
@Entry({ routeName: 'myPage' })
@Component
export struct MyComponent {
  build() {
    Row() {
      Column() {
        Text('Library Page')
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
      }
      .width('100%')
    }
    .height('100%')
  }
}
  1. 配置成功后需要在跳转的页面中引入命名路由的页面并跳转。
代码语言:ts
复制
import { router } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
import('library/src/main/ets/pages/Index');  // 引入共享包中的命名路由页面

@Entry
@Component
struct Index {
  build() {
    Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
      Text('Hello World')
        .fontSize(50)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 20 })
        .backgroundColor('#ccc')
        .onClick(() => { // 点击跳转到其他共享包中的页面
          try {
            router.pushNamedRoute({
              name: 'myPage',
              params: {
                data1: 'message',
                data2: {
                  data3: [123, 456, 789]
                }
              }
            })
          } catch (err) {
            let message = (err as BusinessError).message
            let code = (err as BusinessError).code
            console.error(`pushNamedRoute failed, code is ${code}, message is ${message}`);
          }
        })
    }
    .width('100%')
    .height('100%')
  }
}

Navigation作为路由组件,默认支持跨包跳转。

  1. 从HSP(HAR)中完成自定义组件(需要跳转的目标页面)开发,将自定义组件申明为export。
代码语言:ts
复制
@Component
export struct PageInHSP {
  build() {
    NavDestination() {
        // ...
    }
  }
}
  1. 在HSP(HAR)的index.ets中导出组件。
代码语言:ts
复制
export { PageInHSP } from "./src/main/ets/pages/PageInHSP"
  1. 配置好HSP(HAR)的项目依赖后,在mainPage中导入自定义组件,并添加到pageMap中,即可正常调用。
代码语言:ts
复制
// 1.导入跨包的路由页面
import { PageInHSP } from 'library/src/main/ets/pages/PageInHSP'

@Entry
@Component
struct mainPage {
 pageStack: NavPathStack = new NavPathStack()

 @Builder pageMap(name: string) {
   if (name === 'PageInHSP') {
        // 2.定义路由映射表
        PageInHSP()
   }
 }

 build() {
   Navigation(this.pageStack) {
     Button("Push HSP Page")
       .onClick(() => {
         // 3.跳转到Hsp中的页面
         this.pageStack.pushPath({ name: "PageInHSP" });
       })
   }
   .mode(NavigationMode.Stack)
   .navDestination(this.pageMap)
 }
}

以上是通过静态依赖的形式完成了跨包的路由,在大型的项目中一般跨模块的开发需要解耦,那就需要依赖动态路由的能力。

动态路由

动态路由设计的目的是解决多个产品(Hap)之间可以复用相同的业务模块,各个业务模块之间解耦(模块之间跳转通过路由表跳转,不需要互相依赖)和路由功能扩展整合。

业务特性模块对外暴露的就是模块内支持完成具体业务场景的多个页面的集合;路由管理就是将每个模块支持的页面都用统一的路由表结构管理起来。 当产品需要某个业务模块时,就会注册对应的模块的路由表。

动态路由的优势:

  1. 路由定义除了跳转的URL以外,可以丰富的配置任意扩展信息,如横竖屏默认模式,是否需要鉴权等等,做路由跳转时的统一处理。
  2. 给每个路由设置一个名字,按照名称进行跳转而不是ets文件路径。
  3. 页面的加载可以使用动态Import(按需加载),防止首个页面加载大量代码导致卡顿。

Router实现动态路由主要有下面三个过程:

  1. 定义过程: 路由表定义新增路由 -> 页面文件绑定路由名称(装饰器) -> 加载函数和页面文件绑定(动态import函数)
  2. 定义注册过程: 路由注册(可在入口ability中按需注入依赖模块的路由表)。
  3. 跳转过程: 路由表检查(是否注册过对应路由名称) -> 路由前置钩子(路由页面加载-动态Import) -> 路由跳转 -> 路由后置钩子(公共处理,如打点)。

Navigation实现动态路由有如下两种实现方案:

方案一: 自定义路由表

基本实现跟上述Router动态路由类似。

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

具体的构建过程,可以参考Navigation 自动生成动态路由 示例。

方案二: 系统路由表

从API version 12版本开始,Navigation支持系统跨模块的路由表方案,整体设计是将路由表方案下沉到系统中管理,即在需要路由的各个业务模块(HSP/HAR)中独立配置router_map.json文件,在触发路由跳转时,应用只需要通过NavPathStack进行路由跳转,此时系统会自动完成路由模块的动态加载、组件构建,并完成路由跳转功能,从而实现了开发层面的模块解耦。

生命周期监听

Router可以通过observer实现注册监听,接口定义请参考Router无感监听 observer.on('routerPageUpdate') 。

代码语言:ts
复制
import { uiObserver } from '@kit.ArkUI';

function callBackFunc(info: uiObserver.RouterPageInfo) {
    console.info("RouterPageInfo is : " + JSON.stringify(info))
}

// used in ability context.
uiObserver.on('routerPageUpdate', this.context, callBackFunc);

// used in UIContext.
uiObserver.on('routerPageUpdate', this.getUIContext(), callBackFunc);

在页面状态发生变化时,注册的回调将会触发,开发者可以通过回调中传入的入参拿到页面的相关信息,如:页面的名字,索引,路径,生命周期状态等。

Navigation同样可以通过在observer中实现注册监听。

代码语言:ts
复制
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());
        }
      })
    })
  }
}

页面信息查询

为了实现页面内自定义组件跟页面解耦,自定义组件中提供了全局查询页面信息的接口。

Router可以通过 queryRouterPageInfo 接口查询当前自定义组件所在的Page页面的信息,其返回值包含如下几个属性,其中pageId是页面的唯一标识符:

名称

类型

必填

说明

context

UIAbilityContext/ UIContext

routerPage页面对应的上下文信息

index

number

routerPage在栈中的位置。

name

string

routerPage页面的名称。

path

string

routerPage页面的路径。

state

RouterPageState

routerPage页面的状态

pageId<sup>12+</sup>

string

routerPage页面的唯一标识

代码语言:ts
复制
import { uiObserver } from '@kit.ArkUI';

// 页面内的自定义组件
@Component
struct MyComponent {
  aboutToAppear() {
    let info: uiObserver.RouterPageInfo | undefined = this.queryRouterPageInfo();
  }

  build() {
    // ...
  }
}

Navigation也可以通过 queryNavDestinationInfo 接口查询当前自定义组件所在的NavDestination的信息,其返回值包含如下几个属性,其中navDestinationId是页面的唯一标识符:

名称

类型

必填

说明

navigationId

ResourceStr

包含NavDestination组件的Navigation组件的id。

name

ResourceStr

NavDestination组件的名称。

state

NavDestinationState

NavDestination组件的状态。

index<sup>12+</sup>

number

NavDestination在页面栈中的索引。

param<sup>12+</sup>

Object

NavDestination组件的参数。

navDestinationId<sup>12+</sup>

string

NavDestination组件的唯一标识ID。

代码语言:ts
复制
import { uiObserver } from '@kit.ArkUI';

@Component
export struct NavDestinationExample {
  build() {
    NavDestination() {
      MyComponent()
    }
  }
}

@Component
struct MyComponent {
  navDesInfo: uiObserver.NavDestinationInfo | undefined

  aboutToAppear() {
    this.navDesInfo = this.queryNavDestinationInfo();
    console.log('get navDestinationInfo: ' + JSON.stringify(this.navDesInfo))
  }

  build() {
    // ...
  }
}

路由拦截

Router原生没有提供路由拦截的能力,开发者需要自行封装路由跳转接口,并在自己封装的接口中做路由拦截的判断并重定向路由。

Navigation提供了 setInterception 方法,用于设置Navigation页面跳转拦截回调。

写在最后

如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

  • 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力;
  • 关注小编,同时可以期待后续文章ing🚀,不定期分享原创知识;
  • 想要获取更多完整鸿蒙最新学习知识点,可关注B站:码牛课堂;

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 架构差异
  • 能力对比
  • 切换指导
    • 页面结构
    • 路由操作
      • DD一下:欢迎大家关注公众号<程序猿百晓生>,可以了解到一下知识点。
    • 生命周期
    • 转场动画
    • 共享元素转场
    • 跨包路由
    • 动态路由
    • 生命周期监听
    • 页面信息查询
    • 路由拦截
  • 写在最后
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档