您可能已经到了应用程序工作的阶段,用户已经登录,产品现在想要重新激活流程,感觉像原生应用程序一样。 购物车提醒。 评估提示。 新消息警报。 发布公告。 第一反射往往是“只需连接推送”,然后一周后您会调试为什么某些设备收到警报,模拟器似乎注册正常,但没有人能解释为什么点击不打开正确的屏幕。
这就是Expo推送通知的魅力所在,它们既简单又脆弱。
Expo为React Native团队提供了一个实用的层,覆盖了APNs和FCM,这就是为什么许多团队使用它的原因。 但是,demo和生产就绪的实现之间的差距是真实的。 令牌生命周期、权限时间、监听器设置、负载设计和后端清理都很重要。 如果您还在频繁推送应用逻辑变化,操作性纪律的需求就更加尖锐,特别是如果您的留存工作依赖于可靠的消息传递和发布速度。 这就是更广泛的担忧背后的原因 移动应用用户留存工作如果用户体验周围的可预测性是配送的唯一有用之处。
目录
为Expo推送通知与用户互动的基础
一个 Expo推送通知 设置的吸引力在于一个原因:它消除了团队不想在第一天拥有的大量本机消息复杂性。您不需要在第一时间建立直接APNs和FCM管道,而是可以与Expo的网关合作,专注于产品行为、路由、权限用户体验和后端消息逻辑。
这种抽象并没有使推送“不真实”。它只是改变了您的工程努力的方向。
该服务的速度足够快,通常不会担心性能。从2023年3月14日到2023年6月12日,Expo的推送通知API显示了 42毫秒的中位数响应时间, 273毫秒的p99延迟, 和 平均每日错误率为 0.17% 在 数百万条每日消息根据 Knock 的 Expo 推送 API benchmark 分析这应该会让任何团队放心,Expo 是否仅适合原型。
Expo 实际上抽象了
当团队说“Expo 推送”,他们通常指的是几个分离的关注点捆绑在一起:
- 路由提供者: Expo 将消息转发到 APNs for iOS and FCM for Android.
- Token format: 您的服务器存储并发送 Expo Push Token 代替管理平台特定的令牌处理。
- Request contract: 您将一个负载 POST 到 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 当您的令牌检索期望应用程序与正确的 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 令牌。
为什么 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 应用交付工作流程, where app behavior can change frequently and backend state needs to stay synchronized.
从您的服务器发送通知
一旦您的后端获得了有效的Expo推送令牌,发送通知就变得简单了。难点不是请求本身。它是决定哪些内容应该放在载荷中以及您对客户端状态的信任程度。
以下是一个最小化的Node风格示例使用 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 safer。
保持数据包小且无聊
根据 Courier的指南:Expo通知,Expo推送令牌应被视为临时的,超过约 4 KB 的大小限制的数据包可能会被丢弃,依赖的模式是发送小的元数据数据包,如 { "type": "new_review", "id": 123 } 而不是大型JSON或内联媒体。这个建议与实际系统中的工作原理相符。小的数据包更少会失败,并且在应用逻辑发生变化时也更耐用。
发送足够的数据来路由用户。打开应用后再 fetch剩余的数据。
有用的服务器端习惯
一个基本的发送函数对于测试是足够的。一个生产环境的函数通常会添加一些额外的责任:
- 持久化发送尝试: 保存通知意图与用户ID、令牌、数据包类型和时间戳一起。
- 分离内容生成和传输: 在一个层次中构建消息副本,在另一个层次中构建Expo API 请求。
- 处理失效反馈: 如果Expo稍后报告
DeviceNotRegistered,标记该令牌过时并停止盲目重试。 - 使用友好的 webhook 设计: 如果您的系统已经发出事件,请将通知触发器路由到同样的 后端 webhook 处理模式 您在其他地方使用。
在调试客户端监听器之前,发送一个手动测试推送。如果一个令牌接收到一个带有小负载的普通通知,您的传输路径可能是健康的。如果不是,不要从改变导航code开始。首先验证令牌、负载形状和权限状态。
在您的应用程序中处理 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路径模糊,用户会立即注意到
前台行为应与用户上下文相匹配
当应用程序已经打开时,盲目显示系统样式的警告可能会感到笨拙。有时正确的动作是显示内应用程序横幅、徽标更新或静默刷新。支持邮箱屏幕可能不需要可见的警告,当用户正在阅读该会话时。
这就是为什么您的前台监听器应该根据路由和通知类型分支的原因。例如:
- 聊天屏幕打开: 追加消息并避免冗余横幅
- 仪表板打开: 显示轻量级内应用程序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太有限。它们失败的原因是团队假设令牌是永久的,负载可以携带任何内容,应用更新不会影响通知逻辑。
这种假设在生产环境下是无法生存的。

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