头图

小白必看 HarmonyOS Next HMRouter 轻松上手秘籍

前言

HMRouter 作为 HarmonyOS 的页面跳转场景解决方案,聚焦解决应用内原生页面的跳转逻辑。

HMRouter 底层对系统 Navigation 进行封装,集成了 Navigation、NavDestination、NavPathStack 的系统能力,提供了可复用的路由拦截、页面生命周期、自定义转场动画,并且在跳转传参、额外的生命周期、服务型路由方面对系统能力进行了扩展。

目的是让开发者在开发过程中无需关注 Navigation、NavDestination 容器组件的相关细节及模板代码,屏蔽跳转时的判断逻辑,降低拦截器、自定义转场动画实现复杂度,更好的进行模块间解耦

对比

目前鸿蒙应用开发中,官方推出的路由方案有两个,分别是RouterNavigation。目前官方主要推荐的也是 Navigation。

业务场景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')
获取页面栈对象支持不支持
路由拦截支持通过 setInterception 做路由拦截不支持
路由栈信息查询支持getState() & getLength()
路由栈 move 操作moveToTop & moveIndexToTop不支持
沉浸式页面支持不支持,需通过 window 配置
设置页面标题栏(titlebar)和工具栏(toolbar)支持不支持
模态嵌套路由支持不支持

但是原生的 Navigation 缺少了路由拦截、页面生命周期、自定义转场动画,并且在跳转传参、额外的生命周期、服务型路由。

因此 HMRouter 便是对此做出了拓展和增强。

学习目标

接下来,将通过这篇文章带领小伙伴上手HMRouter的应用。

工程目录

新建完工程后,再新建一个 Cart 动态共享包模块

  1. 工程的目录名称是 study
  2. 入口模块是 entry
  3. cart 是 hsp 模块

image-20241224094003606

配置环境

使用 ohpm 安装依赖

ohpm install @hadss/hmrouter
ohpm install @hadss/hmrouter-transitions

编译插件配置

  1. 修改工程的hvigor/hvigor-config.json文件,加入路由编译插件

    {
      "dependencies": {
        "@hadss/hmrouter-plugin": "^1.0.0-rc.10"
        // 使用npm仓版本号
      }
      // ...其他配置
    }
  2. 在使用到 HMRouter 的模块中引入路由编译插件,修改hvigorfile.ts

    我们项目的模块无非是 Hap、Har 和 Hsp。对应你当前的模块是哪种类型,就使用对应的写法
    1. Hap

      // entry/hvigorfile.ts  entry模块的hvigorfile.ts
      import { hapTasks } from "@ohos/hvigor-ohos-plugin";
      import { hapPlugin } from "@hadss/hmrouter-plugin";
      
      export default {
        system: hapTasks,
        plugins: [hapPlugin()], // 使用HMRouter标签的模块均需要配置,与模块类型保持一致
      };
    2. Har

      import { harTasks } from "@ohos/hvigor-ohos-plugin";
      import { harPlugin } from "@hadss/hmrouter-plugin";
      
      export default {
        system: harTasks,
        plugins: [harPlugin()], // 使用HMRouter标签的模块均需要配置,与模块类型保持一致
      };
    3. Hsp

      import { hspTasks } from "@ohos/hvigor-ohos-plugin";
      import { hspPlugin } from "@hadss/hmrouter-plugin";
      
      export default {
        system: hspTasks,
        plugins: [hspPlugin()], // 使用HMRouter标签的模块均需要配置,与模块类型保持一致
      };

初始化路由框架

entry/src/main/ets/entryability/EntryAbility.ets
export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    HMRouterMgr.init({
      context: this.context,
    });
  }
}

定义路由入口

entry/src/main/ets/pages/Index.ets

当前页面作为整个路由的根容器

import { HMDefaultGlobalAnimator, HMNavigation } from "@hadss/hmrouter";
import { AttributeUpdater } from "@kit.ArkUI";

class MyNavModifier extends AttributeUpdater<NavigationAttribute> {
  initializeModifier(instance: NavigationAttribute): void {
    // instance.hideNavBar(true);  // 先注释掉  否则看不见结果
  }
}

@Entry
@Component
export struct Index {
  modifier: MyNavModifier = new MyNavModifier();

  build() {
    // @Entry中需要再套一层容器组件,Column或者Stack
    Column() {
      // 使用HMNavigation容器
      HMNavigation({
        navigationId: 'mainNavigation', homePageUrl: 'MainPage',
        options: {
          standardAnimator: HMDefaultGlobalAnimator.STANDARD_ANIMATOR,
          dialogAnimator: HMDefaultGlobalAnimator.DIALOG_ANIMATOR,
          modifier: this.modifier
        }
      }) {
        Column({ space: 10 }) {
          Button("跳转到 登录页面")
        }
      }
    }
    .height('100%')
    .width('100%')
  }
}

模块内跳转

PixPin_2024-12-24_10-22-23

我们先演示跳转到当前模块中的某个页面。

HMRouter 默认指定了 页面目录 为 entry/src/main/ets/components

我们在这个里新建一个组件 entry/src/main/ets/components/LoginPage.ets

import { HMRouter } from "@hadss/hmrouter"

@HMRouter({
  pageUrl: 'LoginPage',
})
@Component
export struct LoginPage {
  build() {
    Column() {
      Button('登录页面')
    }
    .width("100%")
    .height("100%")
    .justifyContent(FlexAlign.Center)
  }
}

此时,回到首页中,进行点击跳转登录

Button("跳转到 登录页面").onClick(() => {
  HMRouterMgr.push({ pageUrl: "LoginPage" });
});

路由传参

传递

HMRouterMgr.push({ pageUrl: "LoginPage", param: { 数据 } });

接收

HMRouterMgr.getCurrentParam(HMParamType.all);

指定编译目录

刚才的登录页面是存放到 components 目录下的,实际开发中,我们可以会通过 views来存放页面,所以这里来设置下

在项目根目录创建路由编译插件配置文件study/hmrouter_config.json(可选)

{
  "scanDir": ["src/main/ets/views"]
}

然后重命名之前的文件夹名字 entry/src/main/ets/componentsentry/src/main/ets/views

重新编译执行即可

模块之间跳转

刚才的演示是在同一个模块内进行的,现在我们来演示不同模块之间的跳转

演示的目标是 entry 模块跳转到 cart 模块

cart 模块配置编译插件

cart 是 hsp

cart/hvigorfile.ts

import { hspTasks } from "@ohos/hvigor-ohos-plugin";
import { hspPlugin } from "@hadss/hmrouter-plugin";

export default {
  system: hspTasks,
  plugins: [hspPlugin()], // 使用HMRouter标签的模块均需要配置,与模块类型保持一致
};

新建购物详情页面

cart/src/main/ets/views/CartDetail.ets
import { HMRouter } from "@hadss/hmrouter"

@HMRouter({
  pageUrl: 'CartDetail',
})
@Component
export struct CartDetail {
  build() {
    Column() {
      Button('我的是购物车详情页面')
    }
    .width("100%")
    .height("100%")
    .justifyContent(FlexAlign.Center)
  }
}

entry 模块引入 cart 模块

entry/oh-package.json5
  "dependencies": {
    "cart": "file:../cart"
  },

首页中进行跳转

entry/src/main/ets/pages/Index.ets
Button("跳转到 购物车详情页面").onClick(() => {
  HMRouterMgr.push({ pageUrl: "CartDetail" });
});

效果

PixPin_2024-12-24_10-48-50

跳转动画

我们可以在跳转页面的时候来指定跳转动画

分类两个步骤

  1. 定义动画
  2. 使用动画

定义动画

假设 A 跳转 B, 那么就是 B 使用动画,为了方便使用,可以在 B 页面定义动画

我们继续使用上面的例子

index 跳转到 CarDetail , 所以在 CarDetail 定义动画

cart/src/main/ets/views/CartDetail.ets
@HMAnimator({ animatorName: "liveCommentsAnimator" })
export class liveCommentsAnimator implements IHMAnimator {
  effect(enterHandle: HMAnimatorHandle, exitHandle: HMAnimatorHandle): void {
    // 入场动画
    enterHandle.start(
      (
        translateOption: TranslateOption,
        scaleOption: ScaleOption,
        opacityOption: OpacityOption
      ) => {
        translateOption.y = "100%";
      }
    );
    enterHandle.finish(
      (
        translateOption: TranslateOption,
        scaleOption: ScaleOption,
        opacityOption: OpacityOption
      ) => {
        translateOption.y = "0";
      }
    );
    enterHandle.duration = 500;

    // 出场动画
    exitHandle.start(
      (
        translateOption: TranslateOption,
        scaleOption: ScaleOption,
        opacityOption: OpacityOption
      ) => {
        translateOption.y = "0";
      }
    );
    exitHandle.finish(
      (
        translateOption: TranslateOption,
        scaleOption: ScaleOption,
        opacityOption: OpacityOption
      ) => {
        translateOption.y = "100%";
      }
    );
    exitHandle.duration = 500;
  }
}

使用动画

在 HMRouter 上使用

@HMRouter({
  pageUrl: 'CartDetail',
  // 2 使用动画
  animator: "liveCommentsAnimator"
})
@Component
export struct CartDetail {
  build() {
    Column() {
      Button('我的是购物车详情页面')
    }
    .width("100%")
    .height("100%")
    .justifyContent(FlexAlign.Center)
  }
}

效果

PixPin_2024-12-24_11-22-25

拦截器

拦截器可以分成 2 种,局部拦截器和全局拦截器

标记在实现了IHMInterceptor的对象上,声明此对象为一个拦截器

  • interceptorName: string, 拦截器名称,必填
  • priority: number, 拦截器优先级,数字越大优先级越高,非必填,默认为 9;
  • global: boolean, 是否为全局拦截器,当配置为 true 时,所有跳转均过此拦截器;默认为 false,当为 false 时需要配置在@HMRouter 的 interceptors 中才生效。

执行时机:

在路由栈发生变化前,转场动画发生前进行回调。 1.当发生 push/replace 路由时,pageUrl 为空时,拦截器不会执行,需传入 pageUrl 路径;

2.当跳转 pageUrl 目标页面不存在时,执行全局以及发起页面拦截器,当拦截器未执行 DO_REJECT 时,然后执行路由的 onLost 回调

3.当跳转 pageUrl 目标页面存在时,执行全局,发起页面和目标页面的拦截器;

拦截器执行顺序:

  1. 按照优先级顺序执行,不区分自定义或者全局拦截器,优先级相同时先执行@HMRouter 中定义的自定义拦截器
  2. 当优先级一致时,先执行 srcPage>targetPage>global

srcPage 表示跳转发起页面。

targetPage 表示跳转结束时展示的页面。

局部拦截器

局部拦截器只对单个页面生效。我们拿 登录页面来测试 从首页 跳转到登录页面,登录页面的拦截器便会触发

entry/src/main/ets/views/LoginPage.ets

定义拦截器

@HMInterceptor({ interceptorName: "Loginterceptor", global: false })
export class JumpInfoInterceptor implements IHMInterceptor {
  handle(info: HMInterceptorInfo): HMInterceptorAction {
    console.log("拦截器", JSON.stringify(info));
    // DO_NEXT  正常跳转
    // DO_REJECT 拒绝跳转
    return HMInterceptorAction.DO_NEXT;
  }
}

使用拦截器

// 使用拦截器
@HMRouter({
  pageUrl: 'LoginPage',
  interceptors: ['Loginterceptor']
})
@Component
export struct LoginPage {
  build() {
    Column() {
      Button('登录页面')
    }
    .width("100%")
    .height("100%")
    .justifyContent(FlexAlign.Center)
  }
}

输出效果

{
  "srcName": "HM_NavBar",
  "targetName": "LoginPage",
  "type": "push",
  "routerPathInfo": {
    "pageUrl": "LoginPage"
  },
  "context": {
    "instanceId_": 100000
  },
  "isSrc": false
}

全局拦截器

直接在 index 页面上使用

  aboutToAppear(): void {
    // 注册全局拦截器
    HMRouterMgr.registerGlobalInterceptor({
      interceptorName: "拦截器的名字",
      // 优先级
      priority: 1,
      // 拦截器
      interceptor: {
        // 处理函数
        handle(info: HMInterceptorInfo) {
          return HMInterceptorAction.DO_NEXT
        }
      }
    })
  }

生命周期

@HMLifecycle(lifecycleName, priority, global)

标记在实现了 IHMLifecycle 的对象上,声明此对象为一个自定义生命周期处理器

  • lifecycleName: string, 自定义生命周期处理器名称,必填
  • priority: number, 生命周期优先级,数字越大优先级越高,非必填,默认为 9;
  • global: boolean, 是否为全局生命周期,当配置为 true 时,所有页面生命周期事件会转发到此对象;默认为 false

生命周期触发顺序:

按照优先级顺序触发,不区分自定义或者全局生命周期,优先级相同时先执行@HMRouter 中定义的自定义生命周期

我们可以继续在登录页面上测试对应的生命周期

entry/src/main/ets/views/LoginPage.ets

定义生命周期

@HMLifecycle({ lifecycleName: 'LoginLifecycle' })
export class PageDurationLifecycle implements IHMLifecycle {
  private time: number = 0;

  onShown(ctx: HMLifecycleContext): void {
    this.time = new Date().getTime();
    console.log("生命周期", JSON.stringify(ctx))
  }

  onHidden(ctx: HMLifecycleContext): void {
    const duration = new Date().getTime() - this.time;
  }
}

使用生命周期


// 使用拦截器
@HMRouter({
  pageUrl: 'LoginPage',
  interceptors: ['Loginterceptor'],
  lifecycle: "LoginLifecycle"
})
@Component
export struct LoginPage {
  build() {
    Column() {
      Button('登录页面')
    }
    .width("100%")
    .height("100%")
    .justifyContent(FlexAlign.Center)
  }
}

完整生命周期

export interface HMLifecycleContext {
    uiContext: UIContext;
    navContext?: NavDestinationContext;
}
export type HMLifecycleCallback = (ctx: HMLifecycleContext) => boolean | void;
export interface IHMLifecycle {
    onPrepare?(ctx: HMLifecycleContext): void;
    onAppear?(ctx: HMLifecycleContext): void;
    onDisAppear?(ctx: HMLifecycleContext): void;
    onShown?(ctx: HMLifecycleContext): void;
    onHidden?(ctx: HMLifecycleContext): void;
    onWillAppear?(ctx: HMLifecycleContext): void;
    onWillDisappear?(ctx: HMLifecycleContext): void;
    onWillShow?(ctx: HMLifecycleContext): void;
    onWillHide?(ctx: HMLifecycleContext): void;
    onReady?(ctx: HMLifecycleContext): void;
    onBackPressed?(ctx: HMLifecycleContext): boolean;
}

页面组件和生命周期数据交互

生命周期实例中可以初始化对象,并且在UI组件中获取做为状态变量

import {
  HMInterceptor,
  HMInterceptorAction,
  HMInterceptorInfo,
  HMLifecycle,
  HMLifecycleContext,
  HMRouter,
  HMRouterMgr,
  IHMInterceptor,
  IHMLifecycle
} from '@hadss/hmrouter';

// 定义拦截器
@HMInterceptor({ interceptorName: 'Loginterceptor', global: false })
export class JumpInfoInterceptor implements IHMInterceptor {
  handle(info: HMInterceptorInfo): HMInterceptorAction {
    console.log("拦截器", JSON.stringify(info))
    return HMInterceptorAction.DO_NEXT;
  }
}

@Observed
export class ObservedModel {
  isLoad: boolean = false;
}

@HMLifecycle({ lifecycleName: 'LoginLifecycle' })
export class PageDurationLifecycle implements IHMLifecycle {
  model: ObservedModel = new ObservedModel()

  onAppear(ctx: HMLifecycleContext): void {
  }
}


// 使用拦截器
@HMRouter({
  pageUrl: 'LoginPage',
  interceptors: ['Loginterceptor'],
  lifecycle: "LoginLifecycle"
})
@Component
export struct LoginPage {
  @State model: ObservedModel | null =
    (HMRouterMgr.getCurrentLifecycleOwner()?.getLifecycle() as PageDurationLifecycle).model;

  build() {
    Column() {
      Button('登录页面' + this.model?.isLoad)
        .onClick(() => {
          this.model!.isLoad = !this.model?.isLoad
        })

    }
    .width("100%")
    .height("100%")
    .justifyContent(FlexAlign.Center)
  }
}

小结

hmrouter 的官网文档还是挺零散的,需要结合文档配套学习使用

HMRouter 接口和属性列表

查看详情

HMRouterPlugin 编译插件使用说明

查看详情

HMRouterTransitions 高阶转场动画使用说明

查看详情

自定义模板使用说明

查看详情

自定义转场动画使用说明

查看详情

原生到原生页面跳转场景解决方案

查看详情

SampleCode

查看详情

更多示例

查看详情

FAQ

查看详情

原理介绍

查看详情


万少
66 声望5 粉丝