This documentation is available as Markdown for AI agents and LLMs. See the full Markdown index or append .md to any documentation URL.

使用 Expo 推送服务发送通知

编辑页面

了解如何调用 Expo Push Service API 从您的服务器发送推送通知。


expo-notifications 库提供了推送通知所需的全部客户端功能。Expo 还负责将推送通知发送到 FCM 和 APNs,然后它们再把通知发送到特定设备。你只需要使用通过 getExpoPushTokenAsync 获取的 ExpoPushToken 向 Expo Push API 发送请求即可。

使用服务器发送推送通知

在你配置好推送通知凭据并添加获取 ExpoPushToken 的逻辑后,你可以通过 HTTPS POST 请求将它发送到 Expo API。你可以通过搭建一个带数据库的服务器来完成这件事(你也可以编写一个命令行工具来发送,或者直接从你的应用中发送)。

Expo 团队和社区已经为你准备好了多种不同语言的后端封装:

上面的每个示例服务器都是 Expo Push Service API 的封装。

可靠地实现推送通知

推送通知会从你的服务器经过多个系统传递到接收设备。通知大多数时候都能送达。不过,有时在传输路径中的系统或它们之间的网络连接会出现问题。处理错误有助于让推送通知更可靠地到达目标设备。

限制并发连接数

当一次发送大量推送通知时,请限制并发连接数。Node SDK 已经帮你实现了这一点,并且最多会打开 6 个并发连接。这样可以平滑峰值负载,并帮助 Expo 推送通知服务成功接收推送通知请求。

失败时重试

发送推送通知的第一步是将它们交给 Expo 推送通知服务,服务内部会把它们加入队列,再发送给 Google(FCM v1)和 Apple(APNs)。这第一步可能因为多种原因失败:

  • 你的服务器与 Expo 推送通知服务之间的网络问题
  • Expo 通知服务宕机或可用性下降
  • 推送凭据配置错误
  • 无效的通知负载

其中一些失败是暂时性的。例如,如果 Expo 推送通知服务不可用或无法访问,并且你收到网络错误、HTTP 429 错误(请求过多)或 HTTP 5xx 错误(服务器错误),请使用指数退避等待几秒后再重试。如果第一次重试仍然失败,就等待更长时间(遵循指数退避)再试一次。这能让临时不可用的服务在你重试前恢复。

其他失败则不会自行解决。例如,如果你的推送通知负载格式错误,你可能会收到一个 HTTP 400 响应,说明负载中的问题。如果你的项目没有推送凭据,或者你在同一个请求中为不同项目发送推送通知,也会收到错误。

检查推送回执中的错误

Expo 推送通知服务在成功接收通知后会返回推送票据。推送票据表示 Expo 已经收到你的通知负载,但可能仍需要继续发送。每个推送票据都包含一个票据 ID,之后你可以用它来查询推送回执。当 Expo 尝试将通知投递给 FCM 或 APNs 之后,就会生成推送回执。它会告诉你向推送通知提供方投递是否成功。

你必须检查推送回执。如果推送通知投递过程中出现问题,推送回执是了解根本原因的最佳途径。例如,回执可能会指出 FCM 或 APNs、Expo 推送通知服务,或者你的通知负载存在问题。

如果 APNs 或 FCM 返回了相关信息,推送回执也可能告诉你接收设备已取消订阅通知(例如撤销了通知权限或卸载了你的应用)。推送回执中会包含一个 detailserror 字段,值为 DeviceNotRegistered。在这种情况下,请停止向该设备的推送令牌发送通知,直到它重新向你的服务器注册,这样你的应用才算是“守规矩”的。DeviceNotRegistered 错误仅在 Google 或 Apple 认为该设备未注册时才会出现在推送回执中。这个过程所需时间不确定,而且通常无法通过卸载应用并在不久后发送推送来进行测试。

我们建议在发送推送通知 15 分钟后检查推送回执。虽然推送回执通常会更早可用,但 15 分钟的窗口能给 Expo 推送通知服务充足的时间来提供回执。如果 15 分钟后仍没有推送回执,这通常表示 Expo 推送通知服务存在错误。最后,推送回执会在 24 小时后清除。

SLA

Expo 推送通知服务没有 SLA,FCM 和 APNs 服务也可能偶尔出现中断。遵循上述建议,可以让你的应用在面对临时服务中断时更加健壮。

HTTP/2 API

除了使用前面列出的某个库之外,你也可以直接向我们的 HTTP/2 API 发送请求(此 API 当前不需要任何认证)。

要这样做,请向 https://exp.host/--/api/v2/push/send 发送一个 POST 请求,并附带以下 HTTP 头:

host: exp.host accept: application/json accept-encoding: gzip, deflate content-type: application/json

下面是一个使用 cURL 的“hello world”推送通知,你可以在终端中发送(将占位符推送令牌替换为你自己的):

curl -H "Content-Type: application/json" -X POST "https://exp.host/--/api/v2/push/send" -d '{ "to": "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]", "title":"hello", "body": "world" }'

请求体必须是 JSON。它可以是单个消息对象(如上例所示),也可以是最多 100 个消息对象的数组,只要它们都属于同一个项目,如下所示。当你需要发送多条消息时,我们建议使用数组,以便高效地尽量减少向 Expo 服务器发起的请求数量。 下面是一个发送四条消息的请求体示例:

[ { "to": "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]", "sound": "default", "body": "你好,世界!" }, { "to": "ExponentPushToken[yyyyyyyyyyyyyyyyyyyyyy]", "badge": 1, "body": "你有新邮件" }, { "to": [ "ExponentPushToken[zzzzzzzzzzzzzzzzzzzzzz]", "ExponentPushToken[aaaaaaaaaaaaaaaaaaaaaa]" ], "body": "突发新闻!" } ]

Expo Push Service 也可选地接受 gzip 压缩的请求体。这可以大幅减少发送大量通知所需的上传带宽。 Node Expo Server SDK 会自动为你对请求进行 gzip 压缩,并自动限制请求速率以平滑负载,因此我们强烈推荐使用它。

推送票据

上面的请求会返回一个 JSON 对象,其中包含两个可选字段:dataerrorsdata 将包含一个推送票据数组,其顺序与消息发送顺序一致(如果你向单个接收者发送单条消息,则返回一个推送票据对象)。每个票据都包含一个 status 字段,用于表明 Expo 是否成功接收了通知;如果成功,还会包含一个可用于稍后检索推送回执的 id 字段。

继续上面的示例,成功响应体如下所示:

{ "data": [ { "status": "ok", "id": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" }, { "status": "ok", "id": "YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY" }, { "status": "ok", "id": "ZZZZZZZZ-ZZZZ-ZZZZ-ZZZZ-ZZZZZZZZZZZZ" }, { "status": "ok", "id": "AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA" } ] }

如果某些单条消息出错,但整个请求没有失败,那么出错消息对应的推送票据将具有 error 状态,并包含如下所示描述错误的字段:

{ "data": [ { "status": "error", "message": "\"ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]\" 不是已注册的推送通知接收者", "details": { "error": "DeviceNotRegistered" } }, { "status": "ok", "id": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" } ] }

如果整个请求失败,HTTP 状态码将是 4xx 或 5xx,errors 字段将是一个错误对象数组(通常只有一个)。否则,HTTP 状态码将为 200,你的消息将被发送到 Android 和 iOS 推送通知服务。

推送回执

在接收到一批通知后,Expo 会将每个通知入队,交由 Android 和 iOS 推送通知服务(分别是 FCM 和 APNs)投递。大多数通知通常会在几秒内送达。不过,有时通知送达可能需要更长时间,特别是当 Android 或 iOS 推送通知服务接收和投递通知的时间比平时更长,或者 Expo Push Service 基础设施负载较高时。

一旦 Expo 将通知交给 Android 或 iOS 推送通知服务,Expo 就会创建一份推送回执,以表明 Android 或 iOS 推送通知服务是否成功接收了该通知。如果在投递通知时发生错误,例如凭据有问题或服务宕机,推送回执会包含更多关于该错误的信息。

要获取推送回执,请向 https://exp.host/--/api/v2/push/getReceipts 发送 POST 请求。请求体必须是一个 JSON 对象,其中字段名 ids 是一个票据 ID 字符串数组:

curl -H "Content-Type: application/json" -X POST "https://exp.host/--/api/v2/push/getReceipts" -d '{ "ids": [ "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", "YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY", "ZZZZZZZZ-ZZZZ-ZZZZ-ZZZZ-ZZZZZZZZZZZZ" ] }'

推送回执的响应体与推送票据非常相似;它是一个 JSON 对象,包含两个可选字段:dataerrorsdata 包含一个从回执 ID 到回执的映射。回执包含一个 status 字段,以及两个可选的 messagedetails 字段(在 "status": "error" 的情况下)。如果某个请求的回执 ID 没有对应的推送回执,映射中就不会包含该 ID。下面就是上述请求的成功响应示例:

{ "data": { "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX": { "status": "ok" }, "ZZZZZZZZ-ZZZZ-ZZZZ-ZZZZ-ZZZZZZZZZZZZ": { "status": "ok" } // 当某个 ID 没有对应的回执时(本例中的 YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY), // 该 ID 会从响应中省略。 } }

你必须检查每一条推送回执,因为它可能包含你需要解决的错误信息。 例如,如果某个设备不再有资格接收通知,Apple 的文档要求你停止向该设备发送通知。推送回执中包含这些错误信息。

如果整个请求失败,HTTP 状态码将是 4xx 或 5xx,errors 字段将是一个错误对象数组(通常只有一个)。否则,HTTP 状态码将为 200,你的消息将被发送到用户的设备。

错误

Expo 会提供在整个过程中发生的任何错误的详细信息。下面我们会介绍一些最常见的错误,这样你就可以在服务器上实现自动处理这些错误的逻辑。

如果由于某种原因,Expo 无法将消息投递到 Android 或 iOS 推送通知服务,推送回执的详细信息中也可能包含特定于服务的信息。这主要用于调试以及向 Expo 报告可能的 bug。

单个错误

在推送票据和推送回执中,查找带有 error 字段的 details 对象。如果存在,它可能是以下值之一,你应当像下面这样处理这些错误:

推送票据错误

  • DeviceNotRegistered:设备已无法再接收推送通知,你应该停止向相应的 Expo push token 发送消息。

推送回执错误

  • DeviceNotRegistered:设备已无法再接收推送通知,你应该停止向相应的 Expo push token 发送消息。

  • MessageTooBig:通知有效负载总大小过大。在 Android 和 iOS 上,总有效负载必须最多为 4096 字节。

  • MessageRateExceeded:你向给定设备发送消息过于频繁。请实现指数退避,并缓慢重试发送消息。

  • MismatchSenderId: 这表示你的 FCM 推送凭据存在问题。FCM 推送凭据由两部分组成:FCM server key 和 google-services.json 文件。两者必须与同一个 sender ID 关联。你可以在查找 server key 的位置找到 sender ID。请检查项目 EAS 控制面板中 Credentials > Application identifier > Service Credentials > FCM V1 service account key 下的 server key,以及项目 google-services.json > project_number 中的 sender ID,确认它们与 Firebase 控制台 Project Settings > Cloud Messaging 标签页 > Cloud Messaging API (Legacy) 中显示的内容相同。

  • InvalidCredentials:你的独立应用的推送通知凭据无效(例如,你可能已经撤销了它们)。

    • Android:请确保你已经按照上传 FCM V1 server 凭据中的说明,正确上传了 Firebase Console 中的 server key。
    • iOS:运行 eas credentials 并按照提示重新生成新的推送通知凭据。如果你撤销了 APN key,所有依赖该 key 的应用将无法再发送或接收推送通知,直到你上传新的 key 替换它。上传新的 APN key 不会更改用户的 Expo Push Token。有时,这些错误还会包含进一步的详细信息,声称是 InvalidProviderToken 错误。实际上,这与 APN key 以及你的 provisioning profile 都有关。要解决此错误,你应该重新构建应用并重新生成新的 push key 和 provisioning profile。

请求错误

如果推送票据或推送回执的整个请求出现错误,errors 对象可能具有以下值之一,你应该处理这些错误:

  • TOO_MANY_REQUESTS:你已超过每个项目每秒 600 条通知的请求限制。我们建议在服务器上实现限流,以防止每秒发送超过 600 条通知(注意,如果你使用 expo-server-sdk-node,这已经实现,并且还包含重试的指数退避)。

  • PUSH_TOO_MANY_EXPERIENCE_IDS:你正在尝试向不同的 Expo experience 发送推送通知,例如 @username/projectAAA@username/projectBBB。请检查请求中的 details 字段,其中包含 experience 名称与其关联 push token 的映射,并移除属于其他 experience 的项。

  • PUSH_TOO_MANY_NOTIFICATIONS:你正在尝试在一次请求中发送超过 100 条推送通知。请确保每次请求只发送 100 条或更少的通知。

  • PUSH_TOO_MANY_RECEIPTS:你正在尝试在一次请求中获取超过 1000 条推送回执。请确保你只发送一个包含 1000 个或更少 ticket ID 字符串的数组来获取推送回执。

额外安全性

在我们向用户投递之前,你可以要求任何推送请求都必须使用有效的 access token 发送。你可以在 EAS Dashboard 中启用这种增强的推送安全性。

默认情况下,你可以通过发送用户的 Expo Push Token 以及消息所需的任意文本或附加数据来向用户发送通知。这很容易设置,但如果 token 泄露,恶意用户就可以冒充你的服务器并向你的用户发送消息。 我们从未收到过这类报告。然而,为了遵循最佳安全实践,我们提供将 access token 与 push token 一起使用,以增加一层额外安全保护。

如果你使用 expo-server-sdk-node,请升级到至少 v3.6.0,并在构造函数中将 accessToken 作为选项传入。否则,请在发送到我们的 push API 的任何请求中添加请求头 'Authorization': 'Bearer ${accessToken}'

在启用推送安全后,任何没有有效 access token 的请求都会返回代码为 UNAUTHORIZED 的错误。

格式

消息请求格式

每条消息必须是一个具有给定字段的 JSON 对象(只有 to 字段是必需的):

字段平台类型描述
toAndroid 和 iOSstring | string[]指定此消息接收者的 Expo 推送令牌或 Expo 推送令牌数组。
_contentAvailable仅限 iOSboolean | undefined设置为 true 时,通知将导致 iOS 应用在后台启动以运行后台任务。你的应用需要进行配置才能支持此功能。
dataAndroid 和 iOSObject传递给应用的 JSON 对象。其大小最多约为 4KiB;发送到 Apple 和 Google 的通知负载总大小必须不超过 4KiB,否则会收到“消息过大”错误。
titleAndroid 和 iOSstring要在通知中显示的标题。通常显示在通知正文上方。映射到 AndroidNotification.titleaps.alert.title
bodyAndroid 和 iOSstring要在通知中显示的消息。映射到 AndroidNotification.bodyaps.alert.body
ttlAndroid 和 iOSnumber生存时间:如果消息尚未送达,可为重新投递而保留的秒数。默认为 undefined,以使用各提供商的相应默认值(Android/FCM 和 iOS/APNs 均为 1 个月)。
expirationAndroid 和 iOSnumber指定消息过期时间的 Unix 纪元时间戳。效果与 ttl 相同(ttl 优先于 expiration)。
priorityAndroid 和 iOS'default' | 'normal' | 'high'消息的投递优先级。指定 default 或省略此字段,以使用各平台的默认优先级(Android 上为“普通”,iOS 上为“高”)。
subtitle仅限 iOSstring要在通知中显示的副标题,位于标题下方。映射到 aps.alert.subtitle
sound仅限 iOSstring | null接收者收到此通知时播放声音。指定 default 以播放设备的默认通知声音,或省略此字段以不播放声音。自定义声音需要通过配置插件进行配置,然后在指定时包含文件扩展名。例如:bells_sound.wav
badge仅限 iOSnumber要在应用图标徽章上显示的数字。指定零以清除徽章。
interruptionLevel仅限 iOS'active' | 'critical' | 'passive' | 'time-sensitive'通知的重要性和投递时间。字符串值对应于 UNNotificationInterruptionLevel 枚举中的各个情况。
channelId仅限 Androidstring用于显示此通知的通知渠道 ID。如果指定了 ID,但设备上不存在对应的渠道(且该渠道尚未由你的应用创建),通知将不会显示给用户。
icon仅限 Androidstring通知的图标。Android 可绘制资源的名称(例如:myicon)。默认使用配置插件中指定的图标。
richContentAndroid 和 iOSObject目前支持设置通知图片。提供一个包含 image 键的对象,其值类型为 string,表示图片 URL。Android 开箱即用即可显示图片。在 iOS 上,需要向应用添加通知服务扩展目标。请参阅此示例了解如何执行此操作。
categoryIdAndroid 和 iOSstring此通知关联的通知类别 ID。在此处详细了解通知类别
collapseIdAndroid 和 iOSstring用于折叠通知的标识符。在 Android 上,这只会合并传输中的消息(如果设备处于离线状态,则只会投递具有给定 collapseId 的最新消息),并映射到 FCM 的 collapse_key。若要替换 Android 上已显示的通知,请使用 tag。在 iOS 上,它既会合并传输中的消息,也会替换设备上已显示的通知,并映射到 apns-collapse-id
tag仅限 Androidstring用于替换设备上已显示通知的标识符。如果设备已显示具有相同 tag 的通知,新通知将替换它。这与 collapseId 不同:collapseId 会合并_传输中的_消息,而 tag 会替换_已显示的_通知。映射到 FCM 的 notification.tag 字段。
mutableContent仅限 iOSboolean指定此通知是否可以被客户端应用拦截。默认为 false

关于 ttl 的说明:在 Android 上,我们会尽最大努力立即投递 TTL 为零的消息,并且不会对它们进行限流。不过,将 TTL 设得很低(例如零)可能会导致普通优先级通知永远无法到达处于 doze 模式的 Android 设备。要保证通知被投递,TTL 必须足够长,以便设备能从 doze 模式中唤醒。此字段在同时指定 expiration 时优先于 expiration

关于 priority 的说明:在 Android 上,普通优先级消息不会在休眠设备上打开网络连接,其投递可能会延迟以节省电量。高优先级消息更有可能立即投递,并且可能会唤醒休眠设备以打开网络连接,从而消耗电量。在 iOS 上,普通优先级消息会在考虑设备耗电情况的时间发送,并且可能会被分组后成批投递。它们会被限流,并且可能不会由 Apple 投递。高优先级消息通常会立即发送。普通优先级对应 APNs 优先级 5,高优先级对应 10。

关于 channelId 的说明:如果保持为 null,则会使用 “Default” channel,并且如果设备上尚不存在该 channel,Expo 会在设备上创建它。不过请谨慎使用,因为 “Default” channel 面向用户,你可能无法将其完全删除。

推送票据格式

{ "data": [ { "status": "error" | "ok", "id": string, // 这是回执 ID // 如果 status === "error" "message": string, "details": JSON }, ... ], // 仅当整个请求发生错误时才会填充 "errors": [{ "code": string, "message": string }] }

推送回执请求格式

{ "ids": string[] }

推送回执响应格式

{ "data": { 回执 ID: { "status": "error" | "ok", // 如果 status === "error" "message": string, "details": JSON }, ... }, // 仅当整个请求发生错误时才会填充 "errors": [{ "code": string, "message": string }] }

交付保障

Expo 会尽最大努力将通知传递给由 Google 和 Apple 运营的推送通知服务。Expo 的基础设施被设计为至少尝试一次将通知交付到底层推送通知服务。通知被交付给 Google 或 Apple 不止一次的可能性,比完全没有被交付的可能性更高;不过,这两种情况都不常见。

在通知被移交给底层推送通知服务之后,Expo 会创建一份“推送回执”,记录此次移交是否成功。推送回执表示底层推送通知服务是否收到了该通知。

最后,来自 Google 和 Apple 的推送通知服务会遵循各自的策略将通知送达设备。

故障排查

网络连接问题

本节帮助你诊断并解决常见的网络问题。你的服务器必须能够连接到美国区域的 Google Cloud Platform 服务,因为 Expo 的推送通知服务就托管在这里。

DNS 解析

测试你的服务器是否可以解析 Expo 推送服务的域名:

dig exp.host # 使用公共 DNS 服务器进行检查 dig @8.8.8.8 exp.host

网络路由和连接

验证你的服务器是否可以访问 Expo 的端点:

# 使用 traceroute 识别路由问题 traceroute exp.host # 测试基本连通性 ping exp.host # 测试到推送服务器的 HTTPS 连通性。 # 你应该收到状态码为 200 的 HTTP 响应头。 curl --verbose https://exp.host/

需要检查的常见问题:

  • 防火墙规则阻止了出站 HTTPS(端口 443)流量
  • 可能需要身份验证或特殊配置的企业代理服务器
  • 限制出站连接的网络 ACL 或安全组(在云环境中)
  • 由于 MTU 大小问题导致的数据包分片

TLS 证书验证

确保你的服务器能够验证服务器的 TLS 证书:

openssl s_client -connect exp.host:443 -servername exp.host

我们使用由主要服务提供商签发的标准 TLS 证书,包括 Cloudflare、Google 和 Let's Encrypt。