HarmonyOS HMRouter 页面跳转实战
文章摘要:本文介绍了HarmonyOS应用开发中HMRouter路由框架的实用技巧。HMRouter通过注解配置和链式调用简化了页面跳转操作,支持参数传递、拦截器实现登录检查、单例页面优化性能等功能。文中详细讲解了基础跳转方法、路由拦截实现、单例页面配置以及弹窗场景的三种实现方式,包括返回确认弹窗等常见业务场景的解决方案。该框架可显著提升开发效率,适用于电商、视频等复杂应用场景。
一、开场白:页面跳转这事儿,其实可以很简单
咱们写 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 方法会传入入场和出场的效果对象 enterHandle 与 exitHandle,通过 start 和 finish 方法设置动画的起止状态。
常用配置项:
curve:动画速度曲线,支持Curve枚举,默认Curve.EaseInOutduration:动画持续时长,单位 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 交互式转场:跟随手势的动画
当页面进出场效果需要和用户手势同步时,比如手指在屏幕上移动时页面跟随手势移动,可以用 IHMAnimator 的 interactive 函数控制动画播放进度。
@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注解配置global为true,这样所有页面都会执行这个生命周期onShown里记录当前时间戳onHidden里计算停留时长并打点
九、实战建议
9.1 页面标识命名规范
pageUrl 是页面的唯一标识,建议用有意义的英文名,比如 ProductContent、LoginPage、PaySuccessPage 这种,别用 page1、page2 这种无意义的命名。
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 的细节。
实际开发中,根据具体场景选择合适的功能组合:
- 基础跳转 → 用
to和pop方法 - 权限校验 → 用拦截器
- 特殊页面 → 用单例或 Dialog 配置
- 动画效果 → 根据需求选全局或自定义
- 数据加载 → 用生命周期的
onPrepare - 埋点统计 → 用全局生命周期
把这些功能摸透了,HarmonyOS 应用的页面跳转就能玩得转了。
更多推荐
所有评论(0)