您可能已经到了应用程序工作,用户已经登录,产品现在想要重新激活流程,感觉像原生应用程序一样。购物提醒。审阅提示。新消息提示。发布公告。第一反射往往是“只是连接推送”,然后一周后您会调试为什么某些设备收到警报,模拟器似乎注册正常,但没有人能解释为什么点击不打开正确的屏幕。
这就是Expo推送通知要么是令人愉快的简单,要么是令人惊讶的脆弱。
Expo为React Native团队提供了一个实用的层,覆盖了APNs和FCM,这就是为什么许多团队使用它的原因。但是,demo和生产就绪实现之间的差距是真实的。令牌生命周期、权限时间、监听器设置、负载设计和后端清理都很重要。如果您还在频繁推送应用逻辑变化,需要的运营纪律就更加尖锐,特别是如果您的留存工作依赖于可靠的消息传递和发布速度。这种更广泛的担忧就是 移动应用程序用户留存工作:
只有用户体验周围的可预测性才能使交付有用。
The Foundation for Engaging Users with Expo Push Notifications
一个 Expo push notification setup 的吸引力在于一个原因:它消除了团队不想在第一天拥有的大量本机消息复杂性。
而不是直接构建 APNs 和 FCM 排水系统,
The service is also fast enough that performance usually isn’t the first thing to worry about. From March 14, 2023 through June 12, 2023, Expo’s push notification API showed a 关注产品行为、路由、权限 UX 和后端消息逻辑。, 这种抽象并没有使推送“不真实”。它只是改变了你的工程努力的方向。 该服务也足够快,以至于性能通常不是首要担心的问题。 从 2023 年 3 月 14 日到 2023 年 6 月 12 日, Expo 的推送通知 __CAPGO_KEEP_0__ 展示了一个中位数响应时间为 42 毫秒,p99 延迟为 273 毫秒,平均每日错误率为 0.17%,在数十万条日常消息中。, 根据 Knock的Expo推送APIbenchmark分析. 这应该会让任何团队放心,Expo是否仅适合原型开发。
Expo实际上抽象了什么
当团队说“Expo推送”,他们通常指的是几个分开的关注点捆绑在一起:
- 路由提供者: Expo将消息转发给 APNs 用于iOS和 FCM 用于Android。
- 令牌格式: 您的服务器存储并发送 Expo 推送令牌,而不是首先管理平台特定的令牌处理。
- 请求合同: 您将一个负载发送到 Expo 的推送 API 而不是直接集成本机提供商 API。
这很有帮助,但它也会产生一个常见的误解。团队有时会假设 Expo 拥有每个交付问题。在实践中,许多故障来自于应用 code、过期令牌、 Payload 错误或权限流程不佳。
实践规则: 将 Expo 视为可靠的传输层,而不是客户端和后端设计的替代品。
什么是生产就绪的真正含义
一个工作的演示只证明了一台设备接受了一次载荷。生产就绪意味着另一些东西:
| 关注 | 演示心态 | 生产心态 |
|---|---|---|
| 权限 | 立即询问 | 在用户值清晰后询问 |
| 令牌 | 只保存一次 | 刷新、去重、过期、重新协调 |
| 载荷 | 将所有内容都放在载荷中 data |
保持载荷小且行动导向 |
| 应用行为 | 显示警告 | 正确导航并处理前台状态 |
| 操作 | 手动测试 | 收据、清理、日志和事件处理 |
这就是“通知发送”和“通知支持真正的产品工作流”的区别。
初始项目设置和配置
在首次权限提示之前,Expo 推送痛苦往往会从大量的配置问题开始。如果您的项目配置不当,客户端code可能看起来正确,而应用程序仍然在构建之间表现不一致。

从正确的库开始安装,并且开发环境与构建路径匹配。如果您正在工作于Expo Go之外, align您的本地工作流程与一个“自定义Expo开发客户端设置”,因为通知行为通常需要在更接近生产的构建中验证,而不是快速的沙盒运行。 安装通知包至少,您通常需要:
用于权限请求、令牌检索、监听器和通知呈现的包。
用于权限请求、令牌检索、监听器和通知呈现的包。
expo-notificationsfor permission requests, token retrieval, listeners, and notification presentation.expo-device因为你应该保护令牌获取Device.isDevice.
典型的安装命令取决于你的包管理器,但关键是版本与你的Expo SDK 匹配。不要混淆任意包版本。让Expo解析兼容的版本。
添加项目级别配置
保持你的配置明确。一个最小的 app.json 或 app.config.js 应该反映通知是你的应用协议的一部分,而不是一个后thought。
{
"expo": {
"name": "MyApp",
"slug": "my-app",
"plugins": ["expo-notifications"],
"ios": {
"bundleIdentifier": "com.example.myapp"
},
"android": {
"package": "com.example.myapp"
},
"extra": {
"eas": {
"projectId": "your-project-id"
}
}
}
}
以下几点很重要:
- 包名和bundle标识符 需要与你实际发布的应用匹配。
- 通知插件 确保native项目在构建时获得所需的设置。
- EAS项目ID 在 token 检索中,应用程序需要与正确的 Expo 项目关联时,会发生什么问题。
设置通知处理程序
基本教程中,很多教程都等到定义通知行为太晚了。不要这样做。将其放在应用程序启动时附近,以便前台行为可预测。
import * as Notifications from 'expo-notifications';
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: false,
shouldSetBadge: true,
}),
});
团队决定前台通知是否显示警告、播放声音或影响徽标。具体行为取决于您的产品。聊天应用和支付应用不会做出相同的选择。
如果您不故意定义前台行为,团队会在调试“丢失”的通知时遇到困难。这些通知实际上已经接收,但没有按照产品预期呈现。
Android 需要频道配置
Android 通知频道并非在实践中是可选的。如果您跳过它们,警告可能会看起来不一致或无法匹配用户期望。
import { Platform } from 'react-native';
import * as Notifications from 'expo-notifications';
export async function configureAndroidNotifications() {
if (Platform.OS !== 'android') return;
await Notifications.setNotificationChannelAsync('default', {
name: 'Default',
importance: Notifications.AndroidImportance.MAX,
});
}
在应用程序初始化期间设置此项。然后保持频道 ID 稳定。随意更改它们会使通知行为在后期更难理解。
请求权限和捕获推送令牌
这是很多团队从代码片段中复制的部分,然后后悔的部分。
权限请求需要时间、平台意识和有条不紊的异步处理。令牌捕获需要在物理设备上发生,只有在权限被解决后才会发生,并且只有在您准备好立即在后端存储结果时才会发生。

客户端函数应为您的基准
用以下函数作为您的起点:
import * as Device from 'expo-device';
import * as Notifications from 'expo-notifications';
import Constants from 'expo-constants';
import { Platform } from 'react-native';
type RegisterResult =
| { ok: true; token: string }
| { ok: false; reason: string };
export async function registerForExpoPushNotificationsAsync(): Promise<RegisterResult> {
if (!Device.isDevice) {
return { ok: false, reason: 'Push notifications require a physical device.' };
}
if (Platform.OS === 'android') {
await Notifications.setNotificationChannelAsync('default', {
name: 'Default',
importance: Notifications.AndroidImportance.MAX,
});
}
const permissions = await Notifications.getPermissionsAsync();
let finalStatus = permissions.status;
if (finalStatus !== 'granted') {
const request = await Notifications.requestPermissionsAsync();
finalStatus = request.status;
}
if (finalStatus !== 'granted') {
return { ok: false, reason: 'Notification permission was not granted.' };
}
const projectId =
Constants.expoConfig?.extra?.eas?.projectId ??
Constants.easConfig?.projectId;
if (!projectId) {
return { ok: false, reason: 'Missing EAS project ID configuration.' };
}
const tokenResponse = await Notifications.getExpoPushTokenAsync({ projectId });
return { ok: true, token: tokenResponse.data };
}
顺序很重要。您首先检查设备类型,设置 Android 通道行为,解决权限,验证项目配置,然后请求 Expo token。
为什么 Device.isDevice 不是可选项
这是造成大量噪音的少数几个看似无害的错误之一。专家团队只有在 Device.isDevice 条件为真时才请求权限,而常见的陷阱是跳过这个保护措施,这导致开发者向无效的模拟器令牌发送通知并将问题归咎于 Expo,而实际上问题出在应用配置上,如 Eagerworks 的 Expo 通知实现笔记中所述.
因此,检查位于函数顶部。不要将其隐藏在辅助函数后面。让它显而易见。
模拟器结果对 UI 测试有用,但不能信任它们来验证推送令牌注册。
在正确的时刻请求权限
不在启动屏幕上询问。不要在用户理解通知价值之前询问。通常最好的时间是用户执行一个使通知益处具体化的操作之后,如启用送达更新、加入对话或保存所关注的项目。
一个好的实现通常遵循以下流程:
- 用户到达一个有意义的功能界限。
- 应用程序在自己的 UI 中解释通知的价值。
- 应用程序请求系统权限。
- 应用程序在权限被授予时立即在后端存储令牌。
最后一步是许多应用程序失败的地方。他们检索令牌、记录它本地并延迟后端注册。后来,支持团队就无法确定哪个设备在哪个时间点拥有哪个令牌。
以下是存储令牌后注册的简单示例:
export async function enablePushForCurrentUser(userId: string) {
const result = await registerForExpoPushNotificationsAsync();
if (!result.ok) {
return result;
}
await fetch('https://api.example.com/push-tokens', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer user-session-token',
},
body: JSON.stringify({
userId,
token: result.token,
platform: Platform.OS,
}),
});
return result;
}
在工作流程的后期,这个引导是有用的视觉参考:
对于构建频繁发布应用的团队来说,令牌注册也可以被视为应用程序的运营状态的一部分,而不是仅仅是入门流程的一部分。这种思维方式与更广泛的 Expo 应用程序交付工作流程,应用程序行为可以频繁变化,后端状态需要保持同步。
从您的服务器发送通知
一旦您的后端获得了有效的Expo推送令牌,发送通知就变得简单了。请求本身并不是问题。真正的问题是决定哪些内容应该放在载荷中,以及您对客户端状态的信任程度。
使用 fetch:
type ExpoPushMessage = {
to: string;
title: string;
body: string;
sound?: 'default' | null;
data?: Record<string, unknown>;
};
export async function sendExpoPushNotification(token: string) {
const message: ExpoPushMessage = {
to: token,
title: 'New review received',
body: 'Tap to open the order details.',
sound: 'default',
data: {
type: 'new_review',
orderId: 'ord_123',
screen: 'OrderDetails',
},
};
const response = await fetch('https://exp.host/--/api/v2/push/send', {
method: 'POST',
headers: {
Accept: 'application/json',
'Accept-encoding': 'gzip, deflate',
'Content-Type': 'application/json',
},
body: JSON.stringify(message),
});
const result = await response.json();
return result;
}
每个载荷字段的作用
不要把载荷当作垃圾箱。每个字段都应该有目的。
| 字段 | 目的 | 实用建议 |
|---|---|---|
to |
目标Expo推送令牌 | 验证它是否属于当前设备记录 |
title |
通知标题 | 保持它短小且人类可读 |
body |
主可见文本 | 让动作清晰 |
sound |
系统声音行为 | 仅用于高价值警告 |
data |
应用特定元数据 | 优先使用 ID 和路由提示而不是富内容 |
这个 data 对象是产品工作流程有用的地方。您可以传递一个类型和记录 ID,然后让应用在用户点击时获取最新数据。这比在 payload 直接嵌入大型或敏感的 blob 安全得多。
保持 payload 小且无聊
根据 Courier 的指南Expo 通知 4 KB 可以被丢弃,依赖的模式是发送小的元数据包,如 { "type": "new_review", "id": 123 } 而不是大型JSON或内联媒体。这种建议与实际系统中的工作原理相符。小的包更少会失败,并且在应用逻辑发生变化时更好地保存。
发送足够的数据来路由用户。然后在应用打开后再获取剩余的数据。
有用的服务器端习惯
测试时一个基本的发送函数就足够了。生产环境中的一个通常会添加一些额外的责任:
- 持久化发送尝试: 保存通知意图与用户ID、令牌、包类型和时间戳
- 将内容生成与传输分开: 在一个层次中构建消息副本,在另一个层次中构建Expo API 请求
- 处理失效反馈: 如果Expo后来报告
DeviceNotRegistered标记该令牌过时并停止盲目重试。 - 使用友好的 webhook 设计: 如果您的系统已经发出事件,请将通知触发器路由到同样的后端 webhook 处理模式中, 您在其他地方使用的模式。 在调试客户端监听器之前,发送一个手动测试推送。如果令牌接收到一个带有小负载的普通通知,
Before debugging client listeners, send a manual test push first. If a token receives a plain notification with a tiny payload, your transport path is probably healthy. If not, don’t start by changing navigation code. Start by validating the token, payload shape, and permission state.
不要从导航开始。首先验证令牌、负载形状和权限状态。
在您的应用程序中处理 incoming 通知
仅仅将通知送达是功能的一半。应用程序必须在通知到达时和用户点击通知时做出有意义的反应。
- 这意味着处理两个独立的时刻:
- 通知到达时应用程序处于前台状态

前台收据和用户响应是不同的事件
可靠的设置通常包括两个监听器:
import { useEffect } from 'react';
import * as Notifications from 'expo-notifications';
export function useNotificationObservers(
onForegroundMessage: (notification: Notifications.Notification) => void,
onNotificationTap: (response: Notifications.NotificationResponse) => void
) {
useEffect(() => {
const receivedSub = Notifications.addNotificationReceivedListener(
(notification) => {
onForegroundMessage(notification);
}
);
const responseSub = Notifications.addNotificationResponseReceivedListener(
(response) => {
onNotificationTap(response);
}
);
return () => {
receivedSub.remove();
responseSub.remove();
};
}, [onForegroundMessage, onNotificationTap]);
}
addNotificationReceivedListener 当应用程序处于活动状态时运行。 addNotificationResponseReceivedListener 当用户点击已送达的通知时运行。不要将它们在脑海中结合起来。它们服务于不同的用户体验路径。
读取数据载荷并有意导航
这是一个实用的模式,用于处理点击:
type NotificationData = {
type?: string;
orderId?: string;
screen?: string;
};
export function handleNotificationTap(
response: Notifications.NotificationResponse,
navigation: any
) {
const data =
response.notification.request.content.data as NotificationData;
if (data.screen === 'OrderDetails' && data.orderId) {
navigation.navigate('OrderDetails', { orderId: data.orderId });
return;
}
if (data.type === 'new_review') {
navigation.navigate('Inbox');
return;
}
navigation.navigate('Home');
}
该模式保持了弹性,因为载荷包含路由提示,而不是整个文档。如果自从通知被发送以来,顺序已经改变,应用程序可以在导航后从服务器获取当前状态。
点击通知应该导向一个明显的目的地。如果您的fallback路径模糊,用户会立即注意到。
前台行为应该与用户上下文匹配
当应用程序已经打开时,盲目显示系统样式的警告可能会感到笨拙。有时正确的动作是显示在应用程序内的横幅、徽标更新或静默刷新。支持邮箱屏幕可能不需要在用户正在阅读该会话时显示可见的警告。
这就是为什么您的前台监听器应该根据路由和通知类型分支的原因。例如:
- 聊天屏幕打开: 避免重复弹窗,直接追加消息
- 仪表盘已打开: 显示轻量级弹窗
- 关键账户事件: 显示更强大的UI处理
一个简单的方法是这样:
export function handleForegroundNotification(
notification: Notifications.Notification,
currentRouteName: string
) {
const data = notification.request.content.data as { type?: string };
if (currentRouteName === 'ChatThread' && data.type === 'new_message') {
// refresh local thread state
return;
}
// otherwise show your own in-app UI or update badges
}
如果您的应用程序无法区分这些上下文,用户会更快地感到通知疲劳,即使交付是技术上正确的。
生产最佳实践和常见陷阱
大多数破碎的Expo推送通知设置并不是因为Expo太有限。它们失败的原因是团队假设令牌是永久的,负载可以携带任何内容,应用程序更新不会影响通知逻辑。
但是在生产环境中,这个假设是不成立的。

令牌是临时的,而不是身份记录
在 Expo 推送令牌中,最好将其视为租约,而不是终身设备标识符。令牌在重新安装、系统更改或其他生命周期事件后可能会旋转。如果最终令牌返回 DeviceNotRegistered您的后端应该停止将其视为活跃。
一个实用的后端模型存储:
- 用户 ID
- 平台
- 安装范围的元数据
- 当前令牌
- 最后一次看到的时间戳
- 状态,如活跃、过期或撤销
不要在用户表中存储一个令牌字段并称之为完成。用户有多个设备,设备状态会发生变化。
刷新策略比大多数教程承认的更重要
官方生态系统指南在这里留下了一个真正的运营漏洞。现有的 Expo 推送通知内容往往没有解释如何在令牌有效性方面维持跨设备 App Store 评论周期和 OTA 更新,尤其是团队正在推送实时更改时,这很重要,因为推送可靠性依赖于当前令牌状态和后端同步,正如 Expo 通知文档中所述.
这会影响您设计刷新触发器的方式。好时机重新同步令牌状态包括:
- 应用程序启动后更新
- 用户登录
- 权限设置更改
- 在您的发布过程中进行凭据轮换
- 与推送相关的支持票据恢复流程
安全性和合规性不应出现在 sprint 的末尾
许多 Expo 教程专注于机制并跳过运营风险。对于业余应用程序,这是可以接受的。对于医疗相关、金融科技或受监管的商业产品,这不是。
Courier 对 Expo 通知缺口的企业聚焦讨论 突出了对同意日志、审计跟踪和敏感负载暴露的实用指导缺乏。直接工程的 takeaway 很简单:
- 不要将敏感业务数据放入通知文本或负载元数据中。
- 在服务器端记录同意变更。
- 记录哪个通知意图被发送到哪个令牌。
- 在负载中使用 ID 并在应用程序打开后获取保护内容。
团队在更广泛的应用商店合规性和 __CAPGO_KEEP_0__ 安全实践与发布操作对齐时,推送应该包括在同一审查范畴内。 app store compliance and API security practices通常有效的和通常会出现问题的
通常有效
通常会出现问题
| 通常有效 | 通常会出现问题 |
|---|---|
| 在清晰的值说明之后请求权限 | 在首帧提示 |
| 在真实设备上进行测试 | 信任模拟器注册 |
| 将令牌与设备上下文存储 | 每个用户记录一个令牌 |
| 发送小的元数据负载 | 嵌入大型或敏感的块 |
| 分别处理前台和点击事件 | 假设所有通知都遵循一个路径 |
| 积极地过期陈旧令牌 | 死令牌无限重试 |
A solid Expo push setup isn’t complicated. It’s disciplined.
If your team ships frequent app logic changes and needs tighter control over release behavior, rollbacks, and delivery visibility, Capgo 值得一看。它帮助移动团队快速推送更新,而不需要等待商店审查,这尤其有用,当通知流、路由逻辑或客户端修复需要快速到达用户时。