使用 FCM 和 APN 发送通知

了解如何使用 FCM 和 APN 发送通知。


你可能需要对通知进行更精细的控制,在这种情况下,可能需要直接与 FCM 和 APNs 通信。Expo 平台并不强制你使用 Expo 应用服务,并且 expo-notifications API 对推送服务没有依赖。

🌐 You may need finer-grained control over your notifications, in which case communicating directly with FCM and APNs may be necessary. The Expo platform does not lock you into using Expo Application Services, and the expo-notifications API is push-service agnostic.

注意:本指南并不旨在成为通过 FCM 或 APNs 发送通知的全面资源。我们建议你阅读官方文档,以确保你遵循最新的操作说明。

获取 FCM 或 APN 的设备令牌

🌐 Obtaining a device token for FCM or APNs

使用 Expo 通知服务时,你使用通过 getExpoPushTokenAsync 获取的 ExpoPushToken

🌐 When using Expo notification service, you use the ExpoPushToken obtained with getExpoPushTokenAsync.

如果你想通过 FCM 或 APNs 发送通知,你需要使用 getDevicePushTokenAsync 获取原生设备令牌。

🌐 If you instead want to send notifications via FCM or APNs, you need to obtain the native device token with getDevicePushTokenAsync.

App.js
11import * as Notifications from 'expo-notifications';
22// ...
3const token = (await Notifications.getExpoPushTokenAsync()).data;
3const token = (await Notifications.getDevicePushTokenAsync()).data;
44// send token to your server

FCMv1 服务器

🌐 FCMv1 server

本指南基于Fir​​ebase 官方文档

🌐 This guide is based on Firebase official documentation.

与 FCM 的通信是通过发送 POST 请求完成的。但是,在发送或接收任何通知之前,你需要按照步骤配置 FCM并获取你的 FCM-SERVER-KEY

🌐 Communicating with FCM is done by sending a POST request. However, before sending or receiving any notifications, you'll need to follow the steps to configure FCM and get your FCM-SERVER-KEY.

获取身份验证令牌

🌐 Getting an authentication token

FCM 需要一个 Oauth 2.0 访问令牌,必须通过《更新发送请求的授权》中描述的某种方法获得。

🌐 FCM requires an Oauth 2.0 access token, which must be obtained via one of the methods described in "Update authorization of send requests".

出于测试目的,你可以使用 Google Auth 库和上面获得的私钥文件来获取单个通知的短期令牌,如从 Firebase 文档改编的此 Node 示例所示:

🌐 For testing purposes, you can use the Google Auth Library and your private key file obtained above, to obtain a short lived token for a single notification, as in this Node example adapted from Firebase documentation:

import { JWT } from 'google-auth-library'; function getAccessTokenAsync( key: string // Contents of your FCM private key file ) { return new Promise(function (resolve, reject) { const jwtClient = new JWT( key.client_email, null, key.private_key, ['https://www.googleapis.com/auth/cloud-platform'], null ); jwtClient.authorize(function (err, tokens) { if (err) { reject(err); return; } resolve(tokens.access_token); }); }); }

发送通知

🌐 Sending the notification

下面的示例代码调用上面的 getAccessTokenAsync() 来获取 Oauth 2.0 令牌,然后构造并发送通知 POST 请求。请注意,与 FCM 旧版协议不同,请求的端点包含你的 Firebase 项目的名称。

🌐 The example code below calls getAccessTokenAsync() above to get the Oauth 2.0 token, then constructs and sends the notification POST request. Note that unlike FCM legacy protocol, the endpoint for the request includes the name of your Firebase project.

// FCM_SERVER_KEY: Environment variable with the path to your FCM private key file // FCM_PROJECT_NAME: Your Firebase project name // FCM_DEVICE_TOKEN: The client's device token (see above in this document) async function sendFCMv1Notification() { const key = require(process.env.FCM_SERVER_KEY); const firebaseAccessToken = await getAccessTokenAsync(key); const fcmToken = process.env.FCM_DEVICE_TOKEN; const messageBody = { message: { token: fcmToken, data: { channelId: 'default', message: 'Testing', title: `This is an FCM notification message`, body: JSON.stringify({ title: 'bodyTitle', body: 'bodyBody' }), scopeKey: '@yourExpoUsername/yourProjectSlug', experienceId: '@yourExpoUsername/yourProjectSlug', }, }, }; const response = await fetch( `https://fcm.googleapis.com/v1/projects/${process.env.FCM_PROJECT_NAME}/messages:send`, { method: 'POST', headers: { Authorization: `Bearer ${firebaseAccessToken}`, Accept: 'application/json', 'Accept-encoding': 'gzip, deflate', 'Content-Type': 'application/json', }, body: JSON.stringify(messageBody), } ); const readResponse = (response: Response) => response.json(); const json = await readResponse(response); console.log(`Response JSON: ${JSON.stringify(json, null, 2)}`); }

experienceIdscopeKey 字段仅在使用 Expo Go 时适用(从 SDK 53 开始,Expo Go 不再支持推送通知)。否则,你的通知将无法发送到你的应用。FCM 在 notification payload 中列出了支持的字段,你可以通过查看 FirebaseRemoteMessage 来了解 Android 上哪些字段被 expo-notifications 支持。

🌐 The experienceId and scopeKey fields are only applicable when using Expo Go (from SDK 53, push notifications support is removed from Expo Go). Otherwise, your notifications will not go through to your app. FCM has a list of supported fields in the notification payload, and you can see which ones are supported by expo-notifications on Android by looking at the FirebaseRemoteMessage.

FCM 还提供了一些 服务器端库,支持几种不同的语言,你可以使用它们来替代原生的 fetch 请求。

🌐 FCM also provides some server-side libraries in a few different languages you can use instead of raw fetch requests.

如何查找 FCM 服务器密钥

🌐 How to find FCM server key

你的 FCM 服务器密钥可以通过确保你已经完成了配置步骤来找到,而不是将你的 FCM 密钥上传到 Expo,你可以直接在服务器中使用该密钥(如前面示例中的 FCM-SERVER-KEY)。

🌐 Your FCM server key can be found by making sure you've followed the configuration steps, and instead of uploading your FCM key to Expo, you would use that key directly in your server (as the FCM-SERVER-KEY in the previous example).

APNs 服务器

🌐 APNs server

信息 本文档基于苹果的文档,本节介绍了入门的基础内容。

与 APNs 通信比与 FCM 通信要复杂一些。一些库将所有这些功能封装到一两个函数调用中,例如 node-apn。然而,在下面的例子中,只使用了最少的库。

🌐 Communicating with APNs is a little more complicated than with FCM. Some libraries wrap all of this functionality into one or two function calls such as node-apn. However, in the examples below, a minimum set of libraries are used.

客户端 APN 授权

🌐 Client APNs entitlement

只有在你的 iOS 应用具备 APNs 权限时,才能接收推送通知。对于使用 CNG 的应用,需要通过以下两种方式之一修改 Expo 配置:

🌐 Receiving push notifications only works if your iOS app has the APNs entitlement. For apps using CNG, the Expo config needs to be modified in one of two ways:

  • 推荐:将 expo-notifications 库添加到你的应用中,并确保其插件出现在你的 应用配置plugins 数组中:
app.json
{ "expo": { %%placeholder-start%%... %%placeholder-end%% "plugins": [ %%placeholder-start%%... %%placeholder-end%% "expo-notifications" ] } }
app.json
{ "expo": { %%placeholder-start%%... %%placeholder-end%% "ios": { %%placeholder-start%%... %%placeholder-end%% "entitlements": { "aps-environment": "development" } } } }

如果你没有使用 CNG,那么你应该在 Xcode 中添加推送通知权限

🌐 If you are not using CNG, then you should add the push notification entitlement in Xcode.

注意:如果你正在将 Expo 应用从 SDK 51 及更早版本升级,你应参考 此说明文档

授权

🌐 Authorization

最初,在向 APNS 发送请求之前,你需要获得向你的应用发送通知的权限。这是通过使用 iOS 开发者凭证生成的 JSON Web 令牌来授予的:

🌐 Initially, before sending requests to APNS, you need permission to send notifications to your app. This is granted via a JSON web token which is generated using iOS developer credentials:

  • 与你的应用关联的 APN 密钥(.p8 文件)
  • 上述 .p8 文件的密钥 ID
  • 你的 Apple 团队 ID
const jwt = require("jsonwebtoken"); const authorizationToken = jwt.sign( { iss: "YOUR-APPLE-TEAM-ID" iat: Math.round(new Date().getTime() / 1000), }, fs.readFileSync("./path/to/appName_apns_key.p8", "utf8"), { header: { alg: "ES256", kid: "YOUR-P8-KEY-ID", }, } );

HTTP/2 连接

🌐 HTTP/2 connection

获取 authorizationToken 后,你可以打开与苹果服务器的 HTTP/2 连接。在开发环境中,请向 api.sandbox.push.apple.com 发送请求。在生产环境中,请向 api.push.apple.com 发送请求。

🌐 After getting the authorizationToken, you can open up an HTTP/2 connection to Apple's servers. In development, send requests to api.sandbox.push.apple.com. In production, send requests to api.push.apple.com.

以下是构建请求的方法:

🌐 Here's how to construct the request:

const http2 = require('http2'); const client = http2.connect( IS_PRODUCTION ? 'https://api.push.apple.com' : 'https://api.sandbox.push.apple.com' ); const request = client.request({ ':method': 'POST', ':scheme': 'https', 'apns-topic': 'YOUR-BUNDLE-IDENTIFIER', ':path': '/3/device/' + nativeDeviceToken, // This is the native device token you grabbed client-side authorization: `bearer ${authorizationToken}`, // This is the JSON web token generated in the "Authorization" step }); request.setEncoding('utf8'); request.write( JSON.stringify({ aps: { alert: { title: "\uD83D\uDCE7 You've got mail!", body: 'Hello world! \uD83C\uDF10', }, }, experienceId: '@yourExpoUsername/yourProjectSlug', // Required only when testing in legacy Expo Go (in SDK 52 and earlier) scopeKey: '@yourExpoUsername/yourProjectSlug', // Required only when testing in legacy Expo Go (in SDK 52 and earlier) }) ); request.end();

这个示例是最简化的,未包括错误处理和连接池。用于测试时,你可以参考 sendNotificationToAPNS 示例代码。

APNs 在 通知负载 中提供其支持字段的完整列表。

🌐 APNs provide their full list of supported fields in the notification payload.