1. 动手之前,先看懂 UniPush 的完整链路
1.1 一个 App 的推送为什么是件麻烦事
先说你早晚会撞上的问题:App 一退到后台,系统就把网络连接回收了,你在服务端拼命发消息,客户端却根本不知道该去收。iOS 更直接,普通应用连常驻长连接都不允许,想把消息送到用户眼前,必须借助系统级通道。Android 虽然开放,但国内各种定制系统把后台限制做得五花八门,一个进程想常驻,比上班摸鱼还难。
UniPush 的思路是:不再让 App 自己维持一条长连接,而是把“消息要不要展示在系统通知栏”“消息什么时候触达”这些事交给系统级通道去处理。App 在线时走在线通道,App 离线或被杀时走手机厂商的推送通道。这样用户看到的是系统通知,而不是一个必须先打开 App 才能收到的“假通知”。
这本配置指南,我按“账号准备 → 厂商通道 → manifest 配置 → 客户端接入 → 服务端发送”五层来讲。你只要跟着每一层把参数填对,整个链路就能通。
1.2 UniPush 2.0 与旧版 1.0 的区别,先避开过时教程
网上搜“uni-app 推送”,大概率会翻到plus.push开头的老代码,那是 UniPush 1.0 时代的写法。1.0 把在线通道和厂商离线通道拆得很开,发消息时还要区分“在线发透传”还是“离线发通知”,接口又多又绕。
我现在项目里一律用 UniPush 2.0,它对外暴露的是uni.getPushClientId、uni.onPushMessage这一套。前端只需要拿一个cid(ClientID),后端再拿cid去调推送接口,剩下的在线、离线路由由服务端自动处理。这条升级线需要注意:很多老教程里的plus.push代码在新版本里虽然能跑,但控制台、消息记录、厂商回调都已经翻了新,继续对着老文档配置,你会越配越晕。
再强调一个很容易混淆的点:UniPush 2.0 里的token,和 OAuth 登录的token完全是两码事。这里的 token 是服务端调推送接口时临时换来的访问令牌,一般几分钟就会过期;而登录 token 是业务系统的会话凭证。混用会导致后台怎么调都报 401。
1.3 配置过程中绕不开的三组识别信息
整个配置过程,你会反复填三类“ID”,先把它们焊死在脑子里,排错时不用到处找。
| 信息名称 | 在哪里拿 | 主要用途 |
|---|---|---|
| DCloud AppID | 开发者中心创建应用时自动生成 | uni-app 的 manifest.json 对应字段,全局标识 |
| AppKey / AppSecret | UniPush 推送控制台开通服务后生成 | 服务端调用推送接口时用于签名鉴权 |
| ClientID(cid) | App 客户端运行后由推送 SDK 生成 | 服务端做定向推送时标识一台设备 |
还有一个日常被当成“ID”的字段:Android 应用包名(applicationId)。它在 HBuilderX 打包界面填写,但必须和你去华为、小米等平台登记应用时用的包名完全一致。很多配置流程走到一半发现离线推送效果时好时坏,问题就出在“打包时换过包名”这件事上。
2. 开发者中心到推送服务开通:先有通行证,才能谈配置
2.1 创建 DCloud 应用并完成实名认证
一切配置的起点都在 DCloud 开发者中心。你先注册一个账号,然后做实名认证,这一步不是走形式:推送通知属于消息触达能力,平台需要明确责任主体,实名不过关,后续开通 uni-push 会直接卡住。
认证通过后,进入控制台创建应用。填应用名称、选择类型,提交后会拿到一个__UNI__xxxxxxx格式的 DCloud AppID。这个 AppID 要原样填进项目的manifest.json -> 基础配置 -> Vue 应用 AppID里,前端工程跟推送服务的绑定关系从这一格开始。
我见过不少人不把 AppID 当回事,随手复制别人项目的 ID 进去调试,结果getPushClientId能返回,但服务端一发就报设备不存在。原因很简单:推送后台的系统里压根没有你这台设备。所以这一步宁可慢一点,确认控制台和本地工程是同一个 ID。
2.2 开通 uni-push 服务,并区分套餐能力
在开发者中心的“uni-push”或“消息推送”菜单里,需要先手动开通服务。开通时会看到免费版和按量付费的选项,我建议测试阶段直接开免费版,等验证完到达率和业务场景后再决定要不要升付费。
开通后,系统会为这个 App 生成一组推送专属的AppKey、AppSecret。注意它跟 DCloud AppID 不是一个概念:AppID 是应用在 DCloud 的身份,AppKey/AppSecret 是服务端调用推送 API 的凭证。生产中 AppSecret 只能放后端,绝不能写进前端代码或打包进应用,否则任何人都能伪造你的推送。
开通完成后,记得把AppKey和AppSecret存到后端的配置中心或环境变量里。我习惯同时建一个专门存推送凭证的表,每个环境一套,避免测试环境把消息推到生产设备上。
2.3 iOS 推送证书:容易漏掉的必要项
如果你要接 iOS 端,DCloud 控制台的 uni-push 配置里还有一处独立设置:上传 iOS 推送证书或 .p8 密钥。这个文件要去苹果开发者后台生成,对应你的 Bundle ID,开通 Push Notifications 能力后导出。
有些教程把 iOS 证书放在“厂商通道”章节里讲,容易误导人。iOS 没有像 Android 那样的多个厂商后台可选,它就是 APNs 一个通道,在 UniPush 控制台把证书填好即可。证书有两类:开发证书.p12和发布证书.p12,千万别传反。我踩过的坑是:用开发证书打包 TestFlight 包,结果 AppStore 版永远收不到通知,因为发布环境的推送走的是另一份证书。
上传后控制台一般会显示证书有效期。证书过期是一个非常“安静”的故障:后台消息显示成功,iPhone 就是不响。运维上最好提前一个月在日历里标记证书到期提醒。
3. 厂商推送通道的申请与回填细节
3.1 华为通道:AppID、AppSecret 与证书指纹
华为离线通道是安卓版推送效果好坏的分水岭。登录华为开发者平台,创建一个移动应用,开通推送服务,之后能在应用信息里拿到AppID和AppSecret。同时还要配置 SHA-256 证书指纹,这个指纹必须和你最终打包用的证书一致。
我之前调试时图省事,先在华为后台填了 debug 证书的指纹,HBuilderX 里用发行证书打了一个 release 包,结果在线推送一切正常,一杀后台就彻底沉默了。查到最后,发现是华为推送服务校验证书指纹失败,直接拒绝了离线消息。把指纹改成正式证书后,问题立刻消失。
Android 打包证书指纹的生成方式不复杂,但不同证书工具的输出格式不一样。无论用 keytool 还是第三方工具,拿到指纹后先把冒号去掉再填,华为后台通常要求不带分隔符的大写十六进制字符串。这一步错了基本不会有报错提示,属于“谁都看不出问题,但就是到不了”的典型类型。
3.2 小米、OPPO、vivo 的差异处理
国内用户量大的主流机型基本被几大厂商覆盖,配置参数各不一样。我把常见厂商必需项整理成了一个对照表,操作时对着看。
| 厂商 | 必填参数 | 特殊要求 |
|---|---|---|
| 华为 | AppID、AppSecret | SHA-256 证书指纹,包名严格匹配 |
| 小米 | AppID、AppKey、AppSecret | 需要配置消息回执,否则后台显示到达但没回执 |
| OPPO | AppKey、AppSecret、MasterSecret | 通知渠道、角标权限另设 |
| vivo | AppID、AppKey、AppSecret | 推送服务开关需单独打开 |
| 魅族 | AppID、AppKey | 要求权限声明完整 |
小米的回执是可以独立配置的:在小米开放平台申请“消息回执”权限,拿到回执 URL,填到小米推送服务里。有了回执,服务端才能从“推送到达”和“用户看到”两个维度区分真实效果。不做回执不会让消息消失,但你没法判断用户到底点没点。
OPPO 的角标权限要单独申请,否则通知栏消息到了,应用角标数字不会变。vivo 没有类似角标限制,但推送服务开关默认可能是关闭状态,忘了打开的话所有 vivo 离线消息都会被静默丢弃。
3.3 为什么离线推送“突然失效”:渠道与开关
很多人配置完厂商通道,测试当天一切正常,过了两天发现离线消息收不到了,第一反应是后台参数被改了。其实大概率是 Android 8.0 之后的通知渠道在做怪。
Android 8.0 引入了 Notification Channel,App 必须在推送消息里声明一个渠道 ID,用户也可以在这些渠道上手动关闭通知。如果消息没带渠道 ID,或者渠道 ID 在系统里不存在,系统可能直接丢弃。UniPush 控制台或服务端推送参数里要指定一个渠道 ID 和对应的渠道名称、描述。渠道名称会直接显示在系统设置里,建议写成用户容易理解的文案,比如“订单通知”,而不是技术味十足的“msg_channel”。
另一个很容易被忽视的开关是用户权限:用户在系统设置里手动关掉“通知权限”后,任何通道都无法突破。排查这类问题时,先让用户在设置里确认 App 通知开关确实开着,再去查厂商后台。
4. manifest 配置与打包环节最容易翻车的地方
4.1 在 manifest.json 中开启 Push 模块和 uni-push 模块
打开 HBuilderX 的项目,进入manifest.json,切到“App 模块配置”。这里要勾上Push,如果你用的是 UniPush 2.0,通常还会看到uni-push相关选项,一起勾上。单勾一个Push往往不够,跑起来怎么都监听不到消息,原因就是 2.0 的初始化入口没被编译进去。
还需要在“App 常用其他设置”里确认 Android 包名和 iOS Bundle ID。有些项目是复制模板改的,这里还留着别人家的包名,打包时能成功安装,但厂商平台校验身份时会发现“请求包名不属于这个应用”。
如果你只是用 HBuilderX 自带的“标准基座”运行调试,那恭喜你会踩一个大坑。标准基座只包含基础模块,默认是不带 Push 模块的,所以你必须先制作自定义调试基座,在制作基座时勾选推送模块,再用这个基座运行。否则你在onLaunch里调getPushClientId,回调永远是 fail。
4.2 通知图标、横幅样式与角标配置
推送配置里不只有参数,还有视觉相关的资源。通知栏小图标要准备一个纯白透明背景的 PNG,尺寸不要太大,系统会自己压缩。很多同学传一张带圆角的彩色图上去,通知栏里显示成一个怪异的方块,用户看到第一眼就想卸 App。
横幅样式和桌面角标,Android 厂商各有一套。小米、OPPO 对横幅样式有单独的控制字段,服务端发通知时没有指定样式类型,部分机型会按系统默认样式展示。角标数字则往往需要厂商通道单独授权,OPPO 平台要求上传角标权限申请说明,审核通过后才会开放。
iOS 方面没有小图标的概念,APNs 展示的是 App 本身的图标。角落标也需要在 Xcode 或原生工程里开启对应能力,uni-app 云打包时不能直接改原生工程,需要额外配置或使用原生插件扩展。
4.3 云打包与离线打包的配置差异
HBuilderX 云打包是最省事的:菜单选择“发行 -> 原生 App-云打包”,上传证书、填包名、生成安装包。整个过程里要注意“打包类型”别选错,测试用“自定义调试基座”,真正验证离线推送要用“正式打包”。用测试基座打包,部分厂商 SDK 调不起离线通道,因为应用签名和厂商后台登记的不一致。
离线打包的话,等于你自己维护一个原生工程。这种情况下,除了 manifest 里要配置,原生端的 AndroidManifest.xml 里还得手动声明厂商推送服务、接收器、权限。少了任何一项,SDK 初始化时可能不报错,但推送就是静默失败。我见过最隐蔽的一次事故:原生工程里build.gradle引用了多个厂商 SDK,其中两个依赖版本冲突,导致初始化链路中断,日志里全是一些无关的 warning。
所以我的明确建议是:个人项目或小团队直接用云打包,把精力放在业务逻辑上;只有当你确实需要自制原生插件或深度定制推送行为时,再碰离线打包。
5. 客户端接入:从获取 ClientID 到前后台消息分派
5.1 获取 ClientID:时机和生命周期问题
UniPush 2.0 的客户端接入逻辑很简单,核心就是先拿到cid。把下面这段放到App.vue的onLaunch里:
// #ifdef APP-PLUS uni.getPushClientId({ success(res) { const cid = res.cid console.log('推送 ClientID:', cid) // 上报到自己的服务端,和用户账号绑定 uni.request({ url: 'https://api.example.com/user/bind-cid', method: 'POST', data: { cid } }) }, fail(err) { console.error('getPushClientId 失败', err.code, err.message) // 简单延时重试,不要死循环 setTimeout(() => this.getCid(), 3000) } }) // #endif调用时机越早越好,最好放在onLaunch。因为cid是一台设备在推送系统里的“门牌号”,服务端要定向推送,必须先知道这个门牌号并把门牌号和用户账号做绑定。
这里有个生命周期问题:cid在应用覆盖安装、卸载重装后可能变化。也就是说不能把它当成永久设备 ID 硬编码,必须每次都重新获取并上报。我做用户画像时遇到过一个情况:同一个人换了台新手机,服务端库里还留着旧手机的cid,业务侧就往旧cid发消息,结果自然没到达。处理办法是客户端上报时带上用户 ID,服务端做唯一约束,保证一个账号始终只对应最新的一批cid。
5.2 监听在线透传与通知栏消息
拿到cid后,第二步是注册消息监听:
uni.onPushMessage((res) => { console.log('onPushMessage:', JSON.stringify(res)) // 常见字段: type、payload、title、content })res.type在不同场景下取值不同:App 在运行状态下收到操作系统通知栏展示出来,通常能收到一次回调;用户点击通知栏消息进入 App,又会触发一次带 payload 的点击事件。把这两条事件理清楚很重要,因为你的业务跳转就靠点击事件里的payload。
还要提防重复处理。有的开发者把跳转逻辑直接写在监听回调里,用户点一次通知,回调执行两遍,页面就被重复打开。稳妥做法是回调里先解析payload,生成一个事件标记,页面侧判断是否已经处理过再跳转。
消息处理的核心原则是:通知展示归系统,业务响应归代码。系统负责把通知栏展示出来,你的代码只负责用户点击之后,该往哪个页面带什么参数。
5.3 App 被杀死后点击通知栏的冷启动传参
最复杂的场景是 App 进程被杀掉,用户直接点通知栏消息把 App 拉起来。这种冷启动状态下,onPushMessage的点击回调可能触发得很早,首屏页面还没准备好,跳转参数一赋值就被后续页面生命周期覆盖。
我的做法是把参数先缓存到全局,再让页面主动去取:
// App.vue onLaunch 里 uni.onPushMessage((res) => { if (res.type === 'click') { try { const payload = typeof res.payload === 'string' ? JSON.parse(res.payload) : (res.payload || {}) uni.$vm.$globalData.pushPayload = payload } catch (e) { uni.$vm.$globalData.pushPayload = null } } }) // 首屏页的 onLoad const payload = uni.$vm.$globalData.pushPayload if (payload && payload.page) { uni.navigateTo({ url: payload.page }) }这种旁路缓存虽然土,但非常可靠。后续如果你接了更复杂的路由体系,可以把payload交给路由组件统一分发,避免页面和推送逻辑互相纠缠。
还有一个小技巧:在处理点击跳转时,页面路径尽量用相对路径,并且确认这个路径在pages.json里注册过。我接过一个需求,后端配置的跳转地址是/pages/order/detail,结果开发环境正常,一测试就白屏,最后发现是路径前多了个反斜杠导致页面没注册成功。
6. 服务端推送接口调用与全链路排错
6.1 推送 API 的签名与鉴权逻辑
服务端调 UniPush 推送接口,先要明白鉴权流程:用AppKey、AppSecret加当前毫秒时间戳拼出一个签名串,算法是 SHA256(AppKey+ timestamp +AppSecret)。签名串和 timestamp 一起发过去换 token,后面真正的推送请求带上这个 token。
Node.js 里算签名是这样:
const crypto = require('crypto') function buildSign(appkey, appsecret) { const timestamp = Date.now() // 注意必须是毫秒 const raw = `${appkey}${timestamp}${appsecret}` const sign = crypto .createHash('sha256') .update(raw) .digest('hex') return { sign, timestamp } }常见的 401 错误,九成出在时间戳单位或签名拼接顺序上。Date.now()得到的是毫秒,别用什么秒级时间戳,签名算法会直接验证失败。另外AppSecret因为密钥长度不同,拼接时要注意不要带空格和换行。
6.2 一个可直接跑通的 Node.js 发送示例
换到 token 后,我会用一个封装函数把单推请求发出去。下面这段是简化但能跑的版本:
const url = 'https://推送接口域名/v2/{appId}/push/single/cid' const body = { request_id: crypto.randomUUID(), audience: { cid: ['目标设备的cid'] }, push_message: { notification: { title: '订单已发货', body: '点击查看物流信息', click_type: 'startapp' }, transmission: { transmission_type: 1, transmission_content: JSON.stringify({ page: '/pages/order/detail', orderId: '20240001' }) } } } fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', token: accessToken }, body: JSON.stringify(body) })注意request_id每次推送必须唯一。用 UUID 或数据库自增主键都行,但绝对不要在重试时复用同一个 ID,否则服务端可能判定成重复请求。push_message里notification是系统通知栏展示的内容,transmission是透传给客户端的自定义数据。
很多人在开发阶段犯的一个错,是把完整后端逻辑写在云函数或定时任务里,本地没有现成推送环境时没法联调。我习惯先在本地写好一个独立脚本,用真实测试设备的cid跑一遍,确认返回成功再迁到线上服务。
6.3 从推送记录看问题出在前端、厂商还是后端
推送失败时,先不要怀疑人生,按这张表逐项排查。
| 现象 | 最可能的原因 |
|---|---|
| 在线收不到透传 | manifest 没勾 Push 模块,或用标准基座调试导致接口未注册 |
| 离线收不到通知 | 厂商后台参数不匹配、包名不一致、证书指纹错误 |
| 点击通知栏没跳转 | payload解析异常,或冷启动时序导致参数丢失 |
| 接口返回 401 | 签名串顺序错、timestamp 用了秒、AppSecret 带多余字符 |
| 接口返回 404 | cid不属于当前 AppID,或设备已卸载但服务端还在发 |
UniPush 控制台的消息列表是最直接的证据来源。服务端返回的成功只代表“推送请求已接受”,不代表设备已经收到;控制台里的“任务ID”“到达数”“点击数”才是真实结果。某开发者曾经给我看过一个案例:后台显示“发送成功”,但到达数是 0。最后发现是因为他用了另一台测试机的cid发起单推,那台手机根本没安装这个正式包。
6.4 频率限制、静默通知与定时推送的注意点
厂商通道对推送频率有隐形约束。短时间内给同一台设备发大量通知,部分厂商会判定为骚扰,直接降级或封禁推送能力。运营活动里“轰炸式”群发最容易踩这条线。我的建议是:严格控制单用户日消息条数,重要消息走通知栏,普通消息走透传,不要一上来就把通知权限用尽。
静默通知也是一个需要谨慎的功能。静默通知指的是不发任何展示内容,只把数据悄悄送给 App,很多 App 用它做后台预热。厂商系统对静默通知同样有限流,用得太猛,通道可能会被系统冻结,导致后续所有正常通知都“失声”。我在项目里只用静默通知做紧急配置文件同步,量级非常低。
定时推送如果由前端写定时器处理,App 一死就全断。正确姿势是服务端用任务队列在指定时间调推送接口。如果只需要固定场景的定时推送,UniPush 控制台也提供定时任务入口,不写代码也能配。
最后分享一套验收顺序
整个流程走完之后,我强烈建议按“在线透传 → 离线通知 → 点击跳转 → 服务端联调”四步做验收。第一次先跑通在线透传,确认cid能拿到、监听能触发;然后装一个正式包,杀进程测离线通知;再核对点击事件里的payload跳转;最后才把服务端接进来,用真实业务消息做全链路验证。
这个顺序看起来多了一步,实际上能帮你省掉大量“查不出原因”的时间。每一层功能单独测过,出了问题你一眼就能定位是哪一层的锅。配置这事看着繁琐,但基本原则就一条:让每一层都拿着正确的参数去跟下一层对话。参数对齐了,UniPush 的配置在你手里自然就是“有手就行”。