欢迎光临
我们一直在努力

36-推送点开走错页-用消息路由收件箱承接冷启动

第36篇|推送点开走错页:用消息路由收件箱承接冷启动

摘要:推送点击最容易被写成“收到什么就跳哪里”。实际项目里,冷启动、前台点击、登录拦截、重复送达会让页面走错、详情打不开、登录后回不到目标页。更稳的做法是把推送 payload 先落进消息路由收件箱,再由统一路由器解析、去重、鉴权和跳转。

我遇到过一次订单推送问题:用户点“待支付订单”通知,本该进入订单详情,结果冷启动时只到了首页;如果应用已经在前台,又会重复打开两次详情页。排查后发现有三条入口同时在处理同一个 payload:UIAbility 启动参数、通知点击回调、首页 aboutToAppear 的未读消息刷新。它们各自都觉得自己在做正确的跳转,最后栈里多了旧页面。

在这里插入图片描述

这篇文章解决四个实际问题:

  • 把推送 payload 从页面跳转里拆出来,先进入收件箱。
  • 用 messageId 和 routeKey 做幂等,挡住重复点击和重复送达。
  • 登录态不足时保存 pending route,登录后继续原目标。
  • 用启动、前台、后台三条路径验证同一个路由结果。
  • 在这里插入图片描述
    在这里插入图片描述

    先把三条入口合成一个现场

    推送点击走错页时,不要只查通知 SDK。先把冷启动、前台点击和后台回前台三种入口列出来,看它们是否都在直接操作页面栈。

    位置现象风险处理方向
    冷启动点击 UIAbility 拿到 payload 后首页又刷新一次 目标页被首页跳转覆盖 先写 RouteInbox
    前台点击 通知回调直接 push 详情页 重复点击打开两个页面 messageId 幂等
    未登录点击 路由先跳详情再被登录页拦截 登录后丢失原目标 保存 pending route
    重复送达 服务端重发同一条消息 旧消息再次消费 消费状态落盘

    这一步的价值是先把问题放回运行链路。只要能确认问题停在哪个位置,后面就不用靠猜测改页面。

    路由 owner 不应该是通知回调

    通知回调只负责把原始消息交出来,不应该知道订单详情页、活动页或聊天页怎么跳。真正拥有路由规则的是 MessageRouteResolver。

    模块应该负责不应该负责
    NotificationReceiver 原始 payload、点击时间 页面名称和栈操作
    RouteInbox messageId、payload、consumeState 业务页面展示
    MessageRouteResolver routeKey 到目标页的映射 UIAbility 生命周期
    AuthGate 是否需要登录、登录后续跳 通知栏细节
    NavigationHost NavPathStack push/replace 消息重复判断

    边界定清后,代码就不会在页面、服务和回调之间来回复制同一段判断。后续新增场景也能先判断应该落在哪一层。

    用 RouteMessage 保留原始意图

    export interface RouteMessage {
    messageId: string
    routeKey: string
    payload: string
    receivedAt: number
    consumed: boolean
    }

    export interface RouteTarget {
    name: string
    paramJson: string
    requireLogin: boolean
    }

    export function createRouteMessage(raw: PushPayload): RouteMessage {
    return {
    messageId: raw.messageId,
    routeKey: raw.routeKey,
    payload: raw.payload,
    receivedAt: Date.now(),
    consumed: false
    }
    }

    这个模型把“收到消息”和“跳转目标”分开。payload 保留原始业务参数,consumed 用来挡住重复送达,RouteTarget 则是解析后的页面意图。

    这类模型最好放在 models 或 common 中,页面、Service 和仓储都使用同一份类型,避免各层用字符串互相猜。

    RouteInbox 负责去重和暂存

    export class RouteInbox {
    constructor(private readonly store: RouteMessageStore) {}

    async accept(message: RouteMessage): Promise<void> {
    const old = await this.store.findById(message.messageId)
    if (old !== null && old.consumed) {
    return
    }
    await this.store.upsert(message)
    }

    async takeNext(): Promise<RouteMessage | null> {
    const message = await this.store.findFirstUnconsumed()
    if (message === null) {
    return null
    }
    await this.store.markConsumed(message.messageId)
    return message
    }
    }

    accept() 不直接跳页,只保证消息进入同一个队列。takeNext() 消费前先标记,避免页面栈跳转过程中再次处理同一条消息。

    Service 的目标不是把所有逻辑都塞进去,而是把跨页面、跨生命周期或需要持久化的判断集中起来。页面只表达用户动作。

    UIAbility 只把 Want 转成消息

    export class EntryAbility extends UIAbility {
    private routeInbox: RouteInbox = createRouteInbox()

    async onCreate(want: Want): Promise<void> {
    const payload = readPushPayload(want)
    if (payload !== null) {
    await this.routeInbox.accept(createRouteMessage(payload))
    }
    }

    async onNewWant(want: Want): Promise<void> {
    const payload = readPushPayload(want)
    if (payload !== null) {
    await this.routeInbox.accept(createRouteMessage(payload))
    AppStorage.setOrCreate<boolean>('route_inbox_dirty', true)
    }
    }
    }

    这里没有出现具体页面名。冷启动和新 Want 都只做同一件事:把推送意图交给收件箱,并通知 NavigationHost 稍后处理。

    页面层要控制展示和交互节奏,但不应该拥有底层事实。这样页面重建、横竖屏变化或返回前台时,都能重新从 Service 拿到可信结果。

    NavigationHost 在可跳转时消费收件箱

    @Entry
    @Component
    struct NavigationHost {
    @State routeInboxDirty: boolean = false
    private stack: NavPathStack = new NavPathStack()
    private routeInbox: RouteInbox = createRouteInbox()
    private resolver: MessageRouteResolver = new MessageRouteResolver()

    async onShown(): Promise<void> {
    await this.consumeRouteInbox()
    }

    private async consumeRouteInbox(): Promise<void> {
    const message = await this.routeInbox.takeNext()
    if (message === null) {
    return
    }
    const target = this.resolver.resolve(message)
    if (target.requireLogin && !isSignedIn()) {
    savePendingRoute(target)
    this.stack.pushPathByName('LoginPage', undefined)
    return
    }
    this.stack.pushPathByName(target.name, target.paramJson)
    }
    }

    跳转发生在 NavigationHost 可见之后,而不是通知回调里。登录态不足时保存 pending route,避免用户登录后只停在首页。

    实际项目里,很多问题不是第一次进入页面暴露,而是在冷启动、返回前台、页面重建或回调晚到时暴露。把这段生命周期补上,修复才算闭环。

    兜底页比静默失败更重要

    情况页面表现工程处理
    routeKey 不认识 跳到 MessageFallbackPage 展示消息摘要和刷新入口
    业务对象已删除 进入空结果页 提示订单或内容已失效
    未登录 保存 pending route 登录成功后继续跳转
    重复点击 RouteInbox 忽略已消费消息 页面栈不增加重复详情

    兜底路径不是可有可无。用户遇到异常时,页面至少要给出当前状态、可执行动作和可排查线索。

    排查时先搜直接跳转点

    rg n "pushPath|pushPathByName|replacePath|pushUrl" entry features common
    rg n "messageId|routeKey|notification|Push|Want" entry features common
    rg n "pendingRoute|route_inbox_dirty|markConsumed" entry features common

    第一组命令找旧路径,第二组命令找新边界,第三组命令看生命周期或回调位置。命中后不要只看字段名,要判断它是否仍在直接改页面状态。

    验证清单

    验证项预期结果
    冷启动点击推送 先到目标详情页,不被首页刷新覆盖
    前台连续点两次 只打开一个目标页
    未登录点击 登录后继续进入原目标页
    重复 messageId 第二次不会再次跳转
    未知 routeKey 进入兜底页而不是白屏

    这些验证最好在真机或模拟器里按顺序走一遍。只看源码很难发现冷启动、重启和回调晚到这类问题。

    常见问题和处理方式

    现象常见原因处理方式
    只到首页 UIAbility 处理后首页又覆盖栈 把 payload 写入 RouteInbox,首页不直接跳
    打开两个详情页 前台回调和 onNewWant 都跳了 统一消费入口并加 consumed
    登录后目标丢失 登录页没有 pending route AuthGate 保存 RouteTarget
    未知消息白屏 解析失败没有兜底 增加 MessageFallbackPage

    如果现象没有出现在表里,仍然建议按“输入来源 -> 中间状态 -> 持久化或页面展示 -> 失败兜底”的顺序排查。

    小结:推送点击先入箱,再跳转

    推送路由的关键不是把每个通知都写一个跳转分支,而是把入口收拢。通知、Want、前台点击都先进 RouteInbox,Resolver 决定目标,AuthGate 处理登录,NavigationHost 在合适时机操作页面栈。这样冷启动和前台路径才会有同一个结果。

    如果现象没有出现在表里,仍然建议按“输入来源 -> 中间状态 -> 持久化或页面展示 -> 失败兜底”的顺序排查。

    小结:推送点击先入箱,再跳转

    推送路由的关键不是把每个通知都写一个跳转分支,而是把入口收拢。通知、Want、前台点击都先进 RouteInbox,Resolver 决定目标,AuthGate 处理登录,NavigationHost 在合适时机操作页面栈。这样冷启动和前台路径才会有同一个结果。

    赞(0)
    未经允许不得转载:171主机测评 » 36-推送点开走错页-用消息路由收件箱承接冷启动
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址