架构差异

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

  1. 接口上显式区分标题栏、内容区和工具栏,实现更加灵活的管理和UX动效能力;

  2. 显式提供路由容器概念,由开发者决定路由容器的位置,支持在全模态、半模态、弹窗中显示;

  3. 整合UX设计和一多能力,默认提供统一的标题显示、页面切换和单双栏适配能力;

  4. 基于通用UIBuilder能力,由开发者决定页面别名和页面UI对应关系,提供更加灵活的页面配置能力;

  5. 基于组件属性动效和共享元素动效能力,将页面切换动效转换为组件属性动效实现,提供更加丰富和灵活的切换动效;

  6. 开放了页面栈对象,开发者可以继承,能更好的管理页面显示。

能力对标

业务场景NavigationRouter
一多能力支持,Auto模式自适应单栏跟双栏显示不支持
跳转指定页面pushPath & pushDestinationpushUrl & pushNameRoute
跳转HSP中页面支持支持
跳转HAR中页面支持支持
跳转传参支持支持
获取指定页面参数支持不支持
传参类型传参为对象形式传参为对象形式,对象中暂不支持方法变量
跳转结果回调支持支持
跳转单例页面不支持支持
页面返回支持支持
页面返回传参支持支持
返回指定路由支持支持
页面返回弹窗支持,通过路由拦截实现showAlertBeforeBackPage
路由替换replacePath & replacePathByNamereplaceUrl & replaceNameRoute
路由栈清理clearclear
清理指定路由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中声明。

// main_page.json
{
  "src": [
    "pages/Index",
    "pages/pageOne",
    "pages/pageTwo"
  ]
}

以下为Router页面的示例:

// index.ets
@Entry
@Component
struct Index {
  @State message: string = 'Hello World';

  build() {
    Row() {
      Column() {
        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
      }
      .width('100%')
    }
    .height('100%')
  }
}

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

以下为Navigation导航页的示例:

// 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")
  }
}

以下为Navigation子页的示例:

// 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中(参考 系统路由配置 ):

// 工程配置文件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:

import router from '@ohos.router';

// 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中:

@Entry
@Component
struct Index {
  pathStack: NavPathStack = new NavPathStack()

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



// 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 = 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传递给子页面(有耦合,不推荐);

// Navigation根容器
@Entry
@Component
struct Index {
  // Navigation创建一个Provide修饰的NavPathStack
 @Provide('pathStack') pathStack: NavPathStack

  build() {
    Navigation(this.pathStack) {
        ...
      }.width('100%').height('100%')
    }
    .title("Navigation")
  }
}

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

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

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

@Component
export struct PageOne {
  pathStack: NavPathStack = new NavPathStack()

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

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

@Entry
@Component
struct Index {
  pathStack: NavPathStack = new NavPathStack()

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

  build() {
    Navigation(this.pathStack) {
        ...
      }.width('100%').height('100%')
    }
    .title("Navigation")
  }
}

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

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

方式四:通过自定义组件查询接口获取(参考 自定义组件方法 );

import observer from '@ohos.arkui.observer';

// 子页面中的自定义组件
@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' })
        })
    }
  }
}

生命周期

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

// 页面创建后挂树的回调
aboutToAppear(): void {
}

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

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

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

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

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

具体生命周期描述请参考: Navigation生命周期

@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来实现共享元素转场,具体可以参考如下链接:

Router共享元素转场动画

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

跨包路由

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

  1. 在想要跳转到的共享包 Har 或者 Hsp 页面里,给 @Entry修饰的自定义组件 命名:
// 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. 配置成功后需要在跳转的页面中引入命名路由的页面并跳转:
import router from '@ohos.router';
import { BusinessError } from '@ohos.base';
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;
@Component
export struct PageInHSP {
  build() {
    NavDestination() {
        ...
    }
  }
}
  1. 在HSP(HAR)的index.ets中导出组件
export { PageInHSP } from "./src/main/ets/pages/PageInHSP"
  1. 配置好HSP(HAR)的项目依赖后,在mainPage中导入自定义组件,并添加到pageMap中,即可正常调用。
// 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"});
     })
   }
   .navDestination(this.pageMap)
 }
}

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

动态路由

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

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

动态路由的优势:

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

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

  1. 定义过程: 路由表定义新增路由 -> 页面文件绑定路由名称(装饰器) -> 加载函数和页面文件绑定(动态import函数)

  2. 定义注册过程: 路由注册(可在入口ability中按需注入依赖模块的路由表)。

  3. 跳转过程: 路由表检查(是否注册过对应路由名称) -> 路由前置钩子(路由页面加载-动态Import) -> 路由跳转 -> 路由后置钩子(公共处理,如打点)。

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

方案一: 自定义路由表**

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

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

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

方案二: 系统路由表

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

具体可参考文档: Navigation系统路由

生命周期监听

Router可以通过observer实现注册监听,接口定义请参考: Router无感监听 。

import observer from '@ohos.arkui.observer';

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

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

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

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

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

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是页面的唯一标识符:

名称类型必填说明
contextUIAbilityContext/ UIContextrouterPage页面对应的上下文信息

index

|

number

|

|

routerPage在栈中的位置。

|
|

name

|

string

|

|

routerPage页面的名称。

|
|

path

|

string

|

|

routerPage页面的路径。

|
|

state

|

RouterPageState

|

|

routerPage页面的状态

|
|

pageId12+

|

string

|

|

routerPage页面的唯一标识

|

1.  import observer from '@ohos.arkui.observer';

3.  // 页面内的自定义组件
4.  @Component
5.  struct MyComponent {
6.  aboutToAppear() {
7.  let info: observer.RouterPageInfo | undefined = this.queryRouterPageInfo();
8.  }

10.  build() {
11.  // ...
12.  }
13.  }

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

|

名称

|

类型

|

必填

|

说明

|
| :-- | :-- | :-- | :-- |
|

navigationId

|

ResourceStr

|

|

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

|
|

name

|

ResourceStr

|

|

NavDestination组件的名称。

|
|

state

|

NavDestinationState

|

|

NavDestination组件的状态。

|
|

index12+

|

number

|

|

NavDestination在页面栈中的索引。

|
|

param12+

|

Object

|

|

NavDestination组件的参数。

|
|

navDestinationId12+

|

string

|

|

NavDestination组件的唯一标识ID。

|

1.  import observer from '@ohos.arkui.observer';

3.  @Component
4.  export struct NavDestinationExample {
5.  build() {
6.  NavDestination() {
7.  MyComponent()
8.  }
9.  }
10.  }

12.  @Component
13.  struct MyComponent {
14.  navDesInfo: observer.NavDestinationInfo | undefined

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

21.  build() {
22.  // ...
23.  }
24.  }

路由拦截

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

Navigation提供了setInterception方法,用于设置Navigation页面跳转拦截回调。具体可以参考文档:Navigation路由拦截

鸿蒙全栈开发全新学习指南

有很多小伙伴不知道学习哪些鸿蒙开发技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?而且学习时频繁踩坑,最终浪费大量时间。所以要有一份实用的鸿蒙(HarmonyOS NEXT)学习路线与学习文档用来跟着学习是非常有必要的。

针对一些列因素,整理了一套纯血版鸿蒙(HarmonyOS Next)全栈开发技术的学习路线,包含了鸿蒙开发必掌握的核心知识要点,内容有(ArkTS、ArkUI开发组件、Stage模型、多端部署、分布式应用开发、WebGL、元服务、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、OpenHarmony驱动开发、系统定制移植等等)鸿蒙(HarmonyOS NEXT)技术知识点。

本路线共分为四个阶段

第一阶段:鸿蒙初中级开发必备技能

在这里插入图片描述

第二阶段:鸿蒙南北双向高工技能基础:gitee.com/MNxiaona/733GH

第三阶段:应用开发中高级就业技术

第四阶段:全网首发-工业级南向设备开发就业技术:gitee.com/MNxiaona/733GH

《鸿蒙 (Harmony OS)开发学习手册》(共计892页)

如何快速入门?

1.基本概念
2.构建第一个ArkTS应用
3.……

开发基础知识:gitee.com/MNxiaona/733GH

1.应用基础知识
2.配置文件
3.应用数据管理
4.应用安全管理
5.应用隐私保护
6.三方应用调用管控机制
7.资源分类与访问
8.学习ArkTS语言
9.……

基于ArkTS 开发

1.Ability开发
2.UI开发
3.公共事件与通知
4.窗口管理
5.媒体
6.安全
7.网络与链接
8.电话服务
9.数据管理
10.后台任务(Background Task)管理
11.设备管理
12.设备使用信息统计
13.DFX
14.国际化开发
15.折叠屏系列
16.……

鸿蒙开发面试真题(含参考答案):gitee.com/MNxiaona/733GH

鸿蒙入门教学视频:

美团APP实战开发教学:gitee.com/MNxiaona/733GH

写在最后

  • 如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
  • 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
  • 关注小编,同时可以期待后续文章ing🚀,不定期分享原创知识。
  • 想要获取更多完整鸿蒙最新学习资源,请移步前往小编:gitee.com/MNxiaona/733GH

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐