使用 Expo 推送服务发送通知
编辑
学习如何从您的服务器调用 Expo 推送服务 API 发送推送通知。
expo-notifications 库提供了推送通知的所有客户端功能。Expo 还处理将推送通知发送到 FCM 和 APNs,然后将其发送到特定设备。您所需要做的就是使用 getExpoPushTokenAsync 获得的 ExpoPushToken 向 Expo 推送 API 发送请求。
如果您更愿意构建一个直接与 APNs 和 FCM 通信的服务器,请参阅 使用 FCM 和 APNs 发送通知。这比使用 Expo 推送服务更复杂,但允许更精细的控制,并且可以完全访问所有 FCM 和 APNs 功能。
使用服务器发送推送通知
在您设置完推送通知凭据并添加逻辑以获取 ExpoPushToken 后,您可以通过 HTTPS POST 请求将其发送到 Expo API。您可以通过设置一个有数据库的服务器来做到这一点(或者您也可以编写一个命令行工具来发送它们,或直接从您的应用程序发送它们)。
Expo 团队和社区为您在几种不同的语言中创建了后端:
| SDKs | Back-end | Maintained by |
|---|---|---|
| expo-server-sdk-node | Node.js | Expo team |
| expo-server-sdk-python | Python | Community |
| expo-server-sdk-ruby | Ruby | Community |
| expo-push-notification-client-rust | Rust | Community |
| expo-notifier | Symfony | Symfony |
| exponent-server-sdk-php | PHP | Community |
| expo-server-sdk-php | PHP | Community |
| exponent-server-sdk-golang | Golang | Community |
| exponent | Golang | Community |
| exponent-server-sdk-elixir | Elixir | Community |
| expo-server-sdk-dotnet | dotnet | Community |
| expo-server-sdk-java | Java | Community |
| laravel-expo-notifier | Laravel | Community |
上述每个示例服务器都是 Expo 推送服务 API 的包装器。
可靠地实现推送通知
推送通知从您的服务器到接收设备要经过多个系统。通知大多数时间都能送达。然而,偶尔会出现沿途系统的问题以及它们之间的网络连接。处理错误有助于推送通知更可靠地到达目的地。
限制并发连接
在一次性发送大量推送通知时,请限制并发连接的数量。Node SDK 为您实现了这一点,并最多打开六个并发连接。这可以平滑您的峰值负载,并帮助 Expo 推送通知服务成功接收推送通知请求。
出现失败时重试
发送推送通知的第一步是将其交付给 Expo 推送通知服务,后者会将其内部添加到一个队列中,以便交付给 Google(FCM v1)和 Apple (APNs)。这第一步可能由于几种原因而失败:
- 您的服务器与 Expo 推送通知服务之间的网络问题
- Expo 通知服务出现停机或可用性降低
- 错误配置的推送凭据
- 无效的通知有效负载
其中一些故障是临时的。例如,如果 Expo 推送通知服务宕机或无法访问并且您遇到网络错误——HTTP 429 错误(请求过多)或 HTTP 5xx 错误(服务器错误)——请使用 指数退避 等待几秒钟后再重试。如果第一次重试尝试失败,请等待更长时间(遵循指数退避)然后再次重试。这样可以让临时不可用的服务在您重试之前恢复。
其他故障则不会自行解决。例如,如果您的推送通知有效负载格式错误,您可能会收到一个解释有效负载问题的 HTTP 400 响应。如果您的项目没有推送凭据,或者您在同一请求中为不同项目发送推送通知,您也会收到错误。
检查推送凭据以寻找错误
Expo 推送通知服务在成功接收通知时会响应推送票据。推送票据表明 Expo 已收到您的通知有效负载,但可能还需要发送它。每个推送票据包含一个票据 ID,您稍后可使用该 ID 查找推送收据。推送收据在 Expo 尝试将通知交付给 FCM 或 APNs 后可用。它告诉您推送通知提供方的交付是否成功。
您必须检查您的推送收据。如果在交付推送通知时出现问题,推送收据是获取底层原因信息的最佳途径。例如,收据可能会指示 FCM 或 APNs、Expo 推送通知服务或您的通知有效负载存在问题。
推送收据也可能会告诉您接收设备是否已取消订阅通知(例如,通过撤销通知权限或卸载应用程序)。如果 APNs 或 FCM 返回该信息,推送收据将包含一个 details → error 字段,设置为 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 推送服务还可选择接受 gzip 压缩请求正文。这可以大大减少发送大量通知所需的上传带宽。Node Expo Server SDK 会为您自动 gzip 请求,并自动限制您的请求以平滑负载,因此我们强烈推荐它。
推送票据
上述请求将以 JSON 对象响应,包含两个可选字段 data 和 errors。data 将包含一系列推送票据,按消息发送的顺序排列(或者一个推送票据对象,如果您仅将单条消息发送给单个接收者)。每个票据包括一个 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 的推送服务基础设施负载较高。
一旦 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 对象,包含两个可选字段 data 和 errors。data 包含收据 ID 到收据的映射。收据包括一个 status 字段以及两个可选的 message 和 details 字段(当 "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 报告可能的错误。
单个错误
在推送票据和推送收据内,寻找一个 details 对象及其 error 字段。如果存在,它可能是以下值之一,您应如下处理这些错误:
推送票据错误
DeviceNotRegistered: 该设备无法再接收推送通知,您应停止向相应的 Expo 推送令牌发送消息。
推送收据错误
-
DeviceNotRegistered: 该设备无法再接收推送通知,您应停止向相应的 Expo 推送令牌发送消息。 -
MessageTooBig: 总的通知有效负载过大。在 Android 和 iOS 上,总有效负载必须最大为 4096 字节。 -
MessageRateExceeded: 您向给定设备发送消息的频率太高。实施指数退避并慢慢重试发送消息。 -
MismatchSenderId: 这表明您的 FCM 推送凭据存在问题。FCM 推送凭据有两个部分:您的 FCM 服务器密钥和您的 google-services.json 文件。两者必须与相同的发送者 ID 关联。您可以在找到服务器密钥的同一位置找到发送者 ID。检查您项目的 EAS 控制面板中的服务器密钥,确保该密钥与项目中的 google-services.json 文件的project_number保持一致,这些信息可以在 Firebase 控制台的 项目设置 > 云消息 标签 > 云消息 API(遗留版) 中找到。 -
InvalidCredentials: 您独立应用的推送通知凭据无效(例如,您可能已经撤销了它们)。- Android: 确保按照上传 FCM V1 服务器凭据中指定的方式正确上传 Firebase 控制台中的服务器密钥。
- iOS: 运行
eas credentials并按照提示重新生成新的推送通知凭据。如果您撤销了 APN 密钥,所有依赖该密钥的应用在您上传新的密钥之前都将无法发送或接收推送通知。上传新的 APN 密钥将不更改用户的 Expo 推送令牌。有时,这些错误将包含更多细节,声称存在InvalidProviderToken错误。这实际上与您的 APN 密钥和您的配置文件有关。要解决此错误,您应重新构建应用,并重新生成新的推送密钥和配置文件。
要更好地理解 iOS 凭据,包括推送通知凭据,请阅读我们的应用签名文档。
请求错误
如果整个请求中推送票据或推送收据发生错误,errors 对象可能包含以下值之一,您应处理这些错误:
-
TOO_MANY_REQUESTS: 您超过了每个项目每秒 600 条通知的请求限制。我们建议在您的服务器中实现速率限制,以防止每秒发送超过 600 条通知(请注意,如果您使用 expo-server-sdk-node,这已经实现,并且还实现了重试的指数退避)。 -
PUSH_TOO_MANY_EXPERIENCE_IDS: 您正在尝试向不同的 Expo 体验发送推送通知,例如,@username/projectAAA和@username/projectBBB。检查details字段,以获取请求中体验名称与其相关联推送令牌之间的映射,并删除来自另一体验的任何推送令牌。 -
PUSH_TOO_MANY_NOTIFICATIONS: 您尝试在一个请求中发送超过 100 条推送通知。确保您在每个请求中仅发送 100 条(或更少)通知。 -
PUSH_TOO_MANY_RECEIPTS: 您尝试在一个请求中获取超过 1000 条推送收据。确保您只发送最多 1000 条(或更少)票据 ID 字符串以获取推送收据。
额外安全性
您可以要求所有推送请求在我们将其发送到用户之前使用有效的访问令牌。您可以从 EAS 控制面板启用此增强的推送安全性。
默认情况下,您可以通过发送他们的 Expo 推送令牌和消息所需的任何文本或附加数据来向用户发送通知。这很容易设置,但如果令牌泄漏,恶意用户将能够伪装您的服务器并向您的用户发送消息。 我们从未收到过此类报告。然而,为了遵循最佳安全实践,我们提供了将访问令牌与推送令牌一起作为额外的安全层的选项。
如果您使用 expo-server-sdk-node,请升级到至少 v3.6.0 并在构造函数中将您的 accessToken 作为选项传递。否则,请在与我们的推送 API 的任何请求中传入头部 'Authorization': 'Bearer ${accessToken}'。
在您启用推送安全性后,任何未使用有效访问令牌发送的请求都将导致错误代码:UNAUTHORIZED。
格式
消息请求格式
每条消息必须是具有给定字段的 JSON 对象(仅 to 字段为必填项):
| 字段 | 平台 | 类型 | 描述 |
|---|---|---|---|
to | Android 和 iOS | string | string[] | Expo 推送令牌或指定此消息接收者的 Expo 推送令牌数组。 |
_contentAvailable | 仅限 iOS | boolean | undefined | 当此字段设置为 true 时,通知将导致 iOS 应用在后台启动以运行后台任务。您的应用需要配置以支持此功能。 |
data | Android 和 iOS | Object | 传递到您的应用程序的 JSON 对象。它最多可以达到大约 4KiB;发送给 Apple 和 Google 的通知有效负载总计必须最多为 4KiB,否则您将收到“消息过大”错误。 |
title | Android 和 iOS | string | 在通知中显示的标题。通常在通知正文上方显示。映射到 AndroidNotification.title 和 aps.alert.title。 |
body | Android 和 iOS | string | 在通知中显示的消息。映射到 AndroidNotification.body 和 aps.alert.body。 |
ttl | Android 和 iOS | number | 生命时间:消息在未被送达的情况下可以被保留的秒数以便后续交付。默认设置为 undefined,以使用每个提供者的相应默认值(Android/FCM 和 iOS/APNs 的一个月)。 |
expiration | Android 和 iOS | number | Unix 纪元以来的时间戳,指定消息过期的时间。效果与 ttl 相同(ttl 优先于 expiration)。 |
priority | Android 和 iOS | 'default' | 'normal' | 'high' | 消息的交付优先级。指定 default 或省略此字段以在每个平台上使用默认优先级(Android 为“normal”,iOS 为“high”)。 |
subtitle | 仅限 iOS | string | 在通知标题下方显示的副标题。映射到 aps.alert.subtitle。 |
sound | 仅限 iOS | string | null | 当接收者接收到此通知时播放声音。指定 default 以播放设备的默认通知声音,或省略此字段以不播放声音。自定义声音需要通过配置插件配置,然后指定包括文件扩展名。示例:bells_sound.wav。 |
badge | 仅限 iOS | number | 要在应用程序图标上显示的徽章编号。指定零以清除徽章。 |
interruptionLevel | 仅限 iOS | 'active' | 'critical' | 'passive' | 'time-sensitive' | 通知的重要性和交付时机。字符串值对应于 UNNotificationInterruptionLevel 枚举案例。 |
channelId | 仅限 Android | string | 显示此通知的通知通道的 ID。如果指定了 ID,但相应的通道在设备上(尚未由您的应用创建)不存在,则通知将不会显示给用户。 |
icon | 仅限 Android | string | 通知的图标。Android 可绘制资源的名称(示例:myicon)。默认值为 配置插件 中指定的图标。 |
richContent | Android 和 iOS | Object | 当前支持设置通知图片。提供一个键为 image、值为字符串类型的对象,该值是图片 URL。Android 将直接显示该图片。在 iOS 上,您需要向您的应用添加一个通知服务扩展目标。请参见此示例了解如何实现。 |
categoryId | Android 和 iOS | string | 此通知关联的通知类别 ID。在此处了解更多关于通知类别的信息。 |
mutableContent | 仅限 iOS | boolean | 指定此通知是否可以被客户端应用拦截。默认为 false。 |
关于 ttl 的说明:在 Android 上,我们尽力立即交付零 TTL 的消息,并且不对其进行限制。然而,设置 TTL 为低值(例如零)可能会导致正常优先级的通知无法到达处于待机模式的 Android 设备。为确保通知发送,TTL 必须足够长,以便设备能够从待机模式中唤醒该通知。此字段在指定了两者时对 expiration 优先。
关于 priority 的说明:在 Android 上,正常优先级的消息不会在待机设备上打开网络连接,因此它们的交付可能会延迟,以节省电池。高优先级消息更有可能立即交付,可能会唤醒待机设备以打开网络连接,从而消耗电能。在 iOS 上,正常优先级消息会在考虑设备电量问题的情况下发送,并可能会分组并集中交付。它们受到限制,Apple 可能不会交付。高优先级消息通常会被立即发送。正常优先级对应于 APNs 优先级级别 5,而高优先级对应于 10。
关于 channelId 的说明:如果为 null,将使用“默认”通道;如果该通道尚未创建,Expo 会在设备上创建该通道。但是请谨慎使用,因为“默认”通道是用户可见的,您可能无法完全删除它。
推送票据格式
{ "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
我们使用主要服务提供商(包括 Cloudflare、Google 和 Let's Encrypt)签名的标准 TLS 证书。