跳过主要内容
移动 指南

2026年Expo推送通知指南

2026年Expo推送通知设置指南。 本指南涵盖了权限、令牌、发送、处理和生产最佳实践以确保可靠的交付。

2026年Expo推送通知指南

你可能已经到了这个阶段,应用程序已经工作,用户已经登录,产品现在想要重新引发的流程,感觉像原生应用。购物提醒。审查提示。新消息提示。发布公告。第一个本能往往是“只是连接推送”,然后一周后你就要调试为什么一个设备收到提示,模拟器似乎注册正常,但没有人能解释为什么点击不打开正确的屏幕。

这就是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可能看起来正确,而应用程序仍然在构建之间表现不一致。

现代笔记本电脑放在木桌上,显示项目设置的配置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"
      }
    }
  }
}

这里的几个细节很重要。

  • 应用程序包名和 Bundle 标识符 需要与您实际发布的应用匹配。
  • 推送通知插件 确保原生项目在构建时获得所需的设置。
  • 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稳定。随意更改它们会使通知行为在之后更难理解。

请求权限和捕获推送令牌

这是许多团队从代码片段中复制的部分,但最终会后悔的部分。

权限请求需要考虑时间、平台和异步处理的纪律。令牌捕获需要在物理设备上发生,只有在权限被解决后,且您准备立即在后端存储结果时才会发生。

展示获取和存储Expo推送通知令牌的步骤图。

客户端函数应作为基准

使用以下函数作为起点:

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测试有用,但不能用来验证推送令牌注册的可靠性。

恰当的时机

context

  1. 用户触达了一个有意义的功能界限。
  2. 不要在启动屏幕上询问。不要在用户还没有理解通知价值之前询问。通常最好的时机是在用户执行一个使通知益处变得具体的操作之后,比如启用交付更新、加入对话或保存所看的项目。
  3. 一个好的实现通常遵循这个流程:
  4. 如果获得许可,应用程序会立即在后端存储令牌。

很多应用程序在最后一步失败了。他们检索令牌,记录它本地,推迟后端注册。后来,支持人员无法确定哪个设备在哪个时间点拥有哪个令牌。

注册后存储令牌的简单示例如下:

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令牌,发送通知就变得简单了。困难的部分不是请求本身。它是决定哪些内容应该放在载荷中,以及您对客户端状态的信任程度。

使用 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;
}

每个载荷字段的作用

不要把载荷当作垃圾箱。每个字段都应该有意图。

Field 目的 实用建议
to 目标 Expo 推送令牌 验证它属于当前设备记录
title 通知标题 简短易读
body 主要可见文本 使动作明确
sound 系统声音行为 适度使用高价值警报
data 应用特定元数据 优先使用 ID 和路由提示而不是富内容

The data 产品流程的对象是有用的地方。您可以传递一个类型和记录 ID,然后让应用程序在用户点击时获取最新数据。这样做比直接在 payload 中嵌入大型或敏感的 blob 安全。

保持 payload 小且无聊

根据 Courier 的指南,Expo 通知应该被视为临时的,超过约 4 KB 的 payload 可能会被丢弃,发送小型元数据 payload,如 { "type": "new_review", "id": 123 } 而不是大型 JSON 或内联媒体。这种建议与现实中的系统相符。小 payload 更少会失败,并且在应用逻辑发生变化时更耐用。

发送足够的数据来路由用户。然后在应用程序打开时获取剩余的数据。

有用的服务器端习惯

测试环境下基本的发送函数已经足够了,但是在生产环境中,通常会添加一些额外的职责。

  • 持久化发送尝试: 保存通知意图、用户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.

处理应用中的推送通知

仅仅将推送通知送达是功能的一半。应用必须在接收到通知时做出合理的反应,并在用户点击通知时做出反应。

这意味着处理两个独立的时刻:

  • 通知到达时应用处于前台
  • 用户从系统托盘或锁屏界面与通知进行交互

一个流程图,展示了移动应用在前台和后台状态下的推送通知生命周期。

前台接收和用户响应是两个不同的事件

可靠的设置通常包括两个监听器:

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 在用户点击已送达的通知时运行。不要将它们混淆为同一个UX路径。它们服务于不同的用户体验路径。

读取数据负载并有意导航

这里是一个实用的模式,用于处理点击事件:

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');
}

这个模式保持了弹出提示的完整性,因为弹出提示包含了路由提示,而不是整个文档。如果自从弹出提示发送后,订单发生了变化,应用程序可以在导航后获取当前服务器状态。

点击弹出提示应该指向一个明显的目的地。如果你的回退路径模糊,用户会立即注意到。

前台行为应该与用户上下文相匹配

当应用程序已经打开时,盲目显示一个系统样式的警告可能会感到笨拙。有时正确的动作是显示一个在应用程序内的横幅、徽标更新或静默刷新。支持邮箱屏幕可能不需要一个可见的警告,当用户正在阅读该会话时。

这就是为什么你的前台监听器应该根据路由和通知类型进行分支的原因。例如:

  • 聊天屏幕打开: 在消息后面追加内容并避免重复的横幅
  • 仪表盘打开: 显示一个轻量级的在应用程序内的提示
  • 关键账户事件: 表达一个更强大的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通知文档中所提到的这会影响您设计刷新触发器的方式。重新同步令牌状态的好时机包括: 应用程序启动后更新.

OTA更新

  • App Store审查周期
  • 用户登录
  • 权限设置变更
  • 您的发布过程中进行凭据轮换
  • 推送相关支持票据后的恢复流程

安全性和合规性不应出现在冲刺的末尾

许多Expo教程专注于机械方面而忽略了运营风险。对于业余应用来说这是可以接受的,但对于医疗相关、金融科技或受监管的商业产品来说这并不是一个好主意。

Courier对Expo通知缺口的企业级讨论 突出了围绕同意记录、审计跟踪和最小化敏感负载暴露的实用指导缺乏。直接的工程性 takeaway 是简单的:

  • 不要将敏感业务数据放入通知文本或负载元数据中。
  • 在服务器端记录同意变更。
  • 记录哪个通知意图被发送到哪个令牌。
  • 在载荷中使用 ID 并在应用打开后获取受保护的内容。

对于那些将发布操作与更广泛的 应用商店合规和API安全实践对齐的团队来说推送应该纳入同样的审查范畴中,包括认证、分析和后端事件日志。

推送通知是用户面向的消息,但它们也是一个分布式系统问题。将它们与认证状态和支付事件一样对待。

通常有效的做法和通常会出现问题的做法

通常有效的做法 通常会出现问题的做法
在清晰的价值说明之后请求权限 在首帧上提示
在真实设备上进行测试 信任模拟器注册
在设备上存储令牌 每个用户记录一个令牌
发送小的元数据负载 嵌入大型或敏感的块
分别处理前台和点击事件 假设所有通知都遵循一个路径
积极过期陈旧令牌 永久重试死令牌

一个坚实的Expo推送设置并不是复杂的。它是有纪律的。


如果您的团队频繁推送应用逻辑更改并需要更紧密的控制发布行为、回滚和交付可见性 Capgo 值得一看。它帮助移动团队快速推送更新,而不必等待商店审查,这在通知流、路由逻辑或客户端修复需要快速到达用户时尤其有用。

Capacitor 应用程序的即时更新

When a web-layer bug is live, ship the fix through Capgo instead of waiting days for app store approval. Users get the update in the background while native changes stay in the normal review path.

当 web 层 bug 活跃时,通过 __CAPGO_KEEP_0__ 将修复推送到应用程序,而不是等待几天的应用商店批准。 用户在后台接收更新,而本机更改保持在正常的审查路径中。

上下文: Capgo 市场网站。 角色: 支持描述段落或元描述。 见于: 组件 GetStarted.astro。 保留 Capgo 产品/品牌和开发人员术语的原始形式。 消息键 `instant_updates_for_capacitor_apps_description` (Capacitor 应用程序的即时更新描述)。

来自马丁的人性化支持

Capgo 为您提供创建真正专业的移动应用所需的最佳见解。