您可能已经到了应用程序工作的阶段,用户已经登录,产品现在想要重新参与流程,感觉像原生应用程序。购物车提醒。审查提示。新消息警报。发布公告。第一个本能是往往是“只连接推送”,然后一周后你会调试为什么一个设备收到警报,模拟器似乎注册正常,但没有人能解释为什么点击不打开正确的屏幕。
这就是Expo推送通知的场景:简单或脆弱。
Expo为React Native团队提供了一个实用的层,覆盖了APNs和FCM,这就是为什么许多团队使用它的原因。但是,demo和生产就绪实现之间的差距是真实的。令牌生命周期、权限时间、监听器设置、负载设计和后端清理都很重要。如果你还在频繁推送应用逻辑变化,运营管理的需要就变得更加尖锐,特别是如果你的留存工作依赖于可靠的消息传递和发布速度。这个问题与 移动应用用户留存工作:只有当用户体验是可预测的时,才有用。
目录
推送通知
一个 Expo推送通知 设置
Expo推送通知的设置非常吸引人,因为它可以消除团队不想在第一天拥有的一些本机消息传递复杂性。您不必在第一时间建立直接APNs和FCM管道,而是可以与Expo的网关合作,专注于产品行为、路由、权限UX和后端消息逻辑。
该服务速度也足够快,通常情况下,性能并不是首要考虑的问题。从2023年3月14日至2023年6月12日,Expo的推送通知API显示了 42毫秒的中位数响应时间, 273毫秒的p99延迟,以及 平均每日错误率为0.17% 跨 数百万条每日消息,根据 Knock的Expo推送APIbenchmark分析。这应该会让任何团队放心,Expo是否仅适合原型的担忧
Expo实际上抽象了
当团队说“Expo推送”,他们通常指的是几个分开的关注点捆绑在一起:
- 服务端路由: Expo 将消息转发给 APNs 用于 iOS 和 FCM 用于 Android。
- 令牌格式: 您的服务器存储并发送 Expo 推送令牌,而不是首先管理平台特定的令牌处理。
- 请求契约: 您将一个负载 POST 到 Expo 的推送 API 而不是直接集成原生提供商 API。
这很有帮助,但这也会产生一个常见的误解。团队有时会假设 Expo 拥有每个交付问题。在实践中,许多故障来自于应用 code、过期令牌、 payload 错误或权限流程不佳。
实用规则: 把 Expo 当作可靠的传输层,而不是用来替代良好客户端和后端设计的替代品。
什么是生产就绪的真正含义
一个工作的演示仅仅证明了一台设备接受了一次包裹,但生产就绪意味着其他东西:
| 关注 | 演示模式 | 生产模式 |
|---|---|---|
| 权限 | 立即询问 | 在用户价值清晰后,询问上下文 |
| 令牌 | 只保存一次 | 刷新、去重、过期和重新协调 |
| 推送包 | 将所有内容放在 data |
保持推送包小且行动力强 |
| 应用行为 | 显示警告 | 正确导航并处理前台状态 |
| 操作 | 手动测试 | 收据、清理、日志和事件处理 |
这就是“推送通知发送”和“推送通知支持真正的产品流程”的区别。
初始项目设置和配置
许多Expo推送痛苦始于第一条权限提示之前。如果您的项目配置不当,客户端code可能看起来正确,而应用程序仍然在构建之间表现不一致。

开始使用正确的库和匹配构建路径的开发环境。如果您正在使用Expo Go之外的项目,建议配置一个自定义的Expo开发客户端,因为通知行为通常需要在一个与生产环境更接近的构建中进行验证。 安装通知包至少需要
用于权限请求、令牌获取、监听器和通知呈现
因为您应该在
expo-notifications典型的安装命令取决于您的包管理器,但关键是与您的Expo __CAPGO_KEEP_0__版本保持一致。不要混合任意版本的包。让Expo来解决兼容版本。expo-device添加项目级别的配置Device.isDevice.
Typical install commands depend on your package manager, but the key is version alignment with your Expo SDK. Don’t mix arbitrary package versions. Let Expo resolve compatible ones.
或
因为您应该在 app.json or app.config.js 通知应该是您的应用程序协议的一部分,而不是一个后续想法。
{
"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"
}
}
}
}
这里的几个细节很重要。
- 软件包名称和包名 需要与您实际发布的应用匹配。
- 推送通知插件 确保原生项目在构建过程中获得所需的设置。
- 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稳定。随意更改它们会使通知行为在之后更难理解。
请求权限和捕获推送令牌
这是许多团队从snippet中复制的部分,然后最终会后悔的部分。
权限请求需要时间、平台意识和有条不紊的异步处理。令牌捕获需要在物理设备上发生,只有在权限被解决后才会发生,并且只有当您准备好立即在后端存储结果时才会发生。

客户端函数应该是您的基准
使用以下函数作为您的起点:
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令牌。
为什么 Device.isDevice 不是可选项
这是一个看似无害但实际上会产生大量噪音的错误之一。专家团队只在条件允许的情况下请求权限。 Device.isDevice 是而常见的陷阱是忽略这个保护措施,这导致开发者向无效的模拟器令牌发送通知,并将问题归咎于Expo,而实际上问题出在应用配置上,如 Eagerworks的Expo通知实现笔记.
所以这个检查就放在函数的顶部。不要把它隐藏在一个辅助函数后面。让它显而易见。
模拟器结果对UI测试有用,但不能用来验证推送令牌注册的可靠性。
在合适的时机请求权限
上下文:Capgo营销网站。角色:呼吁行动按钮或链接标签。见于:组件AskAiSection.astro。消息键`ask_ai_button`(Ask Ai Button)。
一个好的实现通常遵循以下流程:
- 一个好的实现通常遵循这个流程:
- 用户到达一个有意义的功能界限。
- 应用解释通知在应用中的价值。
- 如果获得许可,应用程序会立即在后端存储令牌。
很多应用程序在最后一步失败了。它们在后端注册令牌之前,先在本地存储令牌并延迟注册。后来,支持团队就无法确定哪个设备在哪个时间点拥有哪个令牌。
注册后存储令牌的简单示例如下:
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 Push Token,发送通知就变得简单了。困难的部分不是请求本身,而是决定哪些内容应该包含在 payload 中,以及您对客户端状态的信任程度。
使用 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;
}
payload 中各个字段的作用
不要把 payload 当作垃圾箱。每个字段都应该有意图。
| Field | 目的 | 实用建议 |
|---|---|---|
to |
目标 Expo 推送令牌 | 验证它属于当前设备记录 |
title |
通知标题 | 保持简短和易读 |
body |
主要可见文本 | 使动作清晰 |
sound |
系统声音行为 | 适度使用高价值警报 |
data |
应用特定元数据 | 优先使用 ID 和路由提示而不是富内容 |
The data 产品流程的实用之处在于对象。您可以传递类型和记录 ID,然后让应用程序在用户点击时获取最新数据。这样做比直接在 payload 中嵌入大型或敏感的 blob 安全得多。
保持 payload 小而无聊
根据 Expo 通知Courier 的指南 4 KB 4 KB { "type": "new_review", "id": 123 } 可能会被丢弃,一个可靠的模式是发送小的元数据 payload,如
而不是大型 JSON 或内联媒体。这种建议与现实中的系统相符。小 payload 更少会失败,并且在应用逻辑发生变化时更耐用。
发送足够的数据来路由用户。然后在应用程序打开时再获取剩余的数据。
A基本发送函数足够测试。生产环境中通常会添加一些责任:
- 持久化发送尝试: 保存通知意图、用户ID、令牌、载荷类型和时间戳:
- 分离内容生成和传输: 在一个层次中构建消息副本,在另一个层次中构建ExpoAPI请求:
- 处理无效反馈: 如果Expo稍后报告:
DeviceNotRegistered,标记令牌过期并停止盲目重试: - 使用友好的webhook设计: 如果您的系统已经发出事件,请将通知触发器路由到同样的后端webhook处理模式 您在其他地方使用的 您在其他地方使用。
在调试客户端监听器之前,先发送一个手动测试推送。如果一个令牌接收到一个带有小包裹的普通通知,说明你的传输路径可能是健康的。如果不是,那就不要从导航开始改变。code。首先验证令牌、包裹形状和权限状态。
在应用中处理接收到的通知
仅仅将通知送达是不够的。应用必须在接收到通知时做出合理的反应,并在用户点击通知时做出反应。
这意味着处理两个独立的时刻:
- 通知到达时应用处于前台
- 用户从系统托盘或锁屏处与通知进行交互

前台接收和用户响应是不同的事件
一个可靠的设置通常包括两个监听器:
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 在用户点击已送达的通知时运行。不要将它们混淆。它们服务于不同的用户体验路径。
读取数据包裹并有意地导航
在 Expo 推送通知中,处理点击事件的实践模式
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路径模糊,用户会立即注意到。
前台行为应与用户环境相匹配
当应用程序已经打开时,盲目显示系统样式的警告可能会感到笨拙。有时正确的动作是内嵌横幅、徽章更新或静默刷新。支持邮箱屏幕可能不需要在用户正在阅读该会话时显示可见警告。
foreground 监听器应该根据路由和通知类型进行分支。例如:
- 打开聊天界面 在推送通知中附加消息并避免重复的通知栏
- 控制台打开: 在应用内显示一个轻量级的提示信息
- 关键账户事件: surface a stronger UI treatment
一个简单的方法如下:
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通知文档中所述 这会影响您设计刷新触发器的方式。好时机重新协调令牌状态包括:.
应用程序启动后更新
- 应用程序启动后更新
- 用户登录
- 权限设置变更
- 您的发布过程中进行凭据轮换
- 推送相关支持票据后恢复流程
安全性和合规性不应出现在冲刺的末尾
许多Expo教程专注于机制并跳过运营风险。对于业余应用来说这是可以接受的。然而,对于与医疗相关、金融科技或受监管的商业产品来说,这是不够的。
Courier对Expo通知缺口的企业级讨论 突出了围绕同意记录、审计跟踪和最小化敏感负载暴露的实用指导缺乏。直接的工程性 takeaway 是简单的:
- 不要将敏感业务数据放入通知文本或负载元数据中。
- 在服务器端记录同意变更。
- 记录哪个通知意图被发送到哪个令牌。
- 在载荷中使用 ID 并在应用打开后获取保护内容。
对于那些将发布操作与更广泛的 应用商店遵从性和API安全实践对齐的团队来说推送应该在同一审查范围内与认证、分析和后端事件日志
推送通知是用户界面消息,但它们也是分布式系统问题。将它们视为与认证状态和支付事件一样的关注。
通常有效的做法和通常会出现问题的做法
| 通常有效 | 通常会出现问题 |
|---|---|
| 在清晰的解释后请求权限 | 在首帧上提示 |
| 在真实设备上进行测试 | 信任模拟器注册 |
| 在设备上下文中存储令牌 | 每个用户记录一个令牌 |
| 发送小的元数据载荷 | 嵌入大型或敏感的块 |
| 分别处理前台和点击事件 | 假设所有通知遵循一个路径 |
| 积极过期陈旧令牌 | 永久重试死令牌 |
一个坚实的Expo推送设置并不是复杂的。它是有纪律的。
如果您的团队频繁推送应用逻辑变化并需要更紧密的控制发布行为、回滚和交付可见性 Capgo 值得一看。它有助于移动团队快速推送更新,而不必等待商店审查,这在通知流、路由逻辑或客户端修复需要快速到达用户时尤其有用。