UniApp uni-push:receive 里无条件 createPushMessage,iOS 会弹两次,要先判断 aps
照着 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
}
})
同一篇前面还写了两条路:
- 监听到消息后,自己调
uni.createPushMessage - 服务端传
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 | 前台能补出通知栏,但点系统通知时会再弹一条 |
点通知只有 click | iOS 会 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,但:
- 放在 FAQ 中段的流程说明,不在快速接入的示例代码里
- 同一篇 FAQ 的「iOS在线收不到消息」又把快速接入那句复制了一遍:
force_notification: true时「监听不到消息内容」 - 跟着 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 同时存在。