极光推送全攻略(下):uni-app 代码实现与 iOS 排查实战

开篇

上篇我们搞定了证书配置,这篇进入代码实战。

我们项目用 JPush 实现了完整的推送闭环:冷启动自动注册 → 登录绑定用户 → 通知点击跳转 → 退出解除绑定。整个过程踩了不少坑,尤其是 iOS 端,光 registrationId 为空的问题就排查了 4 轮,根因一个比一个隐蔽。

本文会完整呈现:

  • uni-app 中极光推送的架构设计
  • 核心代码实现(含关键设计决策)
  • 完整调用链路
  • 4 轮 iOS 问题排查全过程(这才是本文精髓)

插件版本:jg-jpush-u v1.2.8 | uni-app (Vue 3) | HBuilderX


一、架构设计

先看整体架构,了解每个文件扮演什么角色:

scss 复制代码
App.vue(应用入口)
  │  onLaunch → initJPush()                    // 设备级:注册APNs、获取registrationId
  │           → 已登录 → bindUser(userId, role) // 用户级:关联设备与账号
  │
  ├─ common/utils/push.js(推送核心模块,260行)
  │    │  直接 import 插件方法,不通过中间层
  │    │
  │    ├─ initJPush()               SDK 初始化
  │    ├─ handlePushEvent()         事件分发中枢
  │    ├─ bindUser()                登录后绑定别名 + 上报设备
  │    ├─ unbindUser()              退出时解除别名
  │    ├─ navigate()                推送点击 → 路由查找 → 跳转
  │    └─ reportRidIfLoggedIn()     回调兜底:SDK 延迟就绪时自动上报
  │
  ├─ common/utils/pushRoutes.js(通知路由映射)
  │    │  与消息列表页共用,统一维护
  │    └─ 6 种推送类型 → 患者端/康复师端跳转路径
  │
  ├─ pages/login/login.vue
  │    登录成功 → bindUser(userId, role)
  │
  └─ pages/my/my.vue
       退出登录 → unbindUser()

核心设计原则:

  1. 初始化是设备级的,绑定是用户级的 :initJPush 在 onLaunch 最早执行(注册 APNs、获取 registrationId),不依赖登录状态;bindUser 在登录后执行(关联设备与账号),二者独立
  2. 直接 import 插件方法 ,不通过 receiveJpush 中间注入,减少一层调用链
  3. #ifdef APP-PLUS 包裹所有原生调用,非 APP 端安全降级
  4. 所有别名/标签操作通过独立函数,统一的错误处理和日志

二、核心代码实现

2.1 push.js --- 推送核心模块

push.js 是整个推送系统的心脏,导出 9 个函数:

函数 用途 调用方
initJPush() SDK 初始化 App.vue onLaunch
handlePushEvent() JPush 事件回调入口 SDK 内部回调
bindUser(userId, role) 绑定别名 + 标签 + 上报设备 App.vue / login.vue
unbindUser() 解除别名绑定 my.vue
reportRidIfLoggedIn(rid) 回调兜底:自动上报设备 handlePushEvent 内部
requestNotificationPermission() 请求通知权限 外部按需调用
openNotificationSettings() 跳转系统通知设置 外部按需调用
checkNotificationEnabled(callback) 检测通知是否启用 外部按需调用

① 初始化 --- 对齐官方示例

javascript 复制代码
export function initJPush() {
  // #ifdef APP-PLUS
  _setDebug(true)                                    // 开发模式
  _setEventCallBack({ callback: handlePushEvent })   // ⚠️ 必须最先调用,否则事件丢失

  if (platform === 'ios') {
    _jpushInitPush({ appkey, channel, isProduction, advertisingId })  // iOS
  } else {
    _jpushInit(JPUSH_APPKEY)                         // Android / HarmonyOS
  }

  // #ifdef APP-ANDROID
  _requestPermission()                               // ⚠️ 仅 Android 需要显式请求
  // #endif
  // #endif
}

💡 为什么 setEventCallBack 必须最先调用? 插件内部会缓存事件,如果先 init 再设回调,SDK 可能瞬间返回 onRegister,而此时回调还没注册,事件就丢了。

② 用户绑定 --- 含 3 秒防抖

javascript 复制代码
export function bindUser(userId, role) {
  // 防抖:防止 App.vue 冷启动 + login.vue 登录成功短时间内重复触发
  _setAlias(seq, String(userId))    // 别名设为 userId(方便服务端按用户推送)
  _setTags(seq, [role])             // 标签为角色(patient / therapist)
  reportRegistration()              // 上报设备到服务端
}

③ 设备注册 --- 三层获取 rid

这是最容易出问题的地方。我们的 reportRegistration() 设计了三层兜底:

javascript 复制代码
function reportRegistration() {
  let rid = storage.get('jpush_registrationId')   // ① 优先读缓存(最快)
  if (!rid) rid = _getRegistrationId()            // ② SDK 直接获取(兜底)
  if (rid) doRegisterPush(rid)                    // ③ 上报到服务端
  else console.warn('等待 onRegister / onConnected 回调兜底')
}

④ 回调兜底 --- SDK 延迟就绪时自动补报

javascript 复制代码
// onRegister 或 onConnected(true) 触发时自动执行:
// saveRid() → storage → reportRidIfLoggedIn() → doRegisterPush(rid)

这个兜底机制解决了最常见的时序问题:用户登录比 SDK 初始化快,导致 reportRegistration() 时 rid 还没拿到。


2.2 pushRoutes.js --- 通知路由映射

所有推送通知类型的跳转逻辑集中在这个文件,push.js 和推送消息列表页共用同一份映射:

javascript 复制代码
export const PUSH_ROUTES = {
  THERAPIST_ACCEPT: {
    patient:   (bizId) => `/subcom-pkg/order/info?orderId=${bizId}`,
  },
  VIDEO_GUIDE_REMIND: {
    patient:   (bizId) => `/subcom-pkg/order/info?orderId=${bizId}`,
    therapist: (bizId) => `/subcom-pkg/appoint/appoint?id=${bizId}`,
  },
  HOME_SERVICE_REMIND: {
    patient:   (bizId) => `/subcom-pkg/order/info?orderId=${bizId}`,
    therapist: () => `/pages/index/index?tab=home-service`,
  },
  NEW_APPOINTMENT_VIDEO: {
    therapist: () => `/pages/index/index?tab=video`,
  },
  NEW_APPOINTMENT_HOME_SERVICE: {
    therapist: () => `/pages/index/index?tab=home-service`,
  },
}

💡 新增推送类型,只改这一个文件,push.js 的路由映射自动同步。


2.3 App.vue --- 仅 3 行,但时序很重要

javascript 复制代码
import { initJPush, bindUser } from "@/common/utils/push.js"

onLaunch() {
  // ① 最先:初始化推送(设备级,不依赖登录状态)
  initJPush()

  // ② 然后:检查登录状态
  if (!isLogin) {
    uni.reLaunch({ url: "/subcom-pkg/login/login" })
  } else {
    // ③ 已登录:绑定用户(用户级,关联设备和账号)
    bindUser(userInfo.userId, userInfo.loginRole)
  }
}

2.4 API 接口

javascript 复制代码
// service/api.js
export const registerPush = (data) => {
  return request({
    url: "/app/push/device/register",
    method: "POST",
    data,          // { registrationId, platform, deviceName }
    toast: false,  // 静默请求,不弹 toast
  });
};

三、完整调用链路

3.1 冷启动

scss 复制代码
App.vue onLaunch
  → initJPush()
    → _setDebug(true)           [仅 DEVELOPMENT]
    → _setEventCallBack(...)    注册 handlePushEvent
    → iOS:  _jpushInitPush({appkey, channel, isProduction})
       Android: _jpushInit(APPKEY) → _requestPermission()

JPush SDK 内部:
  → iOS: registerForRemoteNotifications → 系统弹窗「允许通知」
         → APNs 签发 deviceToken → 上传 JPush 服务器 → 返回 registrationId
  → Android: requestPermission → 系统弹窗 → registrationId
  → onRegister 回调触发 → handlePushEvent → saveRid() → storage

3.2 用户登录

scss 复制代码
login.vue
  → bindUser(userId, role)
    → 防抖检查(3秒内相同 userId+role 跳过)
    → _setAlias(seq, userId)      别名 = userId
    → _setTags(seq, [role])       标签 = ["patient"] 或 ["therapist"]
    → reportRegistration()
      → storage 读 rid → 有值: POST /app/push/device/register ✅
      → 无值: 等待 onRegister 回调兜底

3.3 SDK 延迟就绪(兜底路径)

scss 复制代码
onConnected(true) 回调触发
  → handlePushEvent({ eventName: 'onConnected', eventData: 'true' })
    → rid = _getRegistrationId()
    → saveRid(rid)                 存 storage
    → reportRidIfLoggedIn(rid)     已登录 → 自动上报 ✅

3.4 用户退出

scss 复制代码
my.vue handleClear
  → unbindUser()
    → _deleteAlias(seq)   解除别名绑定
    → ⚠️ 保留 jpush_registrationId(设备级标识,不清除!)

3.5 推送点击跳转

scss 复制代码
JPush SDK → onClickMessage 回调
  → handlePushEvent → navigate(eventData)
    → 解析 { type, id, bizId, recordId }
    → recordId 存在 → markPushRead() 标记已读
    → ROUTE_MAP[type] 查找
      ├─ 找到 → 根据 loginRole 选 patient 或 therapist 路由 → uni.navigateTo ✅
      └─ 未找到 → 告警日志 → 兜底跳转推送消息列表

四、平台差异处理

这是 uni-app 开发中最需要注意的部分。iOS 和 Android 的 JPush API 有不少差异:

事项 iOS Android
初始化方法 initPush({appkey, channel, isProduction, advertisingId}) init(appKey)
AppKey 传参 initPush 参数中传入 init 参数中传入
通知权限弹窗 initPush 内部自动触发 需显式调用 requestPermission()
isProduction 参数 ✅ 需要(决定 Sandbox/Production) ❌ 不涉及
requestPermission 方法不存在! ✅ 存在
isNotificationEnabled ❌ 不存在 ✅ 存在
goToAppNotificationSettings ❌ 不存在 ✅ 存在
条件编译 #ifdef APP-IOS #ifdef APP-ANDROID

💡 核心教训 :iOS 不需要也不能调用 requestPermission()。接下来的排查实录会告诉你,调用了会发生什么。


五、4 轮 iOS 问题排查实录 ⭐

这是我们整个推送开发过程中最曲折的部分。6 个问题同时出现:

  1. 登录后 /app/push/device/register 接口没有被调用
  2. getRegistrationId() 始终返回空字符串
  3. setAlias / setTags 返回 code:6002(连接超时)
  4. onRegister 事件回调从未触发
  5. iOS「设置 → 通知」中找不到该 App
  6. App 从未弹出「允许通知」系统弹窗

下面是我们逐轮排查的完整过程。


第一轮:registrationId 在退出登录时被误清除了 🔍

关键日志:

ini 复制代码
[Push], reportRegistration rid =,   ← rid 为空!

定位过程 :日志显示 reportRegistration() 读取到的 rid 是空的。回溯代码,发现 unbindUser() 中有这么一行:

javascript 复制代码
export function unbindUser() {
  // ...
  uni.removeStorageSync('jpush_registrationId')  // ← 不应该清除!
}

根因分析registrationId设备级 推送标识,与用户账号无关。退出登录只需解除别名绑定即可,不应该清除 rid。清除后下次登录时 reportRegistration() 读到空值,直接跳过了上传。

修复

javascript 复制代码
export function unbindUser() {
  // ...
  // 注意:不清除 jpush_registrationId,因为它是设备级标识,与用户账号无关
}

状态:修了,但问题没完全解决 😅


第二轮:reportRegistration() 缺少 SDK 直接获取的兜底 🔍

修复第一轮后,大部分时候能正常上报了,但冷启动后马上登录的场景仍然偶尔失败。

根因 :Storage 里没有缓存 rid(首次安装),此时 reportRegistration() 只有 storage 读取一条路径,读不到就放弃了。

修复:增加 SDK 直接获取的兜底:

javascript 复制代码
function reportRegistration() {
  let rid = uni.getStorageSync('jpush_registrationId')

  // 兜底:直接从 SDK 获取
  if (!rid && api.getRegistrationId) {
    try {
      rid = api.getRegistrationId()
      if (rid) { uni.setStorageSync('jpush_registrationId', rid) }
    } catch (e) { /* 静默失败 */ }
  }

  if (rid) {
    doRegisterPush(rid)
  } else {
    console.warn('rid 未就绪,等待 onRegister / onConnected 回调兜底')
  }
}

状态:冷启动场景好转了,但还有更隐蔽的问题...


第三轮:iOS 调用不存在的 requestPermission 导致初始化崩溃 🔍🔍🔍

关键日志:

ini 复制代码
极光推送初始化失败,App 将继续运行(无推送功能):
  method call failed: {
    class = UTSSDKModulesJgJpushUIndexSwift;
    name = "s_requestPermissionByJs";    ← 方法不存在!
  }

定位过程 :日志明确指出 s_requestPermissionByJs 方法不存在。检查 App.vue,发现我们无脑对 iOS 也调用了 requestPermission()

深挖根源 :直接去看插件的 iOS 源码(/uni_modules/jg-jpush-u/utssdk/app-ios/index.uts),确认各方法在三端的支持情况:

方法 iOS Android
init ---
initPush ---
requestPermission 根本不存在
isNotificationEnabled ❌ 不存在
goToAppNotificationSettings ❌ 不存在

原来 iOS 的通知权限是由 jpushInitPush 内部调用 JPUSHService.register(forRemoteNotificationConfig:delegate:) 自动触发系统弹窗的,根本没有也不需要手动调用 requestPermission

更糟的是,这个调用抛出的异常被外层 try-catch 捕获后,导致后续所有初始化代码(包括轮询逻辑)全部被跳过------整个推送模块静默失效了

修复

javascript 复制代码
// App.vue
// #ifdef APP-ANDROID
requestPermission();    // 仅 Android 编译时包含这行
// #endif
// iOS 编译产物中完全不包含此调用

状态:代码修好了,但通知弹窗还是不出来... 😤


第四轮:Provisioning Profile 缺少推送权限(终极根因!)🔍🔍🔍🔍

代码都修完了,iOS 依然不弹通知权限弹窗,设置里也找不到这个 App。

根因 :HBuilderX 自定义调试基座的 Provisioning Profile 中,Push Notifications 能力没有被激活。registerForRemoteNotifications() 调用后系统静默失败------不弹窗、不报错、不在设置中显示。

这就是一个配置缺失 + 代码无感知的死锁:

  • 代码:API 调用正确 ✅
  • 证书:推送证书上传到极光 ✅
  • Profile:缺少 aps-environment entitlement
  • 表现:静默失败,没有任何错误提示

解决:在 Apple Developer 后台为 App ID 勾选 Push Notifications → 重新生成 Provisioning Profile → 用新 Profile 重新打自定义基座。

💡 这就是为什么上篇反复强调:创建 App ID 时必须勾选 Push Notifications,而且证书配置完后 Profile 必须重新生成。

状态:终于弹窗了!🎉 getRegistrationId() 返回正常!code:6002 消失!


六、正确的推送初始化流程图

scss 复制代码
┌─ App 冷启动 ──────────────────────────────────────────────┐
│                                                            │
│  ① setEventCallBack()   注册事件回调(必须最先)            │
│  ② initPush()           SDK 初始化                        │
│     ├─ iOS: jpushInitPush 内部触发 registerForRemote...    │
│     │      → 系统弹窗「允许通知」(需 Profile 有推送权限)   │
│     │      → 用户允许 → APNs 签发 deviceToken               │
│     │      → 上传至 JPush 服务器 → 返回 registrationId      │
│     │      → onRegister 回调 → saveRid() 存 storage         │
│     └─ Android: 显式调用 requestPermission() 弹权限弹窗     │
│                                                            │
├─ 用户登录 ─────────────────────────────────────────────────┤
│                                                            │
│  ③ bindUser(userId, role)                                 │
│     → setAlias + setTags                                   │
│     → reportRegistration()                                 │
│        → storage 读 → SDK 直接获取 → POST /app/push/... ✅ │
│                                                            │
├─ SDK 延迟就绪(兜底)───────────────────────────────────────┤
│                                                            │
│  ④ onConnected(true) 回调                                  │
│     → getRegistrationId() → saveRid()                      │
│     → reportRidIfLoggedIn() → 已登录则自动上报 ✅           │
│                                                            │
├─ 用户退出 ─────────────────────────────────────────────────┤
│                                                            │
│  ⑤ unbindUser()                                           │
│     → deleteAlias 清除别名                                  │
│     → ⚠️ 保留 jpush_registrationId(不清除!)              │
│                                                            │
└────────────────────────────────────────────────────────────┘

七、关键注意事项

  1. registrationId 不是本地值 --- 需要 JPush SDK 连接服务器后才返回,getRegistrationId() 在 SDK 未就绪时返回空是正常现象
  2. onConnected 回调是兜底路径 --- reportRidIfLoggedIn() 会在 SDK 延迟就绪后自动补报设备
  3. setEventCallBack 必须在 init 之前 --- 否则可能丢失事件
  4. iOS 通知权限弹窗只弹一次 --- 用户拒绝后需引导去系统设置手动开启
  5. code:6002 = SDK 连接超时 --- 排查顺序:证书是否配置 → 网络是否通 → 环境是否匹配
  6. requestPermission 是 Android 独有 --- iOS 调用会直接抛异常,必须用条件编译隔开
  7. 新增推送类型只需改 pushRoutes.js --- push.js 和消息列表页共用同一份映射
  8. iOS 模拟器不支持推送 --- 所有推送测试必须在真机上进行
  9. 退出登录不要清除 registrationId --- 它是设备级标识,和用户账号无关

八、修改历史回顾

阶段 问题 修复
1 unbindUser 错误清除 rid 移除 storage 清除,rid 是设备级标识
2 reportRegistration 无 SDK 兜底 增加 getRegistrationId() 直接获取
3 iOS 调不存在的 requestPermission #ifdef APP-ANDROID 包裹
4 函数重复定义 统一为 reportRidIfLoggedIn
5 单一 try-catch 阻塞全流程 拆分为独立 try-catch
6 中间层注入绕一圈 push.js 直接 import 插件
7 bindUser 短时间重复调用 加 3 秒防抖
8 ROUTE_MAP 冗余路由 精简为仅 pushRoutes.js 中的类型

总结

极光推送在 uni-app 中的集成,代码层面并不复杂(push.js 核心就 260 行),真正的难点在 iOS 证书配置跨平台 API 差异

如果你也在接入极光推送,建议按这个顺序排查问题:

  1. 证书流程走通了没? → 上篇的 8 步操作逐一核对
  2. 代码用条件编译隔开了没?requestPermission 绝不能出现在 iOS 产物中
  3. 回调兜底加了没?onConnected 回调中自动补报 rid
  4. 退出登录没清除 rid 吧? → 设备级标识不要乱删

搞定了这两篇的内容,你的 uni-app 应用就能稳定收发推送了。


标签:uni-app iOS 极光推送 JPush 推送 前端 Vue3 跨端开发

相关推荐
时光足迹1 小时前
极光推送全攻略(上):被iOS证书折磨了三天,我写了一份前端也能看懂的避坑指南
前端·ios·uni-app
疯狂的魔鬼2 小时前
一个"懂分寸"的文本省略组件是怎样炼成的
前端·vue.js·设计
裕波2 小时前
AI 正在重写应用开发。Vue 与 Vite,给出新的答案。
javascript·vue.js
妙码生花2 小时前
现代前端的极致性能 icon 加载方案(死磕成功版)
前端·vue.js·typescript
用户2136610035726 小时前
Vue2脚手架工程化与Axios集成
前端·vue.js
用户83134859306986 小时前
Cesium实现黄昏效果:按钮一键控制打开/关闭黄昏效果,滑块拖动实时控制黄昏浓烈度
vue.js·cesium
Cobyte7 小时前
21.Vue Vapor 组件的实现原理
前端·javascript·vue.js
橙某人7 小时前
LogicFlow 工作流撤销与重做:从「全量快照」到「命令模式」🎯
前端·vue.js
ZhengEnCi18 小时前
Q04-Vite禁用CSS代码分割-解决生产环境样式加载顺序混乱问题
前端·vue.js·vite