踩坑··

UniApp uni-push:receive 里无条件 createPushMessage,iOS 会弹两次,要先判断 aps

照 uni-push 2.0 快速接入在 receive 里补上无条件 createPushMessage 后,iOS 即使 force_notification 为 true 仍会进 receive,点通知还会再来一条带 aps 的 receive,通知弹两次。判断写在常见问题里,快速接入完全没提。

照着 uni-push 2.0 快速接入指南 接在线通知,iOS 真机会踩进一个很隐蔽的坑:官方示例本身没有真正调用 createPushMessage,只是用注释提示「可在此回调里创建通知」;若你按提示在 receive 里补上无条件的 createPushMessage(),通知就会弹两次。

force_notification: true 挡不住。真正要用的 aps 判断,写在另一篇 常见问题 里,快速接入完全没提。

官方快速接入怎么写

接入指南 3.1 节的监听示例是这样的(节选;注意 uni.createPushMessage 未加括号,只是点名 API):

uni.onPushMessage((res) => {
  if (res.type == 'click') {
    console.log('点击消息', res.data.payload)
  }
  // 若云函数设置了 "force_notification":true,则会自动创建通知栏消息,不会触发此 receive。
  if (res.type == 'receive') {
    console.log('在线推送,接收到的消息内容', res.data)
    // 开发者可以自己在此回调,调用 createPushMessage 创建通知栏消息展示。
    uni.createPushMessage
  }
})

同一篇前面还写了两条路:

  1. 监听到消息后,自己调 uni.createPushMessage
  2. 服务端传 force_notification: true,客户端自动建通知栏,此时你监听不到消息内容,要点击后才有 click

按字面理解:force_notification: true 时 receive 根本不该来;要自己展示,就在 receive 里 createPushMessage。两条路互斥。

真机不是这样。

真机现象

服务端已经把 forceNotification: true 打到云函数,sendMessage 入参也是布尔 true。App 在前台时,iOS 日志仍然是:

{
  "type": "receive",
  "data": {
    "aps": null,
    "force_notification": true,
    "title": "通知标题",
    "content": "通知正文",
    "payload": { "code": "daily_care", "id": 1 },
    "unipush_version": "2.0"
  }
}

要点:

文档承诺真机
force_notification: true 不触发 receive照样触发,payload 里还能看到这个字段
receive 里直接 createPushMessage前台能补出通知栏,但点系统通知时会再弹一条
点通知只有 clickiOS 会 click + 一条带 aps 的 receive 几乎同时到

所以若按快速接入的注释,把无条件的 createPushMessage() 塞进所有 receive,点一次通知等于再造一条本地通知。用户看到的就是弹两次。

force_notification 也不会在应用页面上弹框,它最多让客户端去建系统通知栏。iOS 前台本来就不爱出 banner,很多人会误判「参数没生效」——日志里还有 receive 时,更不能拿「有没有 receive」当这个开关的唯一证据。

根因在常见问题里,不在快速接入

uni-push2.0 常见问题 的「iOS消息推送流程说明」写了安卓没有的行为:

与安卓消息流程不同的是,Ios 走了厂商离线通道,在消息有效期内,app 打开在前台时,透传内容还将通过个推通道传递给客户端;此时可在客户端透传回调中的参数 PushMessage 对象中获取 aps 属性值来判断是否是 APNs 下发的消息。

翻译成客户端逻辑:

通道什么时候来data.aps要不要 createPushMessage
个推在线透传App 在前台null / 没有要,否则前台没通知栏
APNs(厂商离线)后台/锁屏,或点系统通知伴随有对象不要,系统已经展示过

安卓走了厂商通道就不会再走个推,消息只到一次。iOS 会再补一条透传,所以必须靠 aps 区分「这是 APNs 已经弹过的」还是「纯在线透传、需要自己建栏」。

快速接入把 iOS 当成和安卓一样的「force_notification 生效就不进 receive」。常见问题里写了 aps,但:

  1. 放在 FAQ 中段的流程说明,不在快速接入的示例代码里
  2. 同一篇 FAQ 的「iOS在线收不到消息」又把快速接入那句复制了一遍:force_notification: true 时「监听不到消息内容」
  3. 跟着 40283 把接入做完的人,几乎不会翻到 40291 去找 aps

文档内部也打架。以真机为准:iOS 上 receive 会来,用 aps 过滤,不要用「有没有 receive」判断 force_notification 有没有生效。

正确写法

aps: null 仍然算在线透传,要用 != null 判断「有 APNs 载荷」,不能写成 if (data.aps) 这种把 null 和「没有」混在一起的假布尔。

function asPushData(data: unknown): Record<string, unknown> | null {
  if (data && typeof data === 'object')
    return data as Record<string, unknown>
  if (typeof data === 'string') {
    try {
      const parsed = JSON.parse(data) as unknown
      return parsed && typeof parsed === 'object'
        ? parsed as Record<string, unknown>
        : null
    }
    catch {
      return null
    }
  }
  return null
}

uni.onPushMessage((message) => {
  if (message.type === 'receive') {
    // 部分端上 message.data 可能是 JSON 字符串,先归一成对象再读 aps
    const data = asPushData(message.data)
    if (!data)
      return
    // 带 aps:APNs / 点系统通知伴随的 receive,再 create 会弹两次
    if (Reflect.get(data, 'aps') != null) {
      return
    }
    const title = data.title as string | undefined
    const content = data.content as string | undefined
    const payload = data.payload
    if (!content && !title)
      return
    uni.createPushMessage({
      title,
      content: content || title,
      payload,
    })
    return
  }

  if (message.type !== 'click')
    return
  // 只在 click 里解析 payload 跳转
})

click 才跳转。receive 只负责「纯透传要不要补通知栏」,不要在这里 navigateTo。

Android 前台要更容易出悬浮横幅,可以先把默认渠道 DcloudChannelID 的 importance 设成 4,再 createPushMessage。实际效果仍受系统限制,官方也承认不是所有机型都有浮动。

排查对照

日志含义动作
receive 且 aps 为 null个推在线透传createPushMessage
receive 且 aps 为对象APNs 或点通知伴随直接 return
click用户点了通知栏解析 payload 跳转
只有 receive、界面无反应没建本地通知,也没应用内 banner补上面的 receive 分支,不要先做自定义顶栏

不要用「还有没有 receive」证明 force_notification 失效。iOS 上它经常和 receive 同时存在。

小结

  • 快速接入(40283)的 receive 示例缺 iOS 过滤;force_notification: true 不会触发 receive 这句话在 iOS 上不成立
  • 真正的判断在常见问题(40291):用 PushMessage.aps 区分 APNs 和个推透传
  • aps != null 才跳过 createPushMessage;aps: null 仍要自己建栏
  • 点通知会 click + 带 aps 的 receive 一起来,无条件 create 就会弹两次
  • 跳转只放 click,receive 只处理展示