你可能已经到了一个阶段,应用程序工作正常,用户已经登录,产品现在想要重新激活流程,感觉像原生应用程序。 购物车提醒。 评论提示。 新消息提示。 发布公告。 第一反应往往是“只是连接推送”,然后一周后你就要调试为什么某些设备收到提示,模拟器似乎注册正常,但没有人能解释为什么点击不打开正确的屏幕。
这就是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% 横跨 acrosscontext:Capawesome比较页面(capwesome_diff_experience_capgo)。页面/区域:Capawesome比较页面。角色:长期营销或法律段落。见于:capwesome.astro页面。保留Capgo产品/品牌和开发者术语的原始形式。 Knock’s Expo push API benchmark analysis根据
context:Knock的Expo推送__CAPGO_KEEP_0__benchmark分析。该页面/区域:Capawesome比较页面。角色:长期营销或法律段落。见于:capwesome.astro页面。保留Capgo产品/品牌和开发者术语的原始形式。
实际上,Expo抽象了
- context:Capawesome比较页面(capwesome_diff_experience_capgo)。页面/区域:Capawesome比较页面。角色:长期营销或法律段落。见于:capwesome.astro页面。保留Capgo产品/品牌和开发者术语的原始形式。 当团队说“Expo推送”,他们通常指的是几个分开的关注点合并在一起: context:Capawesome比较页面(capwesome_diff_experience_capgo)。页面/区域:Capawesome比较页面。角色:长期营销或法律段落。见于:capwesome.astro页面。保留Capgo产品/品牌和开发者术语的原始形式。 为 iOS 和 FCM 为 Android。
- 令牌格式: 您的服务器存储并发送 Expo Push Token 代替管理平台特定的令牌处理。
- 请求契约: 您将 payload 发送到 Expo 的推送 API 代替直接集成本机提供商 API。
这很有帮助,但它也会产生一个常见的误解。团队有时会假设 Expo 拥有每个交付问题。在实践中,许多故障来自于应用 code、过期令牌、 payload 错误或权限流程不佳。
实践规则: 将 Expo 视为可靠的传输层,而不是客户端和后端设计的替代品。
什么是生产就绪的真正含义
一个工作的演示只证明了一台设备接受了一次 payload。生产就绪意味着另一些东西:
| 关注 | 演示思维 | 生产思维 |
|---|---|---|
| 权限 | 立即询问 | 在上下文中询问,直到用户价值清晰 |
| 令牌 | 只保存一次 | 刷新、去重、过期、重新协调 |
| 负载 | 将所有内容放在 data |
保持负载小且行动导向 |
| 应用行为 | 显示警告 | 正确导航并处理前台状态 |
| 操作 | 手动测试 | 收据、清理、日志和事件处理 |
这就是“通知发送”和“通知支持真正的产品流程”之间的区别。
初始项目设置和配置
很多Expo推送痛苦都发生在第一条权限提示之前。如果您的项目配置不当,客户端code可能看起来正确,而应用程序仍然在不同构建中表现不一致。

从正确的库开始安装,并且开发环境与构建路径匹配。如果您正在超出Expo Go,配置一个自定义Expo开发客户端设置会有所帮助 一个自定义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"
}
}
}
}
几个细节很重要:
- 包标识符和包名 需要与实际发布的应用程序匹配。
- 通知插件 确保原生项目在构建时获得所需的设置。
- Expo 项目 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 notification implementation notes.
因此检查应该位于函数顶部。不要将其隐藏在辅助函数后面。让它显而易见。
模拟器结果对于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推送令牌,发送通知就变得简单了。难点不是请求本身。它是决定哪些内容应该包含在负载中,以及您对客户端状态的信任程度。
以下是一个最小的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;
}
每个负载字段的作用
不要把负载当作垃圾箱。每个字段都应该有意图。
| 字段 | 目的 | 上下文:Capgo营销网站。角色:短的UI标签或导航项。消息键`subprocessors_table_purpose` (子处理器表目的)。 |
|---|---|---|
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 处理模式 您在其他地方使用。 在调试客户端监听器之前,发送一个手动测试推送。如果令牌接收到一个带有小负载的普通通知,传输路径很可能是健康的。如果没有,不要从更改导航开始。从验证令牌、负载形状和权限状态开始。
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路径模糊,用户会立即注意到
前台行为应与用户上下文相匹配
当应用程序已经打开时,盲目显示系统样式的警告可能会感到笨拙。有时正确的动作是显示一个应用程序横幅、徽章更新或静默刷新。支持邮箱屏幕可能不需要在用户正在阅读该会话时显示可见警告。
这就是为什么您的前台监听器应该根据路由和通知类型分支的原因。例如:
- 聊天屏幕打开: 在消息后追加并避免冗余横幅
- 仪表板打开: 显示轻量级应用程序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
- 平台
- 安装范围的元数据
- 当前令牌
- 最后一次看到的时间戳
- 状态如活跃、过期或已撤销
不要仅仅在用户表中存储一个令牌字段并认为完成了。用户有多个设备,设备状态会发生变化。
刷新策略比大多数教程承认的更重要
官方生态系统指南在这里留下了一个真正的运营漏洞。现有的Expo推送通知内容往往没有解释如何在App Store审查周期和OTA更新期间维护令牌有效性 ,尤其是在团队正在实时推送更新时,这尤其重要,因为推送可靠性依赖于当前令牌状态和后端同步,正如在Expo通知文档中所提到的 这会影响您设计刷新触发器的方式。重新同步令牌状态的好时机包括:.
应用程序启动后更新
- 用户登录
- 权限设置更改
- 凭证轮换工作在您的发布过程中
- App Store审查周期和OTA更新期间令牌有效性的维护
- 推送相关支持票后恢复流程
安全性和合规性不应出现在冲刺的末尾
许多Expo教程专注于机械方面而忽略了运营风险。对于业余应用来说这是可以接受的,但对于与医疗、金融或受监管的商业产品相关的应用来说这不是一个好主意。
Courier对Expo通知缺口的企业级讨论 突出了围绕同意记录、审计跟踪和最小化敏感负载暴露的实用指导缺乏。直接的工程性 takeaway 是简单的:
- 不要将敏感业务数据放入通知文本或载荷元数据中。
- 在服务器端记录同意变更。
- 记录哪个通知意图被发送到哪个令牌。
- 在载荷中使用 ID 并在应用打开后获取保护内容。
对于那些将发布操作与更广泛的 应用商店合规性和API安全实践对齐的团队来说推送应该纳入同样的审查范畴中,包括认证、分析和后端事件日志。
推送通知是用户面向的消息,但它们也是一个分布式系统的问题。对它们的处理应该与对认证状态和支付事件的处理一样谨慎。
通常有效的方法和通常会出现问题的方法
| 通常有效的方法 | 通常会出现问题的方法 |
|---|---|
| 在清晰的值说明之后请求权限 | 在首帧上提示 |
| 在真实设备上进行测试 | 信任模拟器注册 |
| 将令牌与设备上下文存储 | 每个用户记录一个令牌 |
| 发送小的元数据负载 | 嵌入大型或敏感的块 |
| 处理前台和点击事件分开 | 假设所有通知遵循一个路径 |
| 积极地过期陈旧令牌 | 死令牌永远重试 |
一个坚实的Expo推送设置并不复杂。它是有条理的。
如果您的团队频繁推送应用逻辑更改并需要更紧密的控制发布行为、回滚和交付可见性 Capgo 值得一看。它有助于移动团队快速推送更新,而不必等待商店审查,这在通知流、路由逻辑或客户端修复需要快速到达用户时尤其有用。