使用 Expo Push 服务发送通知

编辑页面

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


For the complete documentation index, see llms.txt. Use this Use this file to discover all available pages.

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

如果你更愿意构建一个直接与 APNs 和 FCM 通信的服务器,请参阅 使用 FCM 和 APNs 发送通知。这比使用 Expo Push Service 更复杂,但可以实现更细粒度的控制,并能完全使用所有 FCM 和 APNs 功能。

使用服务器发送推送通知

在你设置好推送通知凭据并添加获取 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": "Hello world!" }, { "to": "ExponentPushToken[yyyyyyyyyyyyyyyyyyyyyy]", "badge": 1, "body": "You've got mail" }, { "to": [ "ExponentPushToken[zzzzzzzzzzzzzzzzzzzzzz]", "ExponentPushToken[aaaaaaaaaaaaaaaaaaaaaa]" ], "body": "Breaking news!" } ]

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

推送票据

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

ok 状态以及回执 ID 仅表示消息已被 Expo 的服务器接收,并不表示它已被用户接收(要确认这一点,你需要检查推送回执)。

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

{ "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]\" is not a registered push notification recipient", "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 的文档要求你停止向该设备发送通知。推送回执中包含这些错误信息。

即使回执的 status 显示为 ok,这也不能保证设备已经收到消息;推送回执中的“ok”只表示 Android(FCM)或 iOS(APNs)推送通知服务已成功接收通知。例如,如果接收设备处于关机状态,iOS 或 Android 推送通知服务会尝试投递消息,但设备不一定会收到它。

如果整个请求失败,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 Dashboard 中 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。

若要更好地理解 iOS 凭据,包括推送通知凭据,请阅读我们的 App Signing 文档

请求错误

如果推送票据或推送回执的整个请求出现错误,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 push token,或一个 Expo push token 数组,用于指定此消息的接收者。
_contentAvailable仅 iOSboolean | undefined当设置为 true 时,该通知会使 iOS 应用在后台启动并运行一个后台任务。你的应用需要进行配置以支持此功能。
dataAndroid 和 iOSObject传递给你应用的一个 JSON 对象。大小最多约为 4KiB;发送给 Apple 和 Google 的通知总有效负载必须最多为 4KiB,否则你会收到 “Message Too Big” 错误。
titleAndroid 和 iOSstring在通知中显示的标题。通常显示在通知正文上方。对应于 AndroidNotification.titleaps.alert.title
bodyAndroid 和 iOSstring在通知中显示的消息。对应于 AndroidNotification.bodyaps.alert.body
ttlAndroid 和 iOSnumber存活时间:如果消息尚未投递,可在多长时间内保留以便重新投递的秒数。默认为 undefined,以使用各提供商的默认值(Android/FCM 和 iOS/APNs 均为 1 个月)。
expirationAndroid 和 iOSnumber自 Unix epoch 以来的时间戳,用于指定消息何时过期。效果与 ttl 相同(ttl 优先于 expiration)。
priorityAndroid 和 iOS'default' | 'normal' | 'high'消息的投递优先级。指定 default 或省略此字段,可使用各平台默认优先级(Android 为 “normal”,iOS 为 “high”)。
subtitle仅 iOSstring在通知标题下方显示的副标题。对应于 aps.alert.subtitle
sound仅 iOSstring | null当接收者收到此通知时播放声音。指定 default 可播放设备默认通知声音;或省略此字段以不播放声音。自定义声音需要通过 config plugin 进行配置,然后连同文件扩展名一起指定。例如:bells_sound.wav
badge仅 iOSnumber显示在应用图标角标中的数字。指定为零可清除角标。
interruptionLevel仅 iOS'active' | 'critical' | 'passive' | 'time-sensitive'通知的重要性和投递时机。这些字符串值对应于 UNNotificationInterruptionLevel 枚举项。
channelId仅 Androidstring用于显示此通知的 Notification Channel 的 ID。如果指定了 ID,但设备上对应的 channel 不存在(尚未由你的应用创建),则不会向用户显示该通知。
icon仅 Androidstring通知图标。Android drawable 资源的名称(例如:myicon)。默认使用 config plugin 中指定的图标。
richContentAndroid 和 iOSObject当前支持设置通知图片。请提供一个包含 image 键的对象,其值类型为 string,即图片 URL。Android 会开箱即用地显示该图片。在 iOS 上,你需要为应用添加一个 Notification Service Extension target。请参见这个示例了解如何实现。
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。