HarmonyOS UIAbility 启动参数丢失怎么办:Want 和页面路由怎么接住
https://mp.csdn.net/mp_blog/manage/article
HarmonyOS UIAbility 启动参数丢失怎么办:Want 和页面路由怎么接住
做 HarmonyOS 页面跳转时,很多问题不是页面路由本身写错了,而是入口参数接得太晚,或者只接了一半。
比如卡片、通知、聚合链接、应用内 startAbility 都可能把参数放进 Want.parameters 里。应用没启动时,参数会跟着 onCreate 进来;应用已经在后台或已有实例时,新的参数又可能走到 onNewWant。如果代码只在一个地方读参数,就很容易出现“第一次能跳,第二次不跳”“冷启动能跳,热启动丢参数”“日志里有 recipeId,页面却还停在首页”。
这篇只拆一个问题:UIAbility 收到 Want 以后,怎么把启动参数稳定交给页面路由。
先把边界说清楚
我一般会把这件事分成三层:
| 层级 | 负责什么 | 不该负责什么 |
|---|---|---|
| UIAbility | 接住系统给的 Want | 不直接写一堆页面状态 |
| RouteBridge | 把 Want 转成页面能识别的路由指令 | 不关心页面怎么渲染 |
| 页面入口 | 等首屏准备好以后消费路由指令 | 不猜启动来源 |
这样拆的好处是,生命周期变化不会把页面逻辑搅乱。onCreate 和 onNewWant 都只做同一件事:把 Want 丢给一个统一入口。
坏例子:只在 onCreate 里读参数
很多丢参就是从这段写法开始的:
export default class EntryAbility extends UIAbility {
onCreate(want: Want) {
const targetPage = want.parameters?.targetPage as string;
const recipeId = want.parameters?.recipeId as string;
AppRouteStore.target = { targetPage, recipeId };
}
}
这段在冷启动时可能没问题。应用第一次被拉起,onCreate 能拿到参数,页面也能根据 AppRouteStore.target 跳到详情页。
但问题在第二次出现。应用已经存在时,再从通知或卡片拉一次,系统不一定重新创建这个 UIAbility。已有实例收到新的 Want 后,入口可能变成 onNewWant。如果这里只写了 onCreate,新参数就没有人接。
表现出来就是:通知点了,应用被拉到前台了,但页面没有按新的参数跳。
案例一:冷启动时页面还没准备好
冷启动还有一个坑:onCreate 比首屏页面更早。你在 onCreate 里拿到了参数,但页面树还没挂起来,这时直接调页面路由,很可能失败,或者被后续默认首页覆盖。
更稳的做法是先缓存成路由指令,等首屏告诉你“我准备好了”,再消费。
type PendingRoute = {
page: string;
id: string;
source: string;
};
class RouteBridge {
private ready = false;
private pending: PendingRoute[] = [];
acceptWant(want: Want, stage: 'onCreate' | 'onNewWant') {
const params = want.parameters ?? {};
const route: PendingRoute = {
page: String(params.targetPage ?? 'home'),
id: String(params.recipeId ?? ''),
source: String(params.from ?? stage),
};
if (this.ready) {
this.open(route);
return;
}
this.pending.push(route);
}
markPageReady() {
this.ready = true;
while (this.pending.length > 0) {
this.open(this.pending.shift()!);
}
}
private open(route: PendingRoute) {
// 这里再转成 NavPathStack.pushPath、router.pushUrl 或自己的页面入口。
console.info(`open ${route.page}, id=${route.id}, source=${route.source}`);
}
}
这里的重点不是写一个全局队列,而是不要在 UIAbility 生命周期里直接假设页面已经可用。
冷启动可以这样接:
const routeBridge = new RouteBridge();
export default class EntryAbility extends UIAbility {
onCreate(want: Want) {
routeBridge.acceptWant(want, 'onCreate');
}
onWindowStageCreate(windowStage: window.WindowStage) {
windowStage.loadContent('pages/Index', () => {
routeBridge.markPageReady();
});
}
}
这样即使 Want.parameters.recipeId 早于页面到达,也不会丢。
案例二:热启动时新参数被旧页面吃掉
热启动更容易被忽略。页面还在,状态也还在,看起来一切正常,但这次启动其实带了新的参数。
如果不处理 onNewWant,页面还是旧详情;如果处理了但不做去重,又可能同一个通知被重复点击后重复压栈。
我会把 onNewWant 接到同一个 RouteBridge,并加一个简单的去重键:
class RouteBridge {
private ready = false;
private pending: PendingRoute[] = [];
private lastKey = '';
acceptWant(want: Want, stage: 'onCreate' | 'onNewWant') {
const params = want.parameters ?? {};
const route: PendingRoute = {
page: String(params.targetPage ?? 'home'),
id: String(params.recipeId ?? ''),
source: String(params.from ?? stage),
};
const key = `${route.page}:${route.id}:${route.source}`;
if (key === this.lastKey) {
return;
}
this.lastKey = key;
if (this.ready) {
this.open(route);
} else {
this.pending.push(route);
}
}
}
然后在 UIAbility 里统一调用:
export default class EntryAbility extends UIAbility {
onCreate(want: Want) {
routeBridge.acceptWant(want, 'onCreate');
}
onNewWant(want: Want) {
routeBridge.acceptWant(want, 'onNewWant');
}
}
这样冷启动和热启动的入口不再分裂,页面只需要消费一套路由指令。
本地跑一个最小复现
我用一个小脚本把两种情况跑了一遍:
class RouteBridge {
private uiReady = false;
private pending: Route[] = [];
routeLog: Route[] = [];
markUiReady() {
this.uiReady = true;
while (this.pending.length > 0) {
this.routeLog.push(this.pending.shift()!);
}
}
acceptWant(stage: string, want: WantLike) {
const params = want.parameters ?? {};
const route = {
stage,
page: params.targetPage ?? 'home',
id: params.recipeId ?? '',
source: params.from ?? 'unknown',
};
this.uiReady ? this.routeLog.push(route) : this.pending.push(route);
}
}
运行结果是:
[
{
"name": "cold-start",
"pendingAfterReady": 0,
"routes": [
{
"stage": "onCreate",
"page": "recipeDetail",
"id": "r-1024",
"source": "card"
}
]
},
{
"name": "warm-start",
"pendingAfterReady": 0,
"routes": [
{
"stage": "onNewWant",
"page": "recipeDetail",
"id": "r-2048",
"source": "notification"
}
]
}
]
这个结果说明两件事:
- 冷启动参数先进入队列,首屏准备好以后再打开页面;
- 热启动参数直接从
onNewWant进入路由,不会继续沿用旧页面。
几种方案怎么选
| 方案 | 优点 | 问题 | 更适合 |
|---|---|---|---|
| 只在 onCreate 里读 Want | 写起来最少 | 热启动参数容易丢 | 简单一次性入口 |
| onCreate 和 onNewWant 各写一套跳转 | 看起来直观 | 两套逻辑容易不一致 | 临时验证 |
| 统一 RouteBridge | 冷热启动走一套规则 | 需要多写一个小模块 | 正式项目 |
| 页面自己读取全局 Want | 页面拿数据方便 | 页面会知道太多启动细节 | 不建议长期用 |
我更倾向第三种。UIAbility 只负责接参数,页面只负责渲染,中间用一个小桥接层处理“页面是否准备好”和“是否重复跳转”。
以后怎么避免
写这类入口时,我会固定检查四件事:
onCreate和onNewWant是否都接到了同一个处理入口;- 页面没准备好时,路由是否会先进队列;
- 同一个参数重复进入时,是否会重复压栈;
- 参数解析失败时,是否有默认页或错误兜底。
如果这四点都过了,通知、卡片、应用内跳转、聚合链接这几类入口就不会各写各的。后面再接更多来源,也只是多一种 source,不需要改页面内部的状态逻辑。
关联知识点
这块和 UIAbility 生命周期、Want 参数、UIAbility 启动模式、页面路由状态都有关系。真正容易出问题的不是某个 API 名字,而是把“系统入口”和“页面状态”混在一起写。
我的经验是:生命周期里少做页面活,先把参数变成稳定指令。页面准备好之后再消费,这样冷启动、热启动和重复点击都能解释清楚,也更容易排查。
更多推荐



所有评论(0)