Skip to content

HMRouter 简介

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

HMRouter 底层对系统 Navigation 进行封装,封装了 Navigation、NavDestination、NavPathStack 的能力,提供了可复用的路由拦截、自定义转场动画、页面生命周期框架,目的是让开发者在开发中无需关注 Navigation、NavDestination 容器组件的相关细节及模板代码,更好的进行模块间解耦。

特性

  • 基于注解声明路由信息

  • 支持 Har、Hsp、Hap

  • 支持 Navigation 路由栈嵌套

  • 支持路由拦截器(包含全局拦截、单页面拦截、跳转时一次性拦截)

  • 支持生命周期回调(包含全局生命周期、单页面生命周期、跳转时一次性生命周期)

  • 内置转场动画(页面、Dialog),可配置方向、透明度、缩放,支持跟手抽屉式转场动画,同时支持配置某个页面的转场动画、跳转时的一次性动画

  • 支持 Dialog 类型页面、支持单例页面

依赖版本

HarmonyOS NEXT Beta3 及以上

使用方法

1. 打开允许权限

bash
# 在模块的module.json5文件中配置网络权限

{
  "module":{
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET"
      }
    ],
  }
}

2. 安装依赖

bash
ohpm install @hadss/hmrouter

3. 使用配置

  • (1) 修改项目的 hvigor/hvigor-config.json 文件,加入路由编译插件(根目录)
js
{
  "dependencies": {
    "@hadss/hmrouter-plugin": "^1.0.0-rc.3" // 你自己的版本号必须和oh-package.json5的版本号一致
  },
  // ...其他配置
}
  • (2) 在模块中引入路由编译插件,修改 hvigorfile.ts

这里的文件是在模块下面 就是 Entry 下面的hvigorfile.ts

而不是根目录下的hvigorfile.ts

js
import { hapTasks } from "@ohos/hvigor-ohos-plugin";
import { hapPlugin } from "@hadss/hmrouter-plugin";

export default {
  system: hapTasks,
  plugins: [hapPlugin()],
};

INFO

如果是 Har 则使用 harPlugin(), Hsp 则使用 hspPlugin()

初始化

  • (1) 在模块的 entry/src/main/ets>entryability>entryAbility.ets 中初始化 HMRouter
js
import { HMRouterMgr } from "@hadss/hmrouter";

// 初始化 HMRouter
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
    HMRouterMgr.init({
      context: this.context
    })
  }
  • (2) 工程目录下的 build-profile.json5 中,配置 useNormalizedOHMUrl 属性为 true
js
{
  "app": {
    "products": [
      {
        "name": "default",
        "signingConfig": "default",
        "compatibleSdkVersion": "5.0.0(12)",
        "runtimeOS": "HarmonyOS",
        "buildOption": {
          "strictMode": {
            "useNormalizedOHMUrl": true
          }
        }
      }
    ],
    // ...其他配置
  }
}

初级使用

(1) 创建三个页面

  1. Index.ets (路由入口页面)

  2. Index2.ets (跳转页面)

  3. Index3.ets (跳转页面)

(2) Index.ets 路由入口页面

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

@Entry
@Component
struct Index {
  @State message: string = 'Hello World';
  modifier: AppNavModifier = new AppNavModifier();

  build() {
    Column() {
      // 使用HMNavigation容器
      HMNavigation({
        navigationId: 'AppNavigation', options: {
          standardAnimator: HMDefaultGlobalAnimator.STANDARD_ANIMATOR,
          dialogAnimator: HMDefaultGlobalAnimator.DIALOG_ANIMATOR,
          modifier: this.modifier
        }
      }){
        Button("点击跳转到2").fontSize(62).onClick(()=>{
          HMRouterMgr.push({
            pageUrl:'Index2'
          })
        })
      }
    }
    .height('100%')
    .width('100%')
  }
}

class AppNavModifier extends AttributeUpdater<NavigationAttribute> {
  initializeModifier(instance: NavigationAttribute): void {
    instance.mode(NavigationMode.Stack);
    instance.navBarWidth('100%');
    instance.hideTitleBar(true);
    instance.hideToolBar(true);
  }
}

(3) Index2.ets 跳转页面(页面必须导出否则报错)

js
import { HMRouter, HMRouterMgr } from '@hadss/hmrouter';
// 必须导出 必须里面写pageUrl
@HMRouter({pageUrl:'Index2'})
@Entry
@Component
export struct Index2 {
  build() {
    Column(){
      Button("点击2").type(ButtonType.Capsule).onClick(()=>{
         HMRouterMgr.push({
           navigationId:'AppNavigation',
           pageUrl:'Index3'
         })
      })
    }
  }
}

(4) Index3.ets 跳转页面(页面必须导出否则报错)

js
import { HMRouter, HMRouterMgr } from '@hadss/hmrouter'

@HMRouter({pageUrl:'Index3'})
@Entry
@Component
 export struct Index3 {
  build() {
    Column(){
      Button("点击3").type(ButtonType.Capsule).onClick(()=>{
        console.log("点击了我3")
        HMRouterMgr.push({
          navigationId:'AppNavigation',
          pageUrl:'Index2'
        })
      })
    }
  }
}

跳转

push

  • 压入栈,如果栈中存在则不压入
js
HMRouterMgr.push(
  {
    navigationId: "AppNavigation", // 操作页面栈(你自己的ID),当navigationId为空时,表示对最近一次操作的navigation进行路由跳转
    pageUrl: "Index2", // 需要跳转的目标页面,push/replace操作必填参数
    param: {
      data: { name: "第一个页面过来的数据", id: 1 },
    }, //跳转页面携带参数,push/replace表示传递给下个页面的参数,pop表示回传给上一个页面的返回参数
    interceptors: [], // IHMInterceptor[] 自定义拦截器,最高优先级执行
    animator: false, // IHMAnimator | boolean 自定义动画,使用传入的动画进行专场,不再使用原先定义的转场动画,传入false时将禁用动画
    skipAllInterceptor: false, //  // 是否跳过所有拦截器执行,不能跳过传入的interceptors拦截器
  },
  {
    // 页面返回回调,最好用箭头函数因为要改变外面的状态值
    onResult: (popInfo: HMPopInfo) => {
      console.log(JSON.stringify(popInfo));
    },
    // // 目标页面跳转完成回调
    onArrival: () => {
      console.log("页面跳转过去了,已经完成了");
    },
    // 目标页面找不到回调
    onLost: () => {
      console.log("没有这个页面啊");
    },
  }
);
  • 接收参数
js
this.param = HMRouterMgr.getCurrentParam();
Text(this.param).fontSize(62);

replace

参数同 push 只不过他是替换栈顶的页面

pop(不一样)

如需使用页面返回功能,在对应的业务逻辑位置使用 HMRouterMgr 提供的 pop 方法实现页面返回,同样的 pop 方法支持传入 navigationId,同时 HMRouter 还支持在返回时通过配置 param 参数向其所返回的页面传递参数。

  • 他接收两个参数一个就是返回页面的信息

  • 另外那个是层级(本人测试没有效果)

js
//  @param pathInfo
//  @param skipedLayerNumber 页面返回的层级数量,默认为0,表示返回上一级,1表示跳过一级页面返回,
//  即同时两个页面出栈,pathInfo.pathInfo参数优先级高于skipedLayerNumber
HMRouterMgr.pop({
  navigationId: "mainNavigationId",
  pageUrl: "HomePage",
  param: this.param,
});
  • pop 接收参数 一定要写到 onResult 回调函数里面 它里面在改变状态
js
 Button("点击2").type(ButtonType.Capsule).onClick(() => {
    console.debug("测试路由", JSON.stringify(this))
    HMRouterMgr.push({
      navigationId: 'AppNavigation',
      pageUrl: 'Index3'
    }, {
      // 页面返回回调
      onResult: (popInfo: HMPopInfo) => {
        console.debug("测试路由", JSON.stringify(this))
        console.debug("测试路由", JSON.stringify(popInfo.result))
        this.routerId = (popInfo.result as ParamsType).id;
        console.debug("测试路由", this.routerId)
        if (this.routerId == 4) {
          console.debug("测试路由", "this.routerId确实等于3")
          console.debug("测试路由结果", JSON.stringify(this))
          this.flag = true;
          this.name = (popInfo.result as ParamsType).name;
        }
      },
      // // 目标页面跳转完成回调
      onArrival() {
        console.log("页面跳转过去了,已经完成了")
      },
      // 目标页面找不到回调
      onLost() {
        console.log("没有这个页面啊")
      }
    })
  })
  • 多次页面跳转返回指定页面

当页面跳转路径如 HomePage->PageA->PageB->PageC,开发者希望在 PageC 的页面逻辑中直接返回到 HomePage 并携带参数,开发者仅需使用 HMRouterMgr 提供的 pop 方法,传入要返回目标页面的 pageUrl、传递的参数 param,即可直接带参返回到指定页面。

js
HMRouterMgr.pop({
  navigationId: "mainNavigationId",
  pageUrl: "HomePage",
  param: this.param,
});

图片

生命周期

(1) 创建生命周期钩子函数

在 entry>src>main>etc 下面新建一个 Lifecycle 文件夹,在 Lifecycle 文件夹下面新建一个 Lifecycle.ets 文件,Lifecycle.ets 文件内容如下:

js
import { HMLifecycle, HMLifecycleContext, IHMLifecycle } from "@hadss/hmrouter";

@HMLifecycle({ lifecycleName: "RouterLife" })
export class RouterLife implements IHMLifecycle {
  onPrepare(ctx: HMLifecycleContext): void {
    console.debug("测试路由生命周期", "RouterLife", "onPrepare");
    console.debug(
      "测试路由生命周期",
      "RouterLife",
      "onPrepare",
      JSON.stringify(ctx)
    );
  }

  onAppear(ctx: HMLifecycleContext): void {
    console.debug("测试路由生命周期", "RouterLife", "onAppear");
    console.debug(
      "测试路由生命周期",
      "RouterLife",
      "onAppear",
      JSON.stringify(ctx)
    );
  }

  onDisAppear(ctx: HMLifecycleContext): void {
    console.debug("测试路由生命周期", "RouterLife", "onDisAppear");
    console.debug(
      "测试路由生命周期",
      "RouterLife",
      "onDisAppear",
      JSON.stringify(ctx)
    );
  }

  onShown(ctx: HMLifecycleContext): void {
    console.debug("测试路由生命周期", "RouterLife", "onShown");
    console.debug(
      "测试路由生命周期",
      "RouterLife",
      "onShown",
      JSON.stringify(ctx)
    );
  }

  onHidden(ctx: HMLifecycleContext): void {
    console.debug("测试路由生命周期", "RouterLife", "onHidden");
    console.debug(
      "测试路由生命周期",
      "RouterLife",
      "onHidden",
      JSON.stringify(ctx)
    );
  }

  onWillAppear(ctx: HMLifecycleContext): void {
    console.debug("测试路由生命周期", "RouterLife", "onWillAppear");
    console.debug(
      "测试路由生命周期",
      "RouterLife",
      "onWillAppear",
      JSON.stringify(ctx)
    );
  }

  onWillDisappear(ctx: HMLifecycleContext): void {
    console.debug("测试路由生命周期", "RouterLife", "onWillDisappear");
    console.debug(
      "测试路由生命周期",
      "RouterLife",
      "onWillDisappear",
      JSON.stringify(ctx)
    );
  }

  onWillShow(ctx: HMLifecycleContext): void {
    console.debug("测试路由生命周期", "RouterLife", "onWillShow");
    console.debug(
      "测试路由生命周期",
      "RouterLife",
      "onWillShow",
      JSON.stringify(ctx)
    );
  }

  onWillHide(ctx: HMLifecycleContext): void {
    console.debug("测试路由生命周期", "RouterLife", "onWillHide");
    console.debug(
      "测试路由生命周期",
      "RouterLife",
      "onWillHide",
      JSON.stringify(ctx)
    );
  }

  onReady(ctx: HMLifecycleContext): void {
    console.debug("测试路由生命周期", "RouterLife", "onReady");
    console.debug(
      "测试路由生命周期",
      "RouterLife",
      "onReady",
      JSON.stringify(ctx)
    );
  }

  onBackPressed(ctx: HMLifecycleContext): boolean {
    console.debug("测试路由生命周期", "RouterLife", "onBackPressed");
    console.debug(
      "测试路由生命周期",
      "RouterLife",
      "onBackPressed",
      JSON.stringify(ctx)
    );
    return false;
  }
}

(2) 页面上使用生命周期函数

  • 在需要的页面中引入生命周期

我这里拿一个基础页面举例子,要在 HMrouter 里面加入 lifecycle,名字就是你定义的生命周期类名

js
import { HMLifecycle, HMPopInfo, HMRouter, HMRouterMgr } from '@hadss/hmrouter';


// 必须导出 必须里面写pageUrl
@HMRouter({ pageUrl: 'Index2', lifecycle: 'RouterLife' })
@Entry
@Component
export struct Index2 {
  @State routerId: number = 0;
  @State flag: boolean = false;
  @State name: string = ""

  build() {
    Column() {
      if (this.flag) {
        Text("收到了返回值").fontSize(67)
        Text(this.name).fontSize(72)
      }
      Button("点击2").type(ButtonType.Capsule).onClick(() => {
        console.debug("测试路由", JSON.stringify(this))
        HMRouterMgr.push({
          navigationId: 'AppNavigation',
          pageUrl: 'Index3'
        }, {
          // 页面返回回调
          onResult: (popInfo: HMPopInfo) => {
            console.debug("测试路由", JSON.stringify(this))
            console.debug("测试路由", JSON.stringify(popInfo.result))
            this.routerId = (popInfo.result as ParamsType).id;
            console.debug("测试路由", this.routerId)
            if (this.routerId == 4) {
              console.debug("测试路由", "this.routerId确实等于3")
              console.debug("测试路由结果", JSON.stringify(this))
              this.flag = true;
              this.name = (popInfo.result as ParamsType).name;
            }
          },
          // // 目标页面跳转完成回调
          onArrival() {
            console.log("页面跳转过去了,已经完成了")
          },
          // 目标页面找不到回调
          onLost() {
            console.log("没有这个页面啊")
          }
        })
      })
    }
  }
}

interface ParamsType {
  id: number,
  name: string,
}

(3) 结果

图片

拦截器

创建拦截器

  • (1) 在 entry>src>main>stc>model(你自己起的一个文件夹)新建 Interceptor.ets 文件

INFO

HMInterceptor 是拦截器装饰器 里面有三个参数

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

我在下面创建了 2 个拦截器 它优先执行 priority 为 9 的,在执行 priority 为 7 的

js
import { HMInterceptor, HMInterceptorAction, HMInterceptorInfo, HMRouterMgr, IHMInterceptor } from '@hadss/hmrouter';

@HMInterceptor({priority:9,interceptorName:'LoginInterceptor'})

export class LoginInterceptor implements IHMInterceptor {
  private isLogin: boolean = true;

  handle(info: HMInterceptorInfo): HMInterceptorAction {
    if (this.isLogin) {
      // 跳转下一个拦截器处理
      console.debug("测试拦截",'拦截器1','拦截住了');
      return HMInterceptorAction.DO_NEXT;
    } else {
      HMRouterMgr.push({
        pageUrl: 'Index4',
        param: { targetUrl: info.targetName },
        skipAllInterceptor: true
      })
      // 拦截结束,不再执行下一个拦截器,不再执行相关转场和路由栈操作
      return HMInterceptorAction.DO_REJECT;
    }
  }
}

@HMInterceptor({priority:7,interceptorName:'LoginInterceptor2'})
export class LoginInterceptor2 implements IHMInterceptor {
  private isLogin2: boolean = true;

  handle(info: HMInterceptorInfo): HMInterceptorAction {
    if (this.isLogin2) {
      // 跳转下一个拦截器处理
      console.debug("测试拦截",'拦截器2','拦截住了');
      return HMInterceptorAction.DO_NEXT;
    } else {
      HMRouterMgr.push({
        pageUrl: 'Index4',
        param: { targetUrl: info.targetName },
        skipAllInterceptor: true
      })
      // 拦截结束,不再执行下一个拦截器,不再执行相关转场和路由栈操作
      return HMInterceptorAction.DO_REJECT;
    }
  }
}

图片

动画

全局动画

  • (1) 创建自定义动画 entry>src>main>ets>model(自己定义的文件夹)>(新建的 HMAnimator)
js
import { IHMAnimator } from "@hadss/hmrouter";

export const globalPageTransitionEffect: IHMAnimator.Effect =
  new IHMAnimator.Effect({
    direction: IHMAnimator.Direction.BOTTOM_TO_TOP,
    opacity: { opacity: 0.5 },
    scale: { x: 0.5, y: 0.2 },
  });
  • (2) 挂载到全局
js
import { globalPageTransitionEffect } from "../model/HMAnimator";
HMNavigation({
  navigationId: "AppNavigation",
  options: {
    standardAnimator: globalPageTransitionEffect,
    dialogAnimator: HMDefaultGlobalAnimator.DIALOG_ANIMATOR,
    modifier: this.modifier,
  },
});

特殊页面 自定义动画

开发者可以自定义动画类并实现 IHMAnimator 接口中的 effect 方法

该方法会将页面进出场的效果对象 enterHandle 与 exitHandle 作为参数传入

可通过参数对象上的 start、finish 方法,设置对应效果的起止状态,支持设置的常用属性还有:

  • curve:设置动画速度曲线,支持通过 Curve 枚举传入值,默认 Curve.EaseInOut。

  • duration:动画持续时长,单位 ms。

INFO

start/finish 方法参数说明如下:

  • translateOption:坐标位置,以屏幕左上角为原点,水平向右为 x 轴正方向,竖直向下为 y 轴正方向。百分比相对于屏幕宽度。例如希望从右侧进入可以设置 translateOption.x 从 100%变到 0。

  • scaleOption:页面缩放,可通过 scaleOption.x、scaleOption.y 单独设置横纵方向的缩放比例。

  • opacityOption:跳转页面的透明度

以下代码示例表示入场时由屏幕底部以线性速度向屏幕顶部运动,入场动画持续时长为 3000ms。

出场时从屏幕顶部以线性速度向屏幕底部运动,出场动画持续时长也为 3000ms。

  • 流程

(1) 创建自定义动画 entry>src>main>ets>model(自己定义的文件夹)>(新建的 HMCustomAnimator)

js
import {
  HMAnimator,
  HMAnimatorHandle,
  IHMAnimator,
  OpacityOption,
  ScaleOption,
  TranslateOption,
} from "@hadss/hmrouter";

@HMAnimator({ animatorName: "CustomAnimator" })
export class CustomAnimator implements IHMAnimator {
  effect(enterHandle: HMAnimatorHandle, exitHandle: HMAnimatorHandle): void {
    // 入场动画
    enterHandle.start(
      (
        translateOption: TranslateOption,
        scaleOption: ScaleOption,
        opacityOption: OpacityOption
      ) => {
        translateOption.y = "100%";
        scaleOption.x = 0.7;
        opacityOption.opacity = 0.3;
      }
    );
    enterHandle.finish(
      (
        translateOption: TranslateOption,
        scaleOption: ScaleOption,
        opacityOption: OpacityOption
      ) => {
        translateOption.y = "0";
        scaleOption.x = 1;
        opacityOption.opacity = 1;
      }
    );
    enterHandle.duration = 3000;
    enterHandle.curve = Curve.Linear;

    // 出场动画
    exitHandle.start(
      (
        translateOption: TranslateOption,
        scaleOption: ScaleOption,
        opacityOption: OpacityOption
      ) => {
        translateOption.y = "0";
        scaleOption.x = 1;
        opacityOption.opacity = 1;
      }
    );
    exitHandle.finish(
      (
        translateOption: TranslateOption,
        scaleOption: ScaleOption,
        opacityOption: OpacityOption
      ) => {
        translateOption.y = "100%";
        scaleOption.x = 0.7;
        opacityOption.opacity = 0.3;
      }
    );
    exitHandle.duration = 3000;
    enterHandle.curve = Curve.Linear;
  }
}

(2) 要跳转到的页面使用

js
import { CustomAnimator } from "../model/HMCustomAnimator";
Button("点击3")
  .type(ButtonType.Capsule)
  .onClick(() => {
    console.log("点击了我3");
    HMRouterMgr.push({
      navigationId: "AppNavigation",
      pageUrl: "Index4",
      animator: new CustomAnimator(),
    });
  });

依据条件来执行不同的动画

  • 比如说横屏一个动画 效果 竖屏一个动画效果

简单来说就是通过判断,横屏一个自定义动画,竖屏就走另外一个动画

js
@Component
export struct CommentInput {
  // ...
  build() {
    Row() {
      // ...
      Image($r('app.media.icon_comments'))
        .width(24)
        .height(24)
        .margin({ right: 16 })
        .onClick(() => {
          if (this.isLandscape) {
            HMRouterMgr.push({
              navigationId: this.queryNavigationInfo()?.navigationId,
              pageUrl: 'liveComments',
              param: {
                commentRenderNode: this.commentRenderNode,
              },
              animator: myAnimator2
            }, {
              onResult: (paramInfo: PopInfo) => {
                this.videoWidth = '100%';
              }
            })
            this.videoWidth = '50%';
          } else {
            HMRouterMgr.push({
              navigationId: this.queryNavigationInfo()?.navigationId,
              pageUrl: 'liveComments',
              param: {
                commentRenderNode: this.commentRenderNode,
              },
              animator: myAnimator1
            }, {
              onResult: (paramInfo: PopInfo) => {
                this.videoHeight = '100%'
              }
            })
            this.videoHeight = '30%'
          }
        })//
        })// ...
    }
    // ...
  }
}