普通视图

发现新文章,点击刷新页面。
昨天 — 2026年3月15日首页

【鸿蒙开发】HMRouter一款和好用的管理路由三方工具

2026年3月15日 15:25

HMRouter

HMRouter作为应用内页面跳转场景解决方案,为开发者提供了功能完备、高效易用的路由框架。

HMRouter底层对系统Navigation进行封装,集成了NavigationNavDestinationNavPathStack的系统能力,提供了可复用的路由拦截、页面生命周期、自定义转场动画,并且在跳转传参、额外的生命周期、服务型路由方面对系统能力进行了扩展,同时开发者可以高效的将历史代码中的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' } })
      })
    }
  }
}

现在我们已经能实现初步的页面跳转了

PixPin_2026-03-15_11-34-28.gif

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()
        })
    }
  }
}

成果展示

PixPin_2026-03-15_13-34-23.gif

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()
        })
    }
  }
}

成果展示

PixPin_2026-03-15_13-57-28.gif

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' })

成果展示

PixPin_2026-03-15_14-15-34.gif

❌
❌