跳过主要内容
移动 指南

2026年推送指南

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

2026年推送指南

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

这就是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"
      }
    }
  }
}

这里的几个细节很重要。

  • 软件包名称和包名 需要与您实际发布的应用匹配。
  • 推送通知插件 确保原生项目在构建过程中获得所需的设置。
  • 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中复制的部分,然后最终会后悔的部分。

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

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

在合适的时机请求权限

上下文:Capgo营销网站。角色:呼吁行动按钮或链接标签。见于:组件AskAiSection.astro。消息键`ask_ai_button`(Ask Ai Button)。

一个好的实现通常遵循以下流程:

  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 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 值得一看。它有助于移动团队快速推送更新,而不必等待商店审查,这在通知流、路由逻辑或客户端修复需要快速到达用户时尤其有用。

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 应用实时更新描述)。

来自 Martin 的人性化支持

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