https://mp.csdn.net/mp_blog/manage/articleHarmonyOS UIAbility 启动参数丢失怎么办:Want 和页面路由怎么接住

HarmonyOS UIAbility 启动参数丢失怎么办:Want 和页面路由怎么接住

做 HarmonyOS 页面跳转时,很多问题不是页面路由本身写错了,而是入口参数接得太晚,或者只接了一半。

比如卡片、通知、聚合链接、应用内 startAbility 都可能把参数放进 Want.parameters 里。应用没启动时,参数会跟着 onCreate 进来;应用已经在后台或已有实例时,新的参数又可能走到 onNewWant。如果代码只在一个地方读参数,就很容易出现“第一次能跳,第二次不跳”“冷启动能跳,热启动丢参数”“日志里有 recipeId,页面却还停在首页”。

这篇只拆一个问题:UIAbility 收到 Want 以后,怎么把启动参数稳定交给页面路由。

先把边界说清楚

我一般会把这件事分成三层:

层级 负责什么 不该负责什么
UIAbility 接住系统给的 Want 不直接写一堆页面状态
RouteBridge 把 Want 转成页面能识别的路由指令 不关心页面怎么渲染
页面入口 等首屏准备好以后消费路由指令 不猜启动来源

这样拆的好处是,生命周期变化不会把页面逻辑搅乱。onCreateonNewWant 都只做同一件事:把 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 只负责接参数,页面只负责渲染,中间用一个小桥接层处理“页面是否准备好”和“是否重复跳转”。

以后怎么避免

写这类入口时,我会固定检查四件事:

  1. onCreateonNewWant 是否都接到了同一个处理入口;
  2. 页面没准备好时,路由是否会先进队列;
  3. 同一个参数重复进入时,是否会重复压栈;
  4. 参数解析失败时,是否有默认页或错误兜底。

如果这四点都过了,通知、卡片、应用内跳转、聚合链接这几类入口就不会各写各的。后面再接更多来源,也只是多一种 source,不需要改页面内部的状态逻辑。

关联知识点

这块和 UIAbility 生命周期、Want 参数、UIAbility 启动模式、页面路由状态都有关系。真正容易出问题的不是某个 API 名字,而是把“系统入口”和“页面状态”混在一起写。

我的经验是:生命周期里少做页面活,先把参数变成稳定指令。页面准备好之后再消费,这样冷启动、热启动和重复点击都能解释清楚,也更容易排查。

Logo

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

更多推荐