一、开场白:页面跳转这事儿,其实可以很简单

咱们写 HarmonyOS 应用,页面跳转是最基础也最频繁的操作。

官方给的 Navigation 组件功能挺全,但用起来有点麻烦,得自己管页面栈、传参数、处理返回,一堆细节。

HMRouter 就是来救场的。这玩意儿是华为官方出的路由框架,底层封装了 Navigation,咱们不用管那些细节,直接用注解配置、链式调用就能搞定跳转。

今天我们就结合实际开发中的各种场景,把 HMRouter 的用法给你们讲明白。


二、基础用法:三分钟上手

2.1 给页面加个注解

要让 HMRouter 管你的页面,第一步是给它加个 @HMRouter 注解,配个 pageUrl

这个 pageUrl 就是页面的唯一标识,后面跳转的时候就靠它找目标页面。

@HMRouter({ pageUrl: 'ProductContent' })
@Component
export struct ProductContent {
  @State param: ParamsType | null = null;

  aboutToAppear(): void {
    this.param = HMRouterMgr.getCurrentParam() as ParamsType;
  }
}

aboutToAppear 生命周期里,用 HMRouterMgr.getCurrentParam() 就能拿到从其他页面传过来的参数。

2.2 执行跳转

在需要跳转的地方,调用 HMRouterMgr.to 方法,传入目标页面的 pageUrl,然后链式调用配置参数,最后用 pushAsync() 执行。

HMRouterMgr.to('ProductContent')
  .withNavigation('mainNavigationId')
  .withParam({ a: 1, b: 2 })
  .onResult((popInfo: HMPopInfo) => {
    const pageName = popInfo.srcPageInfo.name;
    const params = popInfo.result;
    console.log(`page name is ${pageName}, params is ${JSON.stringify(params)}`);
  })
  .pushAsync()

这几个配置项啥意思?

  • withNavigation:指定页面栈的唯一标识。要是你用多个 HMNavigation,建议手动指定;要是只用一个,可以不传,系统会默认处理
  • withParam:传参数给目标页面
  • onResult:配置从目标页面返回时的回调。回调里可以通过 srcPageInfo.name 知道是从哪个页面跳过来的,通过 result 拿到返回时带的参数

2.3 页面返回

HMRouterMgr.pop 方法实现页面返回,同样可以传 navigationId,还能通过 param 参数向返回的页面传递数据。

HMRouterMgr.pop({ navigationId: 'mainNavigationId', param: this.param })

2.4 直接返回到指定页面

有时候会有这种需求:

页面跳转路径是 HomePage → PageA → PageB → PageC,在 PageC 里想直接返回到 HomePage,还要带参数。

用 HMRouter 实现贼简单,还是用 to 方法,传入目标页面地址和参数就行。

HMRouterMgr.to('MainPage')
  .withNavigation('mainNavigationId')
  .withParam(0)
  .pushAsync()

这个功能在购物车结算、订单支付这类场景特别有用。用户从详情页一层层点进去,最后支付成功页可以直接跳回首页或者订单列表页,不用一层层返回。


三、路由拦截:未登录自动跳转登录页

咱们开发的时候,经常碰到这种场景:

用户没登录,点击某些内容时自动跳到登录页。

用 HMRouter 的拦截器功能可以轻松实现。

3.1 定义拦截器类

创建一个类实现 IHMInterceptor 接口,加上 @HMInterceptor 注解,配个拦截器名称。

@HMInterceptor({ interceptorName: 'LoginCheckInterceptor' })
export class LoginCheckInterceptor implements IHMInterceptor {
  async intercept(chain: IHMInterceptorChain): Promise<void> {
    const info = chain.getRouterInfo();
    const context = chain.getContext();
    
    if (!!AppStorage.get('isLogin')) {
      await chain.onContinue()
    } else {
      info.context.getPromptAction().showToast({ message: '请先登录' });
      HMRouterMgr.push({
        pageUrl: 'loginPage',
        skipAllInterceptor: true
      });
      await chain.onIntercept();
    }
  }
}

拦截逻辑很清楚:

  • 用户已登录 → 执行 chain.onContinue(),正常继续跳转
  • 用户未登录 → 先弹 Toast 提示,然后跳转到登录页(skipAllInterceptor: true 表示跳过所有拦截器,避免死循环),最后执行 chain.onIntercept() 拦截此次跳转

3.2 在页面上配置拦截器

在需要拦截的页面的 @HMRouter 注解里配置 interceptors 参数。因为一个页面可以配多个拦截器,所以要传数组。

@HMRouter({
  pageUrl: 'SomePage',
  interceptors: ['LoginCheckInterceptor']
})
@Component
export struct SomePage {
  // ...
}

四、单例页面:避免重复初始化消耗

有些页面初始化时加载资源消耗很大,而且有复用需求,这种就适合做成单例页面。

典型场景是视频类应用的播放页,每次都要加载视频解码器资源并初始化,但这个页面在应用里会频繁出现。

实现单例页面贼简单,在 @HMRouter 注解里把 singleton 参数设为 true 就行。

@HMRouter({
  pageUrl: 'liveHome',
  singleton: true,
  animator: 'liveInteractiveAnimator',
  lifecycle: 'liveHomeLifecycle'
})
@Component
export struct LiveHome {
  // ...
}

这样配置后,这个页面在应用生命周期内只会创建一次,后续跳转都是复用这个实例。


五、弹窗场景:三种实现方式

5.1 把页面变成弹窗

在 HMRouter 里,把 @HMRouter 注解的 dialog 配置设为 true,当前页面就变成弹窗了。

@HMRouter({
  pageUrl: PageConstant.PRIVACY_DIALOG_DETAIL,
  dialog: true,
})
@Component
export struct PrivacyDialogDetail {
  // ...
}

5.2 返回时弹窗确认

有些页面返回时需要用户确认,比如订单支付页,用户点返回时通常要弹窗问是否确认退出。

这个场景可以用弹窗页面加自定义生命周期来实现。

第一步,先开发自定义弹窗组件:

@HMRouter({ pageUrl: 'PayCancel', dialog: true, animator:'PayCancelDialog' })
@Component
export struct PayCancel {
  build() {
    Stack({ alignContent: Alignment.Center }) {
      ConfirmDialog({
        title: '取消订单',
        content: '您确认要取消此订单吗?',
        leftButtonName: '再看看',
        rightButtonName: '取消订单',
        leftButtonFunc: () => {
          HMRouterMgr.popAsync({
            navigationId: this.queryNavigationInfo()?.navigationId
          });
        },
        rightButtonFunc: () => {
          // ...
        }
      });
    }
    .width('100%')
    .height('100%')
    .position({
      x: '50%',
      y: '50%'
    })
    .markAnchor({
      x: '50%',
      y: '50%'
    });
  }
}

第二步,定义生命周期类实现 IHMLifecycle 接口,加上 @HMLifecycle 注解。在 onBackPressed 回调里弹出刚才定义的弹窗。

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

  onBackPressed(): boolean {
    HMRouterMgr.to('PayCancel')
      .withParam(this.model.pageUrl)
      .pushAsync()
    return true;
  }
}

第三步,把生命周期和支付页面绑定,在 @HMRouter 注解的 lifecycle 参数里传入生命周期名称。

@HMRouter({
  pageUrl: 'PayDialogContent',
  dialog: true,
  lifecycle: 'ExitPayDialog',
  interceptors: ['LoginCheckInterceptor']
})
@Component
export struct PayDialogContent {
  // ...
}

5.3 两次返回退出应用

这个场景是:用户第一次点返回时提示"再次返回退出",第二次点返回才真正退出应用。

第一步,定义生命周期类:

@HMLifecycle({ lifecycleName: 'ExitAppLifecycle' })
export class ExitAppLifecycle implements IHMLifecycle {
  private lastTime: number = 0;

  onBackPressed(ctx: HMLifecycleContext): boolean {
    let time = new Date().getTime();
    if (time - this.lastTime > 1000) {
      this.lastTime = time;
      ctx.uiContext.getPromptAction().showToast({
        message: '再次返回退出应用',
        duration: 1000
      });
      return true;
    } else {
      return false;
    }
  }
}

逻辑是判断两次返回操作的时间间隔:

  • 大于 1000ms → 更新 lastTime,弹 Toast 提示,返回 true 表示不执行默认返回逻辑
  • 小于等于 1000ms → 返回 false 表示执行默认返回逻辑,退出应用

第二步,在 @HMRouter 注解里配置 lifecycle 参数关联这个生命周期。


六、转场动效:四种配置方式

6.1 全局自定义转场效果

定义全局页面转场效果,创建 IHMAnimator.Effect 实例,配置动画方向、透明度、缩放等参数。

const globalPageTransitionEffect: IHMAnimator.Effect = new IHMAnimator.Effect({
  direction: IHMAnimator.Direction.BOTTOM_TO_TOP,
  opacity: { opacity: 0.5 },
  scale: { x: 0.5, y: 0.2 }
})

然后把实例传入 HMNavigation 组件的 dialogAnimator 参数。

HMNavigation({
  navigationId: 'mainNavigationId', 
  homePageUrl: 'HomeContent', 
  options: {
    dialogAnimator: globalPageTransitionEffect,
  }
})

6.2 特定页面自定义转场

要是想给特定页面配置专属动画,可以自定义动画类实现 IHMAnimator 接口。

@HMAnimator({ animatorName: 'CustomAnimator' })
export class CustomAnimator implements IHMAnimator {
  effect(enterHandle: HMAnimatorHandle, exitHandle: HMAnimatorHandle): void {
    // 入场动画
    enterHandle.start((modifier: AttributeUpdater<NavDestinationAttribute>) => {
      modifier.attribute?.translate({ y: '100%' }).scale({ x: 0.7 }).opacity(0.3)
    }).finish((modifier: AttributeUpdater<NavDestinationAttribute>) => {
      modifier.attribute?.translate({ y: '0' }).scale({ x: 1 }).opacity(1)
    })
    enterHandle.duration = 400;
    enterHandle.curve = Curve.Linear;

    // 出场动画
    exitHandle.start((modifier: AttributeUpdater<NavDestinationAttribute>) => {
      modifier.attribute?.translate({ y: '0' }).scale({ x: 1 }).opacity(1)
    }).finish((modifier: AttributeUpdater<NavDestinationAttribute>) => {
      modifier.attribute?.translate({ y: '100%' }).scale({ x: 0.7 }).opacity(0.3)
    })
    exitHandle.duration = 400;
    enterHandle.curve = Curve.Linear;
  }
}

effect 方法会传入入场和出场的效果对象 enterHandleexitHandle,通过 startfinish 方法设置动画的起止状态。

常用配置项:

  • curve:动画速度曲线,支持 Curve 枚举,默认 Curve.EaseInOut
  • duration:动画持续时长,单位 ms

上面代码的效果是:入场时从屏幕底部以线性速度向顶部运动,持续 400ms;出场时从顶部以线性速度向底部运动,也是 400ms。

定义完成后,在跳转时把动画实例作为 animator 参数传入。

HMRouterMgr.to('ProductContent')
  .withAnimator(new CustomAnimator())
  .pushAsync()

6.3 根据条件使用不同转场

相同页面在不同情况下可能需要不同的转场效果。

比如短视频播放时的评论页面:

  • 横屏播放时 → 评论页从右至左弹出,视频向左缩放
  • 竖屏播放时 → 评论页从下至上弹出,视频向上缩放

先定义竖屏播放时的动画:

@HMAnimator({ animatorName: 'myAnimator1' })
export class MyAnimator1 implements IHMAnimator {
  effect(enterHandle: HMAnimatorHandle, exitHandle: HMAnimatorHandle): void {
    enterHandle.start((modifier: AttributeUpdater<NavDestinationAttribute>) => {
      modifier.attribute?.translate({ y: '100%' })
    }).finish((modifier: AttributeUpdater<NavDestinationAttribute>) => {
      modifier.attribute?.translate({ y: '0' })
    })

    exitHandle.start((modifier: AttributeUpdater<NavDestinationAttribute>) => {
      modifier.attribute?.translate({ y: '0' })
    }).finish((modifier: AttributeUpdater<NavDestinationAttribute>) => {
      modifier.attribute?.translate({ y: '100%' })
    })
  }
}

再定义横屏播放时的动画:

@HMAnimator({ animatorName: 'myAnimator2' })
export class MyAnimator2 implements IHMAnimator {
  effect(enterHandle: HMAnimatorHandle, exitHandle: HMAnimatorHandle): void {
    enterHandle.start((modifier: AttributeUpdater<NavDestinationAttribute>) => {
      modifier.attribute?.translate({ x: '100%', y: '0' })
    }).finish((modifier: AttributeUpdater<NavDestinationAttribute>) => {
      modifier.attribute?.translate({ x: 0 })
    })
    enterHandle.duration = 500;

    exitHandle.start((modifier: AttributeUpdater<NavDestinationAttribute>) => {
      modifier.attribute?.translate({ x: '0' })
    }).finish((modifier: AttributeUpdater<NavDestinationAttribute>) => {
      modifier.attribute?.translate({ x: '100%' })
    })
    exitHandle.duration = 500;
  }
}

最后在页面跳转时根据条件选择不同的动画:

@Component
export struct CommentInput {
  build() {
    Row() {
      Image($r('app.media.icon_comments'))
        .width(24)
        .height(24)
        .margin({ right: 16 })
        .onClick(() => {
          if (this.isLandscape) {
            HMRouterMgr.to('liveComments')
              .withNavigation(this.queryNavigationInfo()?.navigationId)
              .withParam({commentRenderNode: ''})
              .withAnimator(new MyAnimator2())
              .onResult((paramInfo: PopInfo)=>{
                this.videoWidth = '100%';
              })
              .pushAsync()
            this.videoWidth = '50%';
          } else {
            HMRouterMgr.to('liveComments')
              .withNavigation(this.queryNavigationInfo()?.navigationId)
              .withParam({commentRenderNode: ''})
              .withAnimator(new MyAnimator1())
              .onResult((paramInfo: PopInfo)=>{
                this.videoHeight = '100%';
              })
              .pushAsync()
            this.videoHeight = '30%'
          }
        });
    }
  }
}

6.4 交互式转场:跟随手势的动画

当页面进出场效果需要和用户手势同步时,比如手指在屏幕上移动时页面跟随手势移动,可以用 IHMAnimatorinteractive 函数控制动画播放进度。

@HMAnimator({ animatorName: 'liveInteractiveAnimator' })
export class LiveInteractiveAnimator implements IHMAnimator {
  effect(enterHandle: HMAnimatorHandle, exitHandle: HMAnimatorHandle): void {
    // ...
  }

  interactive(handle: HMAnimatorHandle): void {
    handle.actionStart((event: GestureEvent) => {
      if (event.offsetX > 0) {
        HMRouterMgr.popAsync();
      }
      handle.startOffset = event.fingerList[0].localX;
    });
    handle.updateProgress((event, proxy, operation, startOffset) => {
      if (!proxy?.updateTransition || !startOffset) {
        return;
      }
      let offset = 0;
      if (event.fingerList[0]) {
        offset = Math.abs(event.fingerList[0].localX - startOffset);
      }
      if (offset < 0) {
        proxy?.updateTransition(0);
        return;
      }
      let rectWidth = event.target.area.width as number;
      let rate = offset / rectWidth;
      proxy?.updateTransition(rate);
    });
    handle.actionEnd((event, proxy, operation, startOffset) => {
      if (!startOffset) {
        return;
      }
      let rectWidth = event.target.area.width as number;
      let rate = 0;
      if (event.fingerList[0]) {
        rate = Math.abs(event.fingerList[0].localX - startOffset) / rectWidth;
      }
      if (rate > 0.4) {
        proxy?.finishTransition();
      } else {
        proxy?.cancelTransition?.();
      }
    });
  }
}

三个关键回调:

  • actionStart:手势开始时触发,判断向右移动就执行页面返回,记录起始位置
  • updateProgress:手势移动时更新动画进度,根据手指移动距离计算进度比例
  • actionEnd:手势结束时根据最终状态判断是完成动画还是取消动画

七、数据加载场景

7.1 数据预加载:网络请求与页面跳转并行

有些场景需要提前发起网络请求,在其他线程执行而不阻塞主线程。可以用 TaskPool 实现。

第一步,定义网络请求函数,用 @Concurrent 标记使其能在其他线程执行。

@Concurrent
async function networkRequest(lifecycle: string): Promise<string> {
  // ...
}

第二步,定义生命周期,在 onPrepare 回调里执行网络请求。onPrepare 的触发时机是拦截器执行后、路由栈真正 push 前。

@HMLifecycle({ lifecycleName: 'requestLifecycle' })
export class ExampleLifecycle implements IHMLifecycle {
  private requestModel: RequestModel = new RequestModel();

  onPrepare(): void {
    console.log(this.requestModel.data);
    let task: taskpool.Task = new taskpool.Task(networkRequest, 'onPrepare');
    taskpool.execute(task).then((res: Object) => {
      console.log(res + '');
    });
  }
}

第三步,把生命周期和组件关联,在 @HMRouter 注解的 lifecycle 参数里传入 lifecycleName

7.2 页面重开数据恢复

这个场景是:页面关闭后,之前浏览的记录依然保留。

典型例子是短视频评论区,用户打开评论区翻到某个位置,关闭后再打开,评论内容还停留在上次的位置。

实现思路是用 BuilderNode 构造评论区组件,在生命周期里控制 BuilderNode 的创建和释放,让它跟随视频播放页面的生命周期,而不是评论区组件的生命周期。

第一步,用 BuilderNode 构造评论区组件:

@Builder
function buildComment(liveComments: LiveCommentsProduct[]) {
  // ...
}

export class CommentNodeController extends NodeController {
  commentList: BuilderNode<[LiveCommentsProduct[]]> | null = null;
  commentListData: LiveCommentsProduct[] = new LiveCommentsModel().getLiveCommentsList();

  constructor() {
    super();
  }

  makeNode(context: UIContext): FrameNode | null {
    if (this.commentList === null) {
      this.nodeBuild(context);
    }
    return this.commentList!.getFrameNode();
  }

  nodeBuild(context: UIContext) {
    this.commentList = new BuilderNode(context);
    if (this.commentList !== null) {
      this.commentList.build(wrapBuilder<[LiveCommentsProduct[]]>(buildComment), this.commentListData);
    }
  }

  dispose() {
    if (this.commentList !== null) {
      this.commentList.dispose();
    }
  }
}

makeNode 函数里,如果评论区不存在就创建,存在就直接返回。这样就能复用之前的实例。

第二步,在生命周期里控制 CommentNodeController 的创建和释放:

@HMLifecycle({ lifecycleName: 'liveHomeLifecycle' })
export class LiveHomeLifecycle implements IHMLifecycle {
  commentRenderNode: CommentNodeController = new CommentNodeController();
  
  onAppear(ctx: HMLifecycleContext): void {
    this.commentRenderNode.makeNode(ctx.uiContext);
  }

  onDisAppear(ctx: HMLifecycleContext): void {
    this.commentRenderNode.dispose();
  }
}

这样当用户在视频播放页时,内存中保存着评论区组件的 BuilderNode,关闭评论区再打开时,浏览进度就和关闭前一致了。

第三步,在 UI 组件里获取生命周期内的 commentRenderNode,用 NodeContainer 挂载。


八、维测场景:页面埋点开发

需要统计页面加载耗时或其他自定义打点数据时,可以用生命周期回调在对应位置打点。

比如统计页面停留时长:

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

  onShown(): void {
    this.time = new Date().getTime();
  }

  onHidden(ctx: HMLifecycleContext): void {
    const duration = new Date().getTime() - this.time;
    console.log(`Page ${ctx.navContext?.pathInfo.name} stay ${duration}`);
  }
}

关键点:

  • 实现 IHMLifecycle 接口
  • @HMLifecycle 注解配置 globaltrue,这样所有页面都会执行这个生命周期
  • onShown 里记录当前时间戳
  • onHidden 里计算停留时长并打点

九、实战建议

9.1 页面标识命名规范

pageUrl 是页面的唯一标识,建议用有意义的英文名,比如 ProductContentLoginPagePaySuccessPage 这种,别用 page1page2 这种无意义的命名。

9.2 navigationId 的使用

要是应用里只用一个 HMNavigation,可以不传 navigationId,系统会默认处理。但要有多个 HMNavigation,一定要手动指定 navigationId,否则可能出现页面栈混乱的问题。

9.3 拦截器的使用场景

拦截器适合做权限校验、登录检查、数据预加载这些需要在页面跳转前执行的操作。一个页面可以配多个拦截器,按配置顺序依次执行。

9.4 生命周期的选择

HMRouter 提供了丰富的生命周期回调:

  • onPrepare:拦截器执行后,路由栈 push 前,适合做数据预加载
  • onAppear:页面显示时
  • onDisAppear:页面隐藏时
  • onShown:页面完全显示后
  • onHidden:页面隐藏后
  • onBackPressed:用户执行返回操作时

根据业务需求选择合适的生命周期。

9.5 动画性能

自定义动画时注意:

  • duration 别设太长,一般 300-500ms 比较合适
  • 复杂的动画效果考虑用 interactive 让用户手势控制
  • 避免在动画执行时做耗时操作

十、常见问题

10.1 页面跳转没反应

检查几点:

  • 目标页面有没有加 @HMRouter 注解
  • pageUrl 配置是否正确
  • to 方法传入的 pageUrl 是否和注解里的一致
  • HMNavigation 组件是否正确配置

10.2 参数传递拿不到

确认:

  • withParam 传参
  • 在目标页面的 aboutToAppear 里用 HMRouterMgr.getCurrentParam() 获取
  • 参数类型要匹配

10.3 拦截器不生效

检查:

  • 拦截器类有没有加 @HMInterceptor 注解
  • interceptorName 配置是否正确
  • 页面的 interceptors 参数里有没有配置这个拦截器名称

10.4 单例页面没复用

确认:

  • @HMRouter 注解里 singleton 是否设为 true
  • 页面跳转是不是用的 HMRouterMgr.to 方法

十一、总结一下

HMRouter 路由框架的核心价值就一个:让页面跳转变得更简单、更灵活

通过注解配置、链式调用、拦截器、生命周期这些机制,咱们可以专注业务逻辑,不用太关注底层 Navigation 的细节。

实际开发中,根据具体场景选择合适的功能组合:

  • 基础跳转 → 用 topop 方法
  • 权限校验 → 用拦截器
  • 特殊页面 → 用单例或 Dialog 配置
  • 动画效果 → 根据需求选全局或自定义
  • 数据加载 → 用生命周期的 onPrepare
  • 埋点统计 → 用全局生命周期

把这些功能摸透了,HarmonyOS 应用的页面跳转就能玩得转了。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐