你可能已经到了一个阶段,应用程序工作,用户已签名,产品现在想要重新激活流程,感觉像原生应用。 购物提醒。 评论提示。 新消息提示。 发布公告。 第一反射往往是“只连接推送”,然后一周后你会调试为什么一个设备收到警报,模拟器似乎注册正常,但没有人能解释为什么点击不打开正确的屏幕。
这是Expo推送通知的特点:既简单又脆弱。
Expo为React Native团队提供了一个实用的层,覆盖了APNs和FCM,这就是为什么许多团队使用它的原因。 但是,demo和生产就绪的实现之间的差距是真实的。 令牌生命周期、权限时间、监听器设置、负载设计和后端清理都很重要。 如果你还在频繁推送应用逻辑变化,操作规范的需要就变得更加尖锐,特别是如果你的留存工作依赖于可靠的消息传递和发布速度。 这是更广泛的担忧背后的原因。 mobile app user retention work: delivery is only useful if the user experience around it is predictable.
Table of Contents
- The Foundation for Engaging Users with Expo Push Notifications
- Initial Project Setup and Configuration
- Requesting Permission and Capturing Push Tokens
- 从您的服务器发送通知
- 在您的应用程序中处理 incoming 通知
- 生产最佳实践和常见陷阱
为Expo推送通知与用户互动的基础
一个 Expo推送通知 设置的吸引力在于:它消除了团队不想在第一天拥有的大量本地消息传递复杂性。您不需要在第一时间建立直接APNs和FCM管道,而是可以与Expo的网关合作,专注于产品行为、路由、权限用户体验和后端消息逻辑。
这种抽象并没有使推送“不真实”。它只是改变了您的工程努力的方向。
该服务的速度足够快,通常不会担心性能。从2023年3月14日到2023年6月12日,Expo的推送通知API显示了 42毫秒的中位数响应时间, 273毫秒的p99延迟,平均每日错误率为0.17% 横跨 数百万条每日消息 ,根据Knock的Expo推送__CAPGO_KEEP_0__benchmark分析 Knock’s Expo push API benchmark analysisExpo实际上抽象了
当团队说“Expo推送”,他们通常指的是几个分开的关注点打包在一起:
路由提供者:
- Expo将消息转发到 APNs APNs for iOS 和 FCM for Android.
- Token 格式: 您的服务器存储并发送 Expo 推送令牌,而不是首先管理平台特定的令牌处理。
- 请求协议: 您将一个负载发送到 Expo 的推送 API 而不是直接集成本机提供商 API。
这很有帮助,但这也会产生一个常见的误解。团队有时会假设 Expo 拥有每个交付问题。实际上,许多故障来自于应用 code、过期令牌、 Payload 错误或权限流程不佳。
实用规则: 将 Expo 视为可靠的传输层,而不是客户端和后端设计的替代品。
什么是生产就绪的真正含义
一个工作的演示只证明了一台设备接受了一个负载一次。生产就绪意味着另一些东西:
| 关注 | 演示思维 | 生产思维 |
|---|---|---|
| 权限 | 立即询问 | 在用户价值清晰后询问 |
| 令牌 | 只保存一次 | 刷新、去重、过期、重新协调 |
| 载荷 | 将所有内容放在 data |
保持载荷小且行动导向 |
| App行为 | 显示警告 | 正确导航并处理前台状态 |
| 操作 | 手动测试 | 收据、清理、日志和事件处理 |
这就是“通知发送”和“通知支持真正的产品流程”的区别。
初始项目设置和配置
很多Expo推送痛苦都始于第一个权限提示之前。如果您的项目配置不当,客户端code可能看起来正确,而应用程序仍然在不同构建中表现不一致。

从正确安装的库开始,并且与构建路径匹配的开发环境。如果您正在超出Expo Go, align您的本地工作流程与一个 自定义Expo开发客户端设置, 因为通知行为通常需要在一个与生产环境更接近的构建中进行验证,而不是快速的沙盒运行。
安装通知包
至少,您通常需要:
expo-notifications用于权限请求、令牌获取、监听器和通知呈现的功能。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"
}
}
}
}
以下几点很重要:
- 包名和捆绑标识符 需要与您实际发布的应用程序匹配。
- 通知插件 确保原生项目在构建时获得所需的设置。
- 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 true时才条件请求权限,而常见的陷阱是跳过该守卫,这导致开发人员向无效的模拟器令牌发送通知,并将问题归咎于 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 应用交付工作流程, where app behavior can change frequently and backend state needs to stay synchronized.
从您的服务器发送通知
一旦您的后端获得了有效的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 安全得多。
保持数据包小且无聊
根据 Courier的指南:Expo通知,Expo推送令牌应被视为临时的,超过约 4 KB 的大小限制的数据包可能会被丢弃,依赖的模式是发送小的元数据数据包,如 { "type": "new_review", "id": 123 } 而不是大型JSON或内联媒体。这个建议与在实际系统中工作的原则相符。小的数据包更少会失败,并且在应用逻辑发生变化时更好地保存。
发送足够的数据来路由用户。打开应用后再获取剩余的数据。
有用的服务器端习惯
测试时一个基本的发送函数就足够了。生产环境中的一个通常会添加一些额外的责任:
- 保存发送尝试: 保存通知意图与用户ID、令牌、数据包类型和时间戳一起。
- 将内容生成与传输分离: 在一个层次中构建消息副本,在另一个层次中构建Expo API 请求。
- 处理无效的反馈: 如果Expo稍后报告
DeviceNotRegistered,标记该令牌过时并停止盲目重试。 - 使用友好的 webhook 设计: 如果您的系统已经发出事件,请将通知触发器路由到同样的 后端 webhook 处理模式 您在其他地方使用的。
在调试客户端监听器之前,发送一个手动测试推送。如果令牌接收到一个带有小负载的普通通知,您的传输路径可能是健康的。如果不是,不要从改变导航code开始。首先验证令牌、负载形状和权限状态。
在您的应用程序中处理 incoming 通知
仅仅将通知传递给用户是功能的一半。应用程序必须在接收到通知和用户点击它时做出有意义的反应。
That means handling two separate moments:
- app在前台时,通知到达
- app在后台时,用户与系统托盘或锁屏通知交互

前台接收和用户响应是不同的事件
可靠的设置通常包括两个监听器:
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 app处于活跃状态时运行. 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路径模糊,用户会立即注意到。
前台行为应与用户上下文相匹配
当应用程序已经打开时,盲目显示系统样式的警告可能会感到笨拙。有时正确的动作是显示应用内横幅、徽标更新或静默刷新。支持邮箱屏幕可能不需要在用户正在阅读该会话时显示可见警告。
这就是为什么您的前台监听器应该根据路由和通知类型分支的原因。
- 聊天屏幕打开: 在消息后追加并避免重复横幅
- 仪表板打开: 显示轻量级应用内toast
- 关键账户事件: 表达更强大的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太有限。它们失败的原因是团队假设令牌是永久的,payload可以携带任何内容,应用程序更新不会影响通知逻辑。
这种假设在生产环境下是无法生存的。

令牌是短暂的,而不是身份记录
Expo推送令牌最好被视为租赁,而不是终身设备标识符。令牌可以在重新安装、系统更改或其他生命周期事件后旋转。如果令牌最终返回 DeviceNotRegistered您的后端应该停止将其视为活跃。
一个实用的后端模型存储:
- 用户ID
- 平台
- 安装范围的元数据
- 当前令牌
- 最后一次看到的时间戳
- __CAPGO_KEEP_0__
不要把一个令牌字段存储在用户表中,认为就完成了。用户有多个设备,设备状态会发生变化。
刷新策略比大多数教程承认的要重要得多
官方生态系统指南在这里留下了一个真正的运营缺口。现有的Expo推送通知内容往往没有解释如何在App Store审查周期和OTA更新期间维持令牌有效性 尤其是在团队正在实时推送更新时,这一点尤为重要,因为推送可靠性依赖于当前令牌状态和后端同步,正如Expo通知文档中所提到的这会影响您设计刷新触发器的方式。好时机包括: 应用程序更新后启动.
用户登录
- 权限设置更改
- 凭证轮换工作在您的发布过程中
- 在这些情况下,令牌状态会被重新协调
- 这会影响您设计刷新触发器的方式。
- 推送相关支持票后恢复流程
安全性和合规性不应出现在 sprint 的末尾
许多 Expo 教程侧重于机制而忽略了运营风险。对于业余应用来说这是可以接受的,但对于与医疗、金融或受监管的商业产品相关的应用来说,这是不够的。
Courier 对 Expo 通知缺陷的企业级讨论 强调了关于同意登记、审计跟踪和最小化敏感负载暴露的实用指导缺乏。直接的工程性 takeaway 是简单的:
- 不要将敏感业务数据放入通知文本或负载元数据中。
- 在服务器端记录同意变更。
- 记录哪个通知意图被发送到哪个令牌。
- 在负载中使用 ID 并在应用打开后获取保护内容。
对于那些将发布操作与更广泛的 应用商店合规性和API安全实践对齐的团队来说,推送应该在同一审查范畴内与认证、分析和后端事件日志一起进行。__CAPGO_KEEP_0__
推送通知是用户面向的消息,但它们也是一个分布式系统的问题。对它们的处理应该与对认证状态和支付事件的处理一样谨慎。
通常有效的方法和通常会出现问题的方法
| 通常有效 | 在清晰的值说明之后请求权限 |
|---|---|
| 在首帧上提示 | 在真实设备上进行测试 |
| 信任模拟器注册 | 将令牌与设备上下文存储 |
| 每个用户记录一个令牌 | 发送小的元数据负载 |
| 将大型或敏感的块嵌入 | 将大型或敏感的块嵌入 |
| 处理前台和点击事件分开 | 假设所有通知遵循一个路径 |
| 积极地过期陈旧令牌 | 死令牌永远重试 |
一个坚实的Expo推送设置并不是复杂的。它是有纪律的。
如果您的团队频繁推送应用逻辑变化并需要对发布行为、回滚和交付可见性有更紧密的控制, Capgo 值得一看。它帮助移动团队快速推送更新,而不必等待商店审查,这在通知流、路由逻辑或客户端修复需要快速到达用户时尤其有用。