Expo推送通知:令牌获取到消息送达的全链路 Expo推送通知令牌获取到消息送达的全链路【免费下载链接】expoAn open-source framework for making universal native apps with React. Expo runs on Android, iOS, and the web.项目地址: https://gitcode.com/GitHub_Trending/ex/expo本地模拟器上发的推送永远收不到线上跑着跑着又有用户的令牌突然失效——Expo 推送的坑大多不在 API 本身而在消息经过哪些节点、每个节点由谁兜底这件事上。这篇直接跑通令牌、发送与三种应用状态下的分发代码均可独立运行。 先看清消息流向配置才有意义一条 Expo 推送的完整路径是这样的你的服务器 → Expo Push Service → FCM(Android) / APNs(iOS) → 设备 ↓ 按应用当时所处状态分叉 前台 → 你的 JS 接管展示 后台 → 系统直接展示 已杀死 → 系统展示点击时再唤醒你的 JS推送令牌push token就是这条链路上的收件地址它把 Expo Push Service 与具体设备绑定。后面所有配置——projectId、原生凭据、通知渠道——都是为了保证这个地址有效、消息能被系统放行。状态分叉决定了后面模块四的写法前台行为由你的代码说了算后台和已杀死状态由系统说了算。参考推送通知前置概念。 模块一获取推送令牌这个模块解决收件地址从哪来。令牌不是设备固定的它由权限、projectId和设备三者共同决定缺一个都拿不到。先写注册函数它封装了权限检查与令牌请求// src/push/register.ts import Constants from expo-constants; import { getExpoPushTokenAsync, getPermissionsAsync, requestPermissionsAsync, } from expo-notifications; export async function registerForPushNotificationsAsync(): Promisestring { // ← 关键先查后申已授权时不再弹系统对话框 const { status: existing } await getPermissionsAsync(); let finalStatus existing; if (existing ! granted) { const { status } await requestPermissionsAsync(); finalStatus status; } if (finalStatus ! granted) { throw new Error(Notification permission not granted); } // ← 关键令牌必须绑定 projectId服务端按它路由消息 const projectId Constants.expoConfig?.extra?.eas?.projectId ?? Constants.easConfig?.projectId; if (!projectId) { throw new Error(Project ID not found); } const token (await getExpoPushTokenAsync({ projectId })).data; return token; }再把它包成 Hook并处理 Android 上令牌会轮换的问题。真实项目中拿到新令牌后要同步到你自己的后端这里只打印演示// src/push/usePushToken.ts import { addPushTokenListener, getDevicePushTokenAsync } from expo-notifications; import { useEffect, useState } from react; import { registerForPushNotificationsAsync } from ./register; export function usePushToken() { const [token, setToken] useStatestring(); useEffect(() { registerForPushNotificationsAsync().then(setToken).catch(console.error); // ← 关键Android 上 FCM 令牌会轮换必须监听并回传服务端 const subscription addPushTokenListener((newToken) { console.log(Token rotated:, newToken); }); // 打印底层设备令牌排查平台层问题时用 getDevicePushTokenAsync() .then((t) console.log(Device token:, JSON.stringify(t))) .catch(console.error); return () subscription.remove(); }, []); return token; }不这么写会怎样跳过getPermissionsAsync的预检每次冷启动都弹权限框不监听addPushTokenListenerAndroid 上应用重装或清除缓存后服务端手里的旧令牌全部失效表现是部分用户收不到消息且无法复现。⚙️ 模块二原生侧的三件事JS 拿到的令牌要真正可用前提是原生工程里装好了厂商通道FCM/APNs。这个模块解决令牌为何在某些环境下无效。第一步是配置expo-notifications插件它替你改原生工程里的图标、声音、默认渠道{ expo: { plugins: [ [ expo-notifications, { icon: ./assets/notification-icon.png, color: #232323, sounds: [./assets/notify.wav] } ] ], extra: { eas: { projectId: your-project-id } } } }第二步是原生凭据两个平台来源不同平台凭据来源注入方式AndroidFCM 的google-services.jsonFirebase 控制台EAS Build 凭据或构建时注入googleServicesFileiOSAPNs 推送密钥Apple Developer 后台eas build构建时由 EAS 自动处理第三步也是最容易忽略的一步推送不可用 Expo Go 测试。Expo Go 没有内置推送能力本地调试要跑开发构建正式包走 EASnpx expo run:android # 本地开发构建USB 连真机 eas build --profile development # 或打开发构建包分发不这么写会怎样在 Expo Go 里调试时getExpoPushTokenAsync直接报错且所有消息永远收不到——这不是代码 bug是运行环境不含推送能力。 模块三发送一条推送发送端就是对 Expo Push Service 的一次 POST。正式架构里这一步跑在你的服务器上这里为了闭环由应用给自己发// src/push/send.ts export async function sendPush(expoPushToken: string) { const payload { to: expoPushToken, sound: notify.wav, // 对应模块二插件里注册的 sounds title: 订单已发货, body: 包裹已离开仓库, data: { orderId: 12345 }, // ← 关键自定义数据用于点击后的深链导航 }; const response await fetch(https://exp.host/--/api/v2/push/send, { method: POST, headers: { Accept: application/json, Content-Type: application/json, }, body: JSON.stringify(payload), }); const result await response.json(); if (!response.ok) { throw new Error(Push failed: ${JSON.stringify(result)}); } return result; }data字段不参与展示它跟着通知一路传到点击回调是深链的依据。 模块四按应用状态分发通知这是整个模块的核心。同一条推送应用在前台、后台、已杀死时走完全不同的处理路径对应三处代码。第一处在入口文件点击通知的监听器注册在模块顶层不在任何组件里因为用户点通知 → 应用冷启动这条路径上组件可能还没来得及挂载。前台展示策略也在这一层设定// index.ts import { registerRootComponent } from expo; import { addNotificationResponseReceivedListener, setNotificationHandler, } from expo-notifications; import App from ./App; // ← 关键顶层注册确保点通知冷启动时也能接到事件 addNotificationResponseReceivedListener((response) { const data response.notification.request.content.data; console.log(Notification tapped, orderId:, data.orderId); // 实际项目中在这里做导航 }); // ← 关键只决定前台收到时怎么展示不拦截后台 setNotificationHandler({ handleNotification: async () ({ shouldShowBanner: true, shouldShowList: true, shouldPlaySound: true, shouldSetBadge: true, }), }); registerRootComponent(App);第二处是后台任务只带data、不带标题正文的 headless 通知会在应用不在前台时唤起这里的 JS哪怕应用已被杀死// src/push/backgroundTask.ts import * as Notifications from expo-notifications; import { defineTask } from expo-task-manager; const TASK_NAME PUSH_BACKGROUND_TASK; // ← 关键defineTask 必须在模块顶层系统随时可能拉起它 defineTask(TASK_NAME, (event) { console.log(Background data:, JSON.stringify(event.data)); // 可在此写存储、发请求再 scheduleNotificationAsync 转为本地通知 return Notifications.BackgroundNotificationTaskResult.NewData; }); // ← 关键只做 defineTask 不 registerTaskAsync系统不认这个任务名 Notifications.registerTaskAsync(TASK_NAME);第三处是界面层只处理前台收到通知后的 UI 反馈// App.tsx import { useEffect, useState } from react; import { Button, Text, View } from react-native; import { addNotificationReceivedListener } from expo-notifications; import { sendPush } from ./push/send; import { usePushToken } from ./push/usePushToken; export default function App() { const token usePushToken(); const [last, setLast] useStatestring(); useEffect(() { // ← 关键该监听器仅在前台触发后台收到通知不会走到这里 const sub addNotificationReceivedListener((n) { setLast(${n.request.content.title}: ${n.request.content.body}); }); return () sub.remove(); }, []); return ( View style{{ flex: 1, padding: 24 }} TextToken: {token ?? 未获取}/Text Text前台收到: {last ?? 无}/Text Button title给自己发一条推送 disabled{!token} onPress{() token sendPush(token)} / /View ); }三个文件各司其职index.ts管点击与展示策略backgroundTask.ts管后台静默处理App.tsx管前台 UI。把监听器全塞进组件会丢失冷启动点击事件把后台任务塞进组件则后台 JS 根本不会执行。⚖️ 三个配置项别照抄先知道边界1.setNotificationHandler的 3 秒契约与 SDK 行为翻转handler 必须在 3 秒内返回否则按未响应处理。SDK 57 到 58 之间超时和缺省的后果变了场景SDK ≤ 57SDK ≥ 58未设置 handler前台收到通知完全不展示默认展示声音横幅列表角标handler 3 秒未响应通知被丢弃按默认行为展示setNotificationHandler(null)—Android 前台不展示iOS 交给其他库设置的 delegate结论SDK 58 之后 handler 的职责从允许展示变成定制展示升级 SDK 时前台行为会变需要回归测试。2. Android 通知渠道的importance等级Android 8.0 的通知先入渠道再展示渠道等级决定用户看到什么等级横幅声音振动MAX有可全屏有有HIGH有有有DEFAULT有有无LOW无仅通知栏列表无无MIN无无无想让用户即时感知消息渠道至少要HIGH只发LOW会出现消息到了但没响也没弹的误报。3.registerTaskAsync后台任务的送达边界后台任务能执行不等于每条消息都会触发它应用状态带标题的通知消息仅 data 的 headless 通知前台receivedListener handlerreceivedListener 后台任务后台系统展示JS 不执行后台任务系统不保证送达已杀死系统展示点击时唤醒 JS后台任务系统不保证送达不保证送达的典型原因Android Doze 模式、iOS 上每小时后台推送超过 2~3 条被系统丢弃。所以需要用户可见的内容用普通通知消息只有纯数据同步才走 headless。 高频翻车排查清单现象Expo Go 里getExpoPushTokenAsync报错发的消息永远收不到根因Expo Go 不含推送能力推送只存在于开发构建和正式包修复npx expo run:android跑开发构建或eas build --profile development现象启动即抛Project ID not found根因app.json缺extra.eas.projectId令牌无法绑定路由身份修复eas init将projectId写入app.json的extra.eas下现象部分 Android 用户收不到且故障设备固定不变根因FCM 令牌轮换后服务端仍是旧值修复注册addPushTokenListener回调中把新令牌写回后端现象Android 通知能弹出但无声音无振动根因走的是默认渠道importance等级不够修复setNotificationChannelAsync(default, { name: default, importance: AndroidImportance.HIGH })现象iOS 前台通知时而展示时而消失根因handler 内含异步 I/O超过 3 秒被按未响应处理SDK 58 起表现为默认展示修复handleNotification内只做同步判断禁止发起网络或存储请求✅ 收尾至此你可以独立构建含推送的开发构建、完成真机闭环并覆盖前台/后台/已杀死三种状态的处理。深入自定义消息与后台通知配置sending-notifications-custom.mdx【免费下载链接】expoAn open-source framework for making universal native apps with React. Expo runs on Android, iOS, and the web.项目地址: https://gitcode.com/GitHub_Trending/ex/expo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考