小白必看 HarmonyOS Next HMRouter 轻松上手秘籍 原创
小白必看 HarmonyOS Next HMRouter 轻松上手秘籍
前言
HMRouter 作为 HarmonyOS 的页面跳转场景解决方案,聚焦解决应用内原生页面的跳转逻辑。
HMRouter 底层对系统 Navigation 进行封装,集成了 Navigation、NavDestination、NavPathStack 的系统能力,提供了可复用的路由拦截、页面生命周期、自定义转场动画,并且在跳转传参、额外的生命周期、服务型路由方面对系统能力进行了扩展。
目的是让开发者在开发过程中无需关注 Navigation、NavDestination 容器组件的相关细节及模板代码,屏蔽跳转时的判断逻辑,降低拦截器、自定义转场动画实现复杂度,更好的进行模块间解耦
对比
目前鸿蒙应用开发中,官方推出的路由方案有两个,分别是Router和Navigation。目前官方主要推荐的也是 Navigation。
业务场景 | Navigation | Router |
---|---|---|
一多能力 | 支持,Auto 模式自适应单栏跟双栏显示 | 不支持 |
跳转指定页面 | pushPath & pushDestination | pushUrl & pushNameRoute |
跳转 HSP 中页面 | 支持 | 支持 |
跳转 HAR 中页面 | 支持 | 支持 |
跳转传参 | 支持 | 支持 |
获取指定页面参数 | 支持 | 不支持 |
传参类型 | 传参为对象形式 | 传参为对象形式,对象中暂不支持方法变量 |
跳转结果回调 | 支持 | 支持 |
跳转单例页面 | 支持 | 支持 |
页面返回 | 支持 | 支持 |
页面返回传参 | 支持 | 支持 |
返回指定路由 | 支持 | 支持 |
页面返回弹窗 | 支持,通过路由拦截实现 | showAlertBeforeBackPage |
路由替换 | replacePath & replacePathByName | replaceUrl & replaceNameRoute |
路由栈清理 | clear | clear |
清理指定路由 | 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
动态共享包模块
- 工程的目录名称是 study
- 入口模块是 entry
- cart 是 hsp 模块
配置环境
使用 ohpm 安装依赖
ohpm install @hadss/hmrouter
ohpm install @hadss/hmrouter-transitions
编译插件配置
-
修改工程的
hvigor/hvigor-config.json
文件,加入路由编译插件{ "dependencies": { "@hadss/hmrouter-plugin": "^1.0.0-rc.10" // 使用npm仓版本号 } // ...其他配置 }
-
在使用到 HMRouter 的模块中引入路由编译插件,修改
hvigorfile.ts
我们项目的模块无非是 Hap、Har 和 Hsp。对应你当前的模块是哪种类型,就使用对应的写法
-
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标签的模块均需要配置,与模块类型保持一致 };
-
Har
import { harTasks } from "@ohos/hvigor-ohos-plugin"; import { harPlugin } from "@hadss/hmrouter-plugin"; export default { system: harTasks, plugins: [harPlugin()], // 使用HMRouter标签的模块均需要配置,与模块类型保持一致 };
-
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%')
}
}
模块内跳转
我们先演示跳转到当前模块中的某个页面。
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/components
为 entry/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" });
});
效果
跳转动画
我们可以在跳转页面的时候来指定跳转动画
分类两个步骤
- 定义动画
- 使用动画
定义动画
假设 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)
}
}
效果
拦截器
拦截器可以分成 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 目标页面存在时,执行全局,发起页面和目标页面的拦截器;
拦截器执行顺序:
- 按照优先级顺序执行,不区分自定义或者全局拦截器,优先级相同时先执行@HMRouter 中定义的自定义拦截器
- 当优先级一致时,先执行 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 的官网文档还是挺零散的,需要结合文档配套学习使用