【鸿蒙开发】HMRouter一款和好用的管理路由三方工具
HMRouter
HMRouter作为应用内页面跳转场景解决方案,为开发者提供了功能完备、高效易用的路由框架。
HMRouter底层对系统Navigation进行封装,集成了Navigation、NavDestination、NavPathStack的系统能力,提供了可复用的路由拦截、页面生命周期、自定义转场动画,并且在跳转传参、额外的生命周期、服务型路由方面对系统能力进行了扩展,同时开发者可以高效的将历史代码中的Navigation组件接入到HMRouter框架中。
目的是让开发者在开发过程中减少模板代码,降低拦截器、自定义转场动画、组件感知页面生命周期等高频开发场景的实现复杂度,帮助开发者更好的实现路由与业务模块间的解耦。
特性
- 基于注解声明路由信息(普通页面、Dialog页面、单例页面)
- 注解参数支持使用字符串常量定义
- 页面路径支持正则匹配
- 支持在Har、Hsp、Hap中使用
- 支持Navigation路由栈嵌套
- 支持服务型路由
- 跳转时支持标准URL解析
- 支持路由拦截器(包含全局拦截、单页面拦截、跳转时一次性拦截)
- 支持生命周期回调(包含全局生命周期、单页面生命周期、NavBar生命周期)
- 内置转场动画(普通页面、Dialog),支持交互式转场动画,同时支持配置某个页面的转场动画、跳转时的一次性动画
- 提供更多高阶转场动画,包括一镜到底等(需依赖
@hadss/hmrouter-transitions) - 支持配置自定义页面模版,可以更灵活的生成页面文件
- 支持混淆白名单自动配置
- 支持与系统Navigation/NavDestination组件混用
gitee链接:HMRouter: 实现HMRouter的基本功能
使用
1. 安装依赖
使用 ohpm 安装
# 安装路由框架核心库
ohpm install @hadss/hmrouter
# 如需高级转场动画,安装转场动画库
ohpm install @hadss/hmrouter-transitions
2. 配置编译插件
依赖配置
修改工程根目录下的hvigor/hvigor-config.json 文件,加入路由编译插件
{
"dependencies": {
"@hadss/hmrouter-plugin": "latest" // 使用npm仓版本号
},
}
插件配置
修改工程根目录下的hvigorfile.ts,使用路由编译插件
import { appTasks } from '@ohos/hvigor-ohos-plugin';
import { appPlugin } from "@hadss/hmrouter-plugin";
export default {
system: appTasks,
plugins: [appPlugin({ ignoreModuleNames: [ /** 不需要扫描的模块 **/ ] })]
};
3. 初始化路由框架
在EntryAbility中初始化路由框架
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
// 日志开启需在init之前调用,否则会丢失初始化日志
HMRouterMgr.openLog("INFO")
HMRouterMgr.init({
context: this.context
})
}
}
这里必须进行初始化
4. 定义路由入口
HMRouter 依赖系统 Navigation 能力,所以必须在页面中定义一个 HMNavigation 容器,并设置相关参数,具体代码如下:
@Entry
@Component
export struct Index {
modifier: MyNavModifier = new MyNavModifier();
build() {
Column(){
// 使用HMNavigation容器
HMNavigation({
navigationId: 'MainNavigation', homePageUrl: 'HomePage',
options: {
standardAnimator: HMDefaultGlobalAnimator.STANDARD_ANIMATOR,// 标准页面的全局转场动画
dialogAnimator: HMDefaultGlobalAnimator.DIALOG_ANIMATOR, // 对话框全局转场动画
modifier: this.modifier // Navigation组件的属性修改器
}
})
}
.height('100%')
.width('100%')
}
}
class MyNavModifier extends AttributeUpdater<NavigationAttribute> {
// AttributeUpdater首次设置给组件时提供的样式
initializeModifier(instance: NavigationAttribute): void {
instance.hideNavBar(true); // 隐藏导航栏
}
}
这里默认不变即可,想实现其他功能可参考官网的属性配置
5. 页面定义与路由跳转
使用 @HMRouter 标签定义页面并跳转
定义HomePage
import { HMRouter, HMRouterMgr } from "@hadss/hmrouter"
const PAGE_URL: string = 'HomePage'
@HMRouter({ pageUrl: PAGE_URL })
@Component
export struct HomePage {
build() {
Column() {
Text('HomePage')
Button('Push')
.onClick(() => {
HMRouterMgr.push({ pageUrl: 'PageB',param:`123456` })
})
}
}
}
这里官网是通过HMRouterMgr.push({ pageUrl: 'PageB?msg=abcdef' })传递参数的,
自测试获取到的值为undefined,强烈避坑
Page页面,并且接收参数
import { HMRouter, HMRouterMgr } from '@hadss/hmrouter'
import { JSON } from '@kit.ArkTS'
@HMRouter({ pageUrl: 'PageB' })
@Component
export struct PageB {
@State param: Object | null = HMRouterMgr.getCurrentParam()
build() {
Column() {
Text('PageB')
Text(`${JSON.stringify(this.param)}`)
Button('popWithResult')
.onClick(() => {
HMRouterMgr.pop({ param: { 'resultFrom': 'PageB' } })
})
}
}
}
现在我们已经能实现初步的页面跳转了
![]()
6.拦截器的使用
拦截器流程简图
首页(HomePage) → 点击跳转 PageB → 拦截器检查 isLogin
│
┌─────────────────┼─────────────────┐
▼ │ ▼
未登录 → 去登录页(LoginPage) 已登录 → 进入 PageB
│
▼
点击登录 → 写 isLogin=true → 再跳 PageB → 进入 PageB
代码实现
EntryAbility中设置全局登录状态
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
// 初始化全局登录状态,供拦截器与登录页使用
AppStorage.setOrCreate('isLogin', false);
}
}
完善Index,添加基本功能
import { HMDefaultGlobalAnimator, HMNavigation } from '@hadss/hmrouter';
import { AttributeUpdater } from '@kit.ArkUI';
import { HomePage } from '../view/home/HomePage';
import { MyPage } from '../view/my/MyPage';
interface TabItem {
text: string
normal: ResourceStr
active: ResourceStr
}
@Entry
@Component
export struct Index {
modifier: MyNavModifier = new MyNavModifier();
@State currentIndex: number = 0
@Provide isLogin: boolean = false
list: TabItem[] = [
{ text: '首页', normal: $r('app.media.startIcon'), active: $r('app.media.startIcon') },
{ text: '商城', normal: $r('app.media.startIcon'), active: $r('app.media.startIcon') },
{ text: '购物 ', normal: $r('app.media.startIcon'), active: $r('app.media.startIcon') },
{ text: '活动', normal: $r('app.media.startIcon'), active: $r('app.media.startIcon') },
{ text: '我的', normal: $r('app.media.startIcon'), active: $r('app.media.startIcon') },
]
build() {
Column() {
HMNavigation({
navigationId: 'MainNavigation',
homePageUrl: 'Index',
options: {
standardAnimator: HMDefaultGlobalAnimator.STANDARD_ANIMATOR,
dialogAnimator: HMDefaultGlobalAnimator.DIALOG_ANIMATOR,
modifier: this.modifier
}
}) {
Tabs({ barPosition: BarPosition.End }) {
ForEach(this.list, (item: TabItem, index: number) => {
TabContent() {
if (this.currentIndex === 0) {
HomePage()
} else if (this.currentIndex === 1) {
} else if (this.currentIndex === 2) {
} else if (this.currentIndex === 3) {
} else if (this.currentIndex === 4) {
MyPage()
}
}
.tabBar(this.TabItemBuilder(item, index))
})
}
.onChange((index: number) => {
this.currentIndex = index
})
.barMode(BarMode.Fixed)
.scrollable(false)
.animationMode(AnimationMode.NO_ANIMATION)
}
}
.height('100%')
.width('100%')
}
@Builder
TabItemBuilder(item: TabItem, index: number) {
Column() {
Image(this.currentIndex === index ? item.active : item.normal)
.fillColor(this.currentIndex === index ? '#ffef9608' : Color.Gray)
.width(20)
.aspectRatio(1)
Text(item.text)
.fontColor(this.currentIndex === index ? '#ffef9608' : Color.Gray)
.fontSize(14)
}
.width('100%')
.justifyContent(FlexAlign.SpaceEvenly)
.height(50)
}
}
class MyNavModifier extends AttributeUpdater<NavigationAttribute> {
initializeModifier(instance: NavigationAttribute): void {
instance.mode(NavigationMode.Stack);
//instance.hideNavBar(true) //这句会导致底部导航不展示
instance.navBarWidth('100%')
instance.hideTitleBar(true)
instance.hideToolBar(true)
}
}
优化LoginPage
import { HMRouter, HMRouterMgr } from '@hadss/hmrouter'
const PAGE_URL: string = 'LoginPage'
const KEY_IS_LOGIN = 'isLogin'
@HMRouter({ pageUrl: PAGE_URL })
@Component
export struct LoginPage {
@Consume isLogin: boolean
build() {
Column() {
Text('登录')
.fontSize(24)
.fontWeight(FontWeight.Medium)
.margin({ bottom: 32 })
Text('登录后即可访问 PageB')
.fontSize(14)
.fontColor('#666')
.margin({ bottom: 48 })
Button('登录并跳转 PageB')
.width('80%')
.fontSize(16)
.onClick(() => {
// 同步到 AppStorage,拦截器据此放行
AppStorage.setOrCreate(KEY_IS_LOGIN, true)
HMRouterMgr.replace({ pageUrl: 'PageB', param: `123456` })
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
创建拦截器
import { HMInterceptor, HMInterceptorAction, HMInterceptorInfo, HMRouterMgr, IHMInterceptor } from "@hadss/hmrouter";
/** 登录状态在 AppStorage 中的 key,与登录页、Index 保持一致 */
const KEY_IS_LOGIN = 'isLogin';
@HMInterceptor({ interceptorName: 'PageInterceptor' })
export class PageInterceptor implements IHMInterceptor {
handle(info: HMInterceptorInfo): HMInterceptorAction {
// 从 AppStorage 读取登录状态(与 LoginPage 写入处一致,避免拦截器实例变量与页面不同步)
const isLogin = AppStorage.get<boolean>(KEY_IS_LOGIN) === true;
if (isLogin) {
return HMInterceptorAction.DO_NEXT;
}
HMRouterMgr.push({
pageUrl: 'LoginPage',
param: { targetUrl: info.targetName },
skipAllInterceptor: true
});
return HMInterceptorAction.DO_REJECT;
}
}
在PageB添加拦截器注解
import { HMRouter, HMRouterMgr } from '@hadss/hmrouter'
import { JSON } from '@kit.ArkTS'
@HMRouter({
pageUrl: 'PageB',
interceptors: ['PageInterceptor'],
})
@Component
export struct PageB {
@State param: Object | null = HMRouterMgr.getCurrentParam()
build() {
Column() {
Text('PageB')
Text(`${JSON.stringify(this.param)}`)
Button('返回首页')
.onClick(() => {
HMRouterMgr.pop()
})
}
}
}
成果展示
![]()
7.生命周期的使用
使用@HMLifecycle标签定义生命周期处理器,并实现IHMLifecycle接口
import { HMLifecycle, HMLifecycleContext, IHMLifecycle } from "@hadss/hmrouter";
@HMLifecycle({ lifecycleName: 'PageLifecycle' })
export class PageLifecycle implements IHMLifecycle {
private time: number = 0;
onShown(ctx: HMLifecycleContext): void {
this.time = new Date().getTime();
}
onHidden(ctx: HMLifecycleContext): void {
const duration = new Date().getTime() - this.time;
// 检测进入PageB到离开PageB持续的时间
console.info(`Page ${ctx.navContext?.pathInfo.name} stay ${duration}`);
}
}
PageB中添加生命周期注解
import { HMRouter, HMRouterMgr } from '@hadss/hmrouter'
import { JSON } from '@kit.ArkTS'
@HMRouter({
pageUrl: 'PageB',
interceptors: ['PageInterceptor'],
lifecycle: 'PageLifecycle',
})
@Component
export struct PageB {
@State param: Object | null = HMRouterMgr.getCurrentParam()
build() {
Column() {
Text('PageB')
Text(`${JSON.stringify(this.param)}`)
Button('返回首页')
.onClick(() => {
HMRouterMgr.pop()
})
}
}
}
成果展示
![]()
8.自定义动画
通过@HMAnimator标签定义转场动画,并实现IHMAnimator接口
import { HMAnimator, HMAnimatorHandle, IHMAnimator, OpacityOption, ScaleOption, TranslateOption } from "@hadss/hmrouter"
@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({ pageUrl: PAGE_URL, animator: 'liveCommentsAnimator' })
成果展示
![]()