跳过主要内容
移动 指南

2026年Expo推送通知指南

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

马丁·多纳迪厄

马丁·多纳迪厄

内容营销人员

2026年Expo推送通知指南

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

这是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

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

一台现代笔记本电脑放在木桌上,显示项目设置的配置code文件。

从正确安装的库开始,并且与构建路径匹配的开发环境。如果您正在超出Expo Go, align您的本地工作流程与一个 自定义Expo开发客户端设置, 因为通知行为通常需要在一个与生产环境更接近的构建中进行验证,而不是快速的沙盒运行。

安装通知包

至少,您通常需要:

  • expo-notifications 用于权限请求、令牌获取、监听器和通知呈现的功能。
  • expo-device 因为您应该将令牌获取保护在 Device.isDevice.

典型的安装命令取决于您的包管理器,但关键是与您的Expo SDK 保持版本一致。不要混合任意包版本。让Expo解析兼容的版本。

添加项目级别的配置

保持您的配置明确。一个最小的 app.jsonapp.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中复制一部分,然后最终后悔。

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

显示 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 true时才条件请求权限,而常见的陷阱是跳过该守卫,这导致开发人员向无效的模拟器令牌发送通知,并将问题归咎于 Expo,而实际上问题出在应用配置上,如所述 Eagerworks的Expo通知实现笔记.

为什么检查放在函数的顶部?不要把它放在辅助函数后面。让它显而易见

模拟器结果对于UI测试有用,但不能信任推送令牌注册的验证

恰当的时候请求许可

不要在启动屏幕上请求许可。不要在用户理解通知价值之前请求许可。通常最好的时间是在用户执行使通知益处具体的操作之后,比如启用交付更新、加入对话或保存所看项目时

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

  1. 用户到达一个有意义的功能界限
  2. 应用程序在自己的UI中解释通知的价值
  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 应用交付工作流程, 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 值得一看。它帮助移动团队快速推送更新,而不必等待商店审查,这在通知流、路由逻辑或客户端修复需要快速到达用户时尤其有用。

实时更新 Capacitor 应用

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

立即开始

博客最新文章

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