HarmonyOS :路由怎么选? Router 和 Navigation 如何实现跳转(传参)及页面回调


highlight: androidstudio theme: channing-cyan

路由的选择

HarmonyOS提供两种路由实现的方式,分别是 RouterNavPatchStack。两者使用场景和特效各有优劣。

组件 适用场景 特点 备注
Router 模块间与模块内页面切换 通过每个页面的url实现模块间解耦
NavPathStack 模块内页面切换 通过组件级路由统一路由管理
  • 什么时候使用 NavPatchStack ?

如果是单包应用开发,不使用动态包(hsp)进行拆包,只是使用静态包(har)简单的进行模块拆分,那么我推荐使用 navPatchStack

  • 什么时候使用 Router ?

如果像开发 鸿蒙元服务,对单包体积有 2M 的限制,那么我们不得不使用动态包的方式。将相对独立的功能,二级页面等拆分出去,封装成动态包,可避开 dependencies 直接依赖得引用形式。

此时使用 router 跳转 url 的方式才可跳转到动态包内非直接引用的页面

NavPatchStatck 如何跳转(传参)及页面回调

NavPathStack 是配合 Navigation 一起使用的, Navigation导航组件做统一的页面跳转管理,它提供了一系列属性方法来设置页面的标题栏、工具栏以及菜单栏的各种展示样式。

如何跳转(传参)及实现页面回调?

//第一步:定义一个用于传参和实现页面回调的模型
export interface RouterModel {
  params?: Object, // 要传递给跳转页面的参数
  popCallback?: (value: Object | undefined) => void // 用于页面回调
}
//第二步,需要在应用的根页面自行维护 navStack 实例,并传递给根节点 Navigation
@Provide('navPathStack') navPathStack: NavPathStack = new NavPathStack()

Navigation(this.pageInfos) {
Column() {}
}
.title(‘NavIndex’)
.navDestination(this.PageMap)

// 统一管理维护路由跳转
@Builder
  PageMap(name: string, params: RouterModel) {
    if (name === ‘pageOne’) {
      TestNavPathPage({ // TestNavPathPage 就是要跳转的目标页面
routerParams: params
})
    } else {
// 默认空页面
}
}

/// 任意一个页面获取 navPathStack 调用跳转并传参
@Component
export struct RouterCallbackExample {
  @Consume('navPathStack') navPathStack: NavPathStack;
 

  // NavPatchStack 方式跳转并获取回调
  navPathStackJump() {
    const routerParams: RouterModel = {
      params: '我是入参 123', //传递到跳转页面的入参
      popCallback: (callbackValue) => {
        // 这里拿到回调结果,注意要判断 callbackValue !== undefine
        // 这里拿到下面目标页面回传的结果 ‘我是回调的结果 345’
      }
    }
   this.navPathStack.pushPathByName('pageOne', routerParams) // 'pageOne' 对应上面 'PageMap' 方法内定义的路径名称常量
  }

  build() {
    Button('跳转').onClick(() => {
        this.navPathStackJump()
    })
  }
}
/// 目标页面接收入参、并返回页面回调
@Component
export struct TestNavPathPage {
  @Consume('navPathStack') navPathStack: NavPathStack;
  routerParams?: RouterModel
  @State receiveParams: string = ''

  aboutToAppear(): void {
    // 接收入参,这里拿到上面传入的 ‘我是入参 123’
    let receiveParams = this.routerParams!.params
  }

  build() {
    NavDestination() {
      Button('关闭页面并回调结果').onClick(() => {
        if (this.routerParams?.popCallback !== undefined) {
          this.routerParams.popCallback('我是回调的结果 345 ')
        }
        this.navPathStack.pop()
      })
    }.title('跳转目标页')
  }
}

Router 如何跳转(传参)及页面回调

Router 跳转可支持跳转本包内页面以及动态包或者拆包内的页面,

  • url 的定义举例如下:
1. 本地包内,或者直接依赖的静态包内页面,url 定义为 : pages/Page1
2. 分包内的页面,url 定义为 :@bundle:com.rex.harmony.atomic.service/featureName/ets/pages/Page2

// com.rex.harmony.atomic.service 是我的应用包名
// featureName 是跳转页面所在的模块名称,对应 module.json5 里面额 name
// ets/pages/Page2 为目标页面在模块内的页面路径,对应 main_pages.json 内的页面路径

  • url 跳转
router.pushUrl({ url: '', params: Object })
  • 注意: 使用 router + url 进行跳转的目标页面必须使用 @Entry 修饰,且在main_pages.json 文件内填写对应路径

重点:截止 API11 版本,router 支持传递的 params 传参,不是引用传递,所以在动态包内实际获取到的不是同一个对象,为了实现页面回调,router 我们需要做如下封装:

  • 如何跳转(传参)及实现页面回调?抛砖引玉 ---
  1. 在公共的har包内定义 Router 管理类 FastRouter

(在下文扩展中解释单例为什么这么实现)

import { RouterModel } from './model/RouterModel'
import { router } from '@kit.ArkUI'

/// 基于 router 库封装,为了实现页面回调
export class FastRouter {
public readonly routerStack: RouterModel =

/// 跨 hsp 使用这种方式实现单例
public static instance(): FastRouter {
const storageKey = ‘REX_FAST_ROUTER’
if (!AppStorage.has(storageKey)) {
AppStorage.setOrCreate(storageKey, new FastRouter())
}
return AppStorage.get(storageKey)!
}

/// 获取路由传递的入参
public static get getRouterCurrentParams(): RouterModel | undefined {
const stack = FastRouter.instance().routerStack
if (stack.length === 0) {
return undefined
}
return stack[stack.length - 1]
}

/// push 页面
public static async push(route: RouterModel): Promise {
try {
await router.pushUrl({ url: route.url, params: route.params })
FastRouter.instance().routerStack.push(route)
} catch (_) {
console.log(‘>>>>’)
}
}

/// replace 页面
public static async replace(route: RouterModel): Promise {
try {
await router.replaceUrl({ url: route.url, params: route.params })
const instance = FastRouter.instance()
const list = instance.routerStack
if (list.length > 0) {
instance.routerStack.splice(instance.routerStack.length - 1, 1, route)
}
} catch (_) {
// 暂无处理
}
}

/// 退出栈顶页面
public static async pop(animated?: boolean): Promise {
router.back()
const routerStack = FastRouter.instance().routerStack
routerStack.pop()
}
}

  1. 任一页面使用 FastRouter 进行 url 跳转
// 跳转到 hsp 包(feature_hsp_page)内的 TestHspHomePage 页面
const routerParams: RouterModel = {
  url: '@bundle:com.rex.harmony.atomic.service/feature_hsp_page/ets/pages/TestHspHomePage',
  params: '我是入参 1488',
  popCallback: (callbackValue) => {
    if (callbackValue !== undefined) {
      //这里获取跳转页的回调数据
      //接收到下文中目标页面的回调结果:‘我是回调的结果 6100 ’
    }
  }
}
FastRouter.push(routerParams)
  1. 在目标页面内接收入参并回调结果
@Entry
@Component
struct Index {
  routerParams?: RouterModel

aboutToAppear(): void {
this.routerParams = FastRouter.getRouterCurrentParams as RouterModel
let receiveParams = this.routerParams.params //这里接收入参,也就是上面传递的 ‘我是入参 1488’
}

build() {
Button(‘关闭页面并回调结果’).onClick(() => {
if (this.routerParams?.popCallback !== undefined) {
this.routerParams.popCallback('我是回调的结果 6100 ')
}
FastRouter.pop()
})
}
}

总结

NavPatchStackRouter 两种路由方式各有优劣,NavPatchStack 方便统一管理,Router 方便解耦,两者没有任何关联,可以一起使用,也可以单独使用。

扩展:动态包、静态包的使用差异

说到动态包(HAR)和静态包(HSP),这里扩展一下两者的区别。

静态包的 module.json5 文件,type 标识为 har

{
  "module": {
    "name": "静态包模块名称",
    "type": "har",
    "deviceTypes": [
      "default",
      "tablet"
    ]
  }
}

静态包的 module.json5 文件,type 标识为 shared

{
  "module": {
    "name": "动态包模块名称",
    "type": "shared",
    "description": "$string:shared_desc",
    "deviceTypes": [
      "phone",
      "tablet"
    ],
    "deliveryWithInstall": true,
    "installationFree": true,
    "pages": "$profile:main_pages"
  }
}

动态包和静态包都可以被直接引用,在 oh-package.json5

{
 ...省略
  "dependencies": {
    "@rex/任意名称": "file:../../base/静态包模块名称",
    "@rex/任意名称": "file:../../base/动态包模块名称"
  }
}

重点:

  • 动态包可以不直接依赖( 例如:使用 preload ),跳转动态包内的页面可以通过 router 拼接 url 进行跳转
  • har 中的代码和资源跟随使用方编译,如果有多个使用方,它们的编译产物中会存在多份相同拷贝
  • hsp 中的代码和资源可以独立编译,运行时在一个进程中代码也只会存在一份

举个例子(entry 直接依赖 harA,entry 直接依赖 harB,harB 直接依赖 harA,hsp 直接依赖 harA):

  • 如果应用内没有使用动态包,或者使用动态包但使用的是直接引用的方式时,我们的单例可以这么写:
    class SimgleProvider {
      private static _instance?: SimgleProvider
  public static instance(): SimgleProvider {
    if (!SimgleProvider._instance) {
      SimgleProvider._instance = new SimgleProvider()
    }
    return SimgleProvider._instance
  }
}

  • 在应用的任意一处获取这个单例都是同一个对象

  • 但如果使用非直接依赖的方式加载动态包,仍然使用上述代码实现单例,我们在 entry 或者 被 entry 直接依赖的 har 中获取到的 SimgleProvider 单例 和在 hsp 包内获取到的 SimgleProvider 单例 不是指向同一个对象。

可以简单的总结为:一个应用内,所有直接依赖的包共享一份内存,所有非直接依赖的包共享另外一份内存

  • 如果需要整个应用内,包括 entry、har、hsp 都指向的是同一个单例对象,需要把上面的单例实现代码改成如下:
class SimgleProvider {
  private static _instance?: SimgleProvider

public static instance(): FastRouter {
const storageKey = ‘SimgleProvider’
if (!AppStorage.has(storageKey)) {
AppStorage.setOrCreate(storageKey, new SimgleProvider())
}
return AppStorage.get(storageKey)!
}
}

  • 借助AppStorage,实现全应用单例

附注(Example)

Demo示例(基于API11开发,支持NEXT及以上版本运行)已上传可供参考,包含如下内容:

  • 静态库+动态包+多模块设计
  • 状态管理
  • 统一路由管理(router+navPathStack)
  • 网络请求、Loading 等工具库封装
  • 自定义组件、自定义弹窗(解耦)
  • EventBus 事件通知

“本文正在参加华为鸿蒙有奖征文征文活动”


这是一个从 https://juejin.cn/post/7369120920148213795 下的原始话题分离的讨论话题