鸿蒙-路由以及自定义动态路由

518 阅读6分钟

鸿蒙中有两个路由 主要推荐使用Navigation

一: 自己的理解

  • Router(不推荐,但是特定时候得使用),当在超级APP中,比如支付宝,支付宝里面有口碑,当支付宝跳转到口碑可以使用router,支付宝有一个NavPathStack,口碑有自己NavPathStack。相当于一个Entry一个NavPathStack(自己的理解),但是看到官方的 hmosworld ,基本上每个模块都有一个Navigation和一个navpathstack(感觉是有问题的,又好像没问题,变更到每个模块都可以单独跑)
  • Navigation(从API Version 10开始,推荐使用NavPathStack配合navDestination属性进行页面路由),一般项目一个Entry已经足够了,可以使用NavPathStack(感觉相当于Android中的单Activity多Fragment的架构)

二:Navigation使用

2.1: 简单使用,使用navDestination属性

推荐使用NavPathStack配合navDestination

const SIMPLE_ONE = "simple_one"
const SIMPLE_TWO = "simple_two"
const SIMPLE_THREE = "simple_three"
let navPathStack: NavPathStack = new NavPathStack()

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

  @Builder
  routerMap(name: string, param: Record<string, string>) {
    if (name == SIMPLE_ONE) {
      SimpleComponent({ title: SIMPLE_ONE, param: param['name'] })
    } else if (name == SIMPLE_TWO) {
      SimpleComponent({ title: SIMPLE_TWO, param: param['name'] })
    } else if (name == SIMPLE_THREE) {
      SimpleComponent({ title: SIMPLE_THREE, param: param['name'] })
    }
  }

  build() {
    Navigation(navPathStack) {
      Column({ space: 20 }) {
        Button('simple_one').onClick(() => {
          navPathStack.pushPathByName(SIMPLE_ONE, new Object({ name: 'simple_one' }))
        }).width('80%')
        Button('simple_two').onClick(() => {
          navPathStack.pushPathByName(SIMPLE_TWO, new Object({ name: 'simple_two' }))
        }).width('80%')
        Button('simple_three').onClick(() => {
          navPathStack.pushPathByName(SIMPLE_THREE, new Object({ name: 'simple_three' }))
        }).width('80%')
      }

    }.title('NavIndex')
    .navDestination(this.routerMap)
  }
}

@Component
struct SimpleComponent {
  title: string = ''
  param: string = ''

  build() {
    NavDestination() {
      Column({ space: 20 }) {
        Text(this.title)
        Text(`传过来的参数name:${this.param}`)
        Button('点我返回').onClick(() => {
          navPathStack.pop()
        }).width('80%')
      }
    }.title(this.title)
  }
}

2.2: 使用系统路由

从API version 12开始,Navigation支持使用系统路由表的方式进行动态路由。各业务模块(HSP/HAR)中需要独立配置router_map.json文件,在触发路由跳转时,应用只需要通过NavPactStack提供的路由方法,传入需要路由的页面配置名称,此时系统会自动完成路由模块的动态加载、页面组件构建,并完成路由跳转,从而实现了开发层面的模块解耦。其主要步骤如下:

  1. 在需要跳转的模块下(或者是自己的模块)的工程resources/base/profile中创建route_map.json文件。添加如下配置信息:
    • name:跳转页面名称
    • buildFunction:定义全局Builder函数的名字
    • pageSourceFile:就是 上面的全局builder所在的文件路径
{
  "routerMap": [
    {
      "name": "SystemRouterPage",
      "pageSourceFile": "src/main/ets/pages/SystemRouterPage.ets",
      "buildFunction": "SystemRouterPageBuilder"
    }
  ]
}
  1. 在跳转目标模块的配置文件module.json5添加路由表配置,使用这个map
"routerMap": "$profile:route_map"

3.在 刚才的pageSourceFile中的路径文件创建 SystemRouterPage.ets,并且写上全局Builder函数是SystemRouterPageBuilder,经测试,加不加export都是无所谓的

@Builder 
export function SystemRouterPageBuilder(pageName: string, params: Record<string, string>) {
  SystemRouterPage({ pageName: pageName, name: params['name'] })
}
  1. 使用navPathStack跳转到route_map.json中定义的name
struct SystemPage {
  navPathStack: NavPathStack = new NavPathStack()

  build() {
    Navigation(this.navPathStack) {
      Column({ space: 20 }) {
        Button('使用系统路由跳转')
          .onClick(() => {
          // 这个SystemRouterPage就是刚才定义的名字
          this.navPathStack.pushPathByName('SystemRouterPage', new Object({ name: 'SystemPage' }))
        })
          .width('80%')
      }
    }.title('NavIndex')
  }
}

2.2.1 主要原理

由于鸿蒙是非开源的系统,大致猜一下主要原理,当打包的过程中,会把所有的模块中的 route_map.json,从pageSourceFile这个文件中,拿到buildFunction,使用 wrapBuilder:封装全局@Builder,把name和这个全局builder,放到一个map中,当调用跳转的时候,从这个map中拿到 对应的name,使用wrapBuilder 跳转到对应的页面

2.3 自定义动态路由前提

2.3.1 首先了解一下动态import

动态import优点和静态import,降低代码加载速度,降低内存,延迟加载,模块解耦

  • 在业务上除了能实现条件延迟加载,还可以实现部分反射功能。
  • 当静态导入的模块很明显的降低了代码的加载速度且被使用的可能性很低,或者并不需要马上使用它。
  • 当静态导入的模块很明显的占用了大量的系统内存且被使用的可能性很低。
  • 动态import的时候,当被导入的模块,在加载时并不存在,需要异步获取。
  • 当被导入的模块有副作用(这里的副作用,可以理解为模块中会直接运行的代码),这些副作用只有在触发了某些条件才被需要时。

2.3.2 动态引入hsp简单例子

在编译期,静态import和常量动态import可以被打包工具rollup及其插件识别解析,加入依赖树中,参与到编译流程,最终生成方舟字节码。但是如果是变量动态import,该变量值可能需要进行运算或者外部传入才能得到,在编译态无法解析出其内容,也就无法加入编译。为了将这部分模块/文件加入编译,还需要额外增加一个runtimeOnly的buildOption配置,用于配置动态import的变量实际的模块名或者文件路径。

  1. 建立一个hsp模块比如是在feature文件夹中建立 dynamic,并且在dynamic/Index.ets中加入
import { Logger } from '@nzy/logger'

let start = getStart()

function getStart() {
  Logger.debug('动态引入 dynamic 模块 开始')
}

export function add() {
  Logger.debug('执行了 add 方法')
  import('./src/main/ets/pages/DynamicDemoPage').then((ns: ESObject) => {
    // 这里再引入本模块的一个文件,执行里面的方法
    ns.dynamicPageFun()
  })
}

let end = getEnd()

function getEnd() {
  Logger.debug('动态引入 dynamic 模块 结束')
}
  1. 在主模块引入 dynamic 模块,在主模块的oh-package.json5中dependencies加入
"@nzy/dynamic": "file:../feature/dynamic"
  1. 主模块中的 build-profile.json5 中buildOption加入下面的,这样就可以动态去加载dynamic模块了
"buildOption": {
  "arkOptions": {
    "runtimeOnly": {
      "sources": [
      ],
      "packages": [
        "@nzy/dynamic",
      ]
    }
  }
},
  1. 在主模块中使用
import('@nzy/dynamic').then((ns: ESObject) => {
  ns.add()
})

效果是

第一次点击

image.png

第二次点击

image.png

上面图片由此可知

当第一次动态import的时候,会初始化里面的属性,第二次不会再初始化里面的属性了,

2.3.3 再了解 wrapBuilder:封装全局@Builder

  1. 定义个全局Builder
@Builder
export function MyBuilder(params: Object) {
  Text(`我是全局Builder的内容,传过来的参数是 :${JSON.stringify(params)}`)
    .fontSize(30)

}
  1. 使用WrappedBuilder包裹
let globalBuilder: WrappedBuilder<[Object]> = wrapBuilder(MyBuilder);
  1. 使用
Column() {
  globalBuilder.builder(new Object({ name: '全局' }))
}

2.4 自定义动态路由

上面都规则都已经讲过,大致流程

  • 配合动态引入模块,比如 dynamic 模块
  • 在模块的dynamic/Index.ets中动态在引入文件DynamicDemoPage.ets
  • 在DynamicDemoPage.ets文件中定义好全局的builder,并放到wrapBuilder中
  • 用一个HashMap<string,WrappedBuilder>,key存name,value存WrappedBuilder
  • 在配合navDestination 属性,根据name拿到WrappedBuilder,这样就可以成功了

2.4.1 首先定义一个 router 模块,所有的模块都依赖这个

它的作用存放HashMap,并且动态导入模块,并且实现跳转

import { Logger } from '@nzy/logger';

export class DynamicRouter {
  // 用来存储全局Builder函数
  private static builderMap: Map<string, WrappedBuilder<[object]>> = new Map<string, WrappedBuilder<[object]>>();
  // 一般咱们只有一个栈,如果项目比较庞大 也就是Entry,每个entry 就是一个栈
  // private static routerMap: Map<string, NavPathStack> = new Map<string, NavPathStack>();
  // 只用一个栈来管理,除非超级大的项目使用多个
  private static navPathStack: NavPathStack = new NavPathStack()

  static getNavPathStack(): NavPathStack {
    return DynamicRouter.navPathStack;
  }

  /**
   * 注册Builder
   * @param pageName
   * @param builder
   */
  public static registerBuilder(pageName: string, builder: WrappedBuilder<[object]>): void {
    DynamicRouter.builderMap.set(pageName, builder);
  }

  /**
   * 注册之前先判断有没有,有就不用重复注册了
   * @param pageName
   * @returns
   */
  public static existBuilder(pageName: string) {
    return DynamicRouter.builderMap.has(pageName)
  }

  /**
   * 根据pageName获取builder
   * @param pageName
   * @returns
   */
  public static getBuilder(pageName: string): WrappedBuilder<[object]> {
    Logger.debug(`调用了Navigation中navDestination的routerMap的builder,pageName=${pageName}`)
    const builder = DynamicRouter.builderMap.get(pageName);
    if (!builder) {
      Logger.info('not found builder ' + pageName);
    }
    return builder as WrappedBuilder<[object]>;
  }
  
  // push一个页面,会通过pageName去分割 比如在entry的 oh-package.json5 是这样引入的 @nzy/login
  // pageName 就是 @nzy/login_pageName
  public static async push(router: RouterParam): Promise<void> {
    const harName = router.pageName.split('_')[0];
    // 动态import 这个包,当已经import之后,相当于import这个包,并且初始化了Index.ets中的代码
    // import一个模块名,实际的行为是import该模块的入口文件,一般为index.ets/ts。
    Logger.debug(`调用了push,pageName=${router.pageName},准备 动态 import ${harName}`)
    let ns: ESObject = await import(harName)
    Logger.debug(`import ${harName} 成功,准备 调用 ${harName} 中 harInit`)
    // 调用har的方法
    await ns.harInit(router.pageName)
    Logger.debug(`调用 ${harName} harInit 成功, 准备真正调用 NavPathStack 的 pushpath ,name: ${router.pageName}`)
    DynamicRouter.navPathStack.pushPath({ name: router.pageName, param: router.param });
  }

  // pop
  public static pop(): void {
    DynamicRouter.navPathStack.pop();
  }

  // clear
  public static clear(): void {
    // Find the corresponding route stack for pop.
    DynamicRouter.navPathStack.clear();
  }

  // 根据一个名字pop
  public static popToName(pageName: string): void {
    DynamicRouter.navPathStack.popToName(pageName);
  }
}
// 参数
interface RouterParam {
  pageName: string,
  param?: Object,
}

2.4.2 使用这个router跳转

@Entry
@Component
struct Index {
  @Builder
  routerMap(builderName: string, param: object) {
    // 因为在builder中不能添加其他代码
    DynamicRouter.getBuilder(builderName).builder(param);

  };

  build() {
    Navigation(DynamicRouter.getNavPathStack()) {
      Button('去登录1的页面').onClick(() => {
        DynamicRouter.push({
          pageName: RouterConstants.LOGIN_PAGE1, param: new Object({
            origin: '主页面传过来的'
          })
        })
      })
        .width('80%')
    }.title('NavIndex')
    .navDestination(this.routerMap)
    .height('100%')
    .width('100%')
  }
}

在模块中的Index中构造 harInit 方法,来初始化要跳转的Page.ets,里面在构建全局,builder

export async function harInit(builderName: string): Promise<object | undefined> {
  // 动态引入要跳转的页面
  switch (builderName) {
    case RouterConstants.LOGIN_PAGE1:
      Logger.debug('调用Login包harInit方法 ,准备动态 import ./src/main/ets/components/Page1')
      return import("./src/main/ets/components/Page1");
    case RouterConstants.LOGIN_PAGE2:
      Logger.debug('调用Login包harInit方法 ,准备动态 import ./src/main/ets/components/Page2')
      return import("./src/main/ets/components/Page2");
    default:
      return undefined
  }
}

在Page1中

初始化全局builder,并注册到map中

@Builder
export function harBuilder(value: object) {
  Page1({ params: value })
}
const builderName = RouterConstants.LOGIN_PAGE1;
if (!DynamicRouter.existBuilder(builderName)) {
  Logger.debug('Login的 .src/main/ets/components/Page1.ets registerBuilder 全局的builder')
  const builder: WrappedBuilder<[object]> = wrapBuilder(harBuilder);
  DynamicRouter.registerBuilder(builderName, builder);
}

效果打印

image.png

源码地址 gitee