expo-notifications 如何绕过 Expo 推送服务直接用 FCM 和 APNs 发送通知
【免费下载链接】expoAn open-source framework for making universal native apps with React. Expo runs on Android, iOS, and the web.项目地址: https://gitcode.com/GitHub_Trending/ex/expo
如果你的通知需要比 Expo 推送服务更细粒度的控制,Expo 平台并不锁定你只能使用 Expo Application Services:expo-notifications的 API 与推送服务无关,你可以拿到客户端原生设备令牌,然后由自己的服务器直接向 FCM(Android)和 APNs(iOS)发送推送。本文给出这条路径的完整操作:获取原生设备令牌、准备服务端凭证、构造并发送 FCM v1 与 APNs 请求。注意,官方文档明确说明 Send notifications with FCM and APNs 不是 FCM/APNs 推送的全面教程,建议同时对照 Firebase 与 Apple 的官方文档确认最新要求。
前提:客户端改用原生设备令牌
走 Expo 推送服务时,客户端用getExpoPushTokenAsync获取ExpoPushToken。改为直连 FCM/APNs 后,需要换成用getDevicePushTokenAsync获取的原生设备令牌,并将其发送到你的服务器:
import * as Notifications from 'expo-notifications'; // ... - const token = (await Notifications.getExpoPushTokenAsync()).data; + const token = (await Notifications.getDevicePushTokenAsync()).data; // send token to your server之后 FCM 与 APNs 两条链路都以这个令牌为发送目标。
Android 侧:配置 FCM 并使用 FCM v1 协议
1. 拿到 FCM 服务端私钥
直连 FCM 需要 配置 FCM 后得到的FCM-SERVER-KEY(Google Service Account 私钥 JSON)。操作路径与 Add Android FCM V1 credentials 相同:在 Firebase Console 创建(或选择已有)项目,进入Project settings > Service accounts,点击Generate New Private Key并保存 JSON 文件。
区别在于:走 Expo 推送服务时该文件要上传到 EAS;直连场景下,把同一个私钥文件直接放在你自己的服务器上使用,即下文代码中的FCM_SERVER_KEY。私钥 JSON 含敏感数据,官方文档建议将其加入.gitignore,不要提交到仓库。
同时确认google-services.json已配置:从 Firebase Console 下载并放到项目根目录,并在app.json中通过expo.android.googleServicesFile指向它,这是 Android 应用注册到 FCM 的前提:
{ "expo": { "android": { "googleServicesFile": "./path/to/google-services.json" } } }2. 获取 OAuth 2.0 访问令牌
FCM v1 要求用 OAuth 2.0 access token 授权请求,文档建议参照 Firebase 的 “Update authorization of send requests” 一节。测试场景下,可以用google-auth-library和上一步的私钥文件换取短时令牌:
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); }); }); }3. 构造并发送 POST 请求
FCM v1 的端点与旧版 legacy 协议不同,请求地址中包含你的 Firebase 项目名:
// 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)}`); }环境变量说明:FCM_SERVER_KEY是服务器上私钥 JSON 文件的路径,FCM_PROJECT_NAME是你的 Firebase 项目名称,FCM_DEVICE_TOKEN是第一步中客户端上报的原生设备令牌。data里的scopeKey与experienceId只在通过 Expo Go 测试时适用(自 SDK 53 起,Expo Go 已移除推送通知支持);你自己的开发构建不需要这两个字段。
FCM 支持哪些 payload 字段以 notification payload 为准,expo-notifications在 Android 侧实际支持的字段可查 SDK 文档中的FirebaseRemoteMessage定义。如果你不想用裸fetch,FCM 也提供了多种语言的 服务端 SDK。
结果验证:发送函数会打印 FCM 返回的 JSON(Response JSON: ...),用它确认请求被 FCM 服务端接受;data.title/data.body中的标题与正文最终显示在设备的系统通知里。
iOS 侧:配置 APNs 并建立 HTTP/2 连接
1. 给 App 添加 APNs entitlement
iOS 应用只有在具有 APNs entitlement 时才能收到推送。使用 CNG(Continuous Native Generation)构建时,推荐把expo-notifications加入app.json的plugins数组:
{ "expo": { "plugins": [ "expo-notifications" ] } }如果不用expo-notifications库,则手动在配置中写入aps-environmententitlement:
{ "expo": { "ios": { "entitlements": { "aps-environment": "development" } } } }不使用 CNG 时,需要在 Xcode 中添加推送通知 entitlement(参照 Apple 文档 “Registering your app with APNs”)。另外,如果你把 SDK 51 及更早版本的应用升级到新版 Expo,Apple 侧的 entitlement 处理方式有变化,文档单独给出了迁移说明链接,升级前先查阅。
2. 用 .p8 密钥生成 JWT
向 APNs 发请求前需要发送权限,由一个用 iOS 开发者凭据生成的 JSON Web Token 授予,需要三样东西:与 App 关联的 APN key(.p8文件)、该 key 的 Key ID、你的 Apple Team 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", }, } );示例中的YOUR-APPLE-TEAM-ID、YOUR-P8-KEY-ID和.p8文件路径需替换为你自己的值。
3. 打开 HTTP/2 连接并发送
拿到authorizationToken后,向 Apple 服务器发起 HTTP/2 连接:开发环境请求api.sandbox.push.apple.com,生产环境请求api.push.apple.com:
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();其中apns-topic需要填你的 bundle identifier,:path中的nativeDeviceToken是客户端通过getDevicePushTokenAsync拿到的原生令牌;experienceId/scopeKey同样只在旧版 Expo Go(SDK 52 及更早)中才需要。APNs 支持的完整 payload 字段以 Apple 的 Payload key reference 为准。
结果验证:请求成功后,设备应在系统通知中显示aps.alert的标题与正文;iOS 侧权限状态可结合Notifications.IosAuthorizationStatus(AUTHORIZED、DENIED、PROVISIONAL等)判断 App 是否被授权接收推送。
限制与排查
- google-services.json 的 API key 受限:如果
client.api_key.current_key被限制,需要在 Google Cloud Console 的API restrictions中放行FCM Registration API和Firebase Installations API(或取消限制);Application restrictions中要使用 Google Play ConsoleRelease > Setup > App Integrity > App signing key certificate里的 SHA-1,而不是 upload key。配置不匹配时 Firebase Installations API 会返回403 PERMISSION_DENIED: Requests from this Android client application are blocked,应用将永远拿不到推送令牌。 - 令牌类型不匹配:
ExpoPushToken不能用于 FCM/APNs 直连,反之原生令牌用于 Expo 推送服务也没有意义。发送前确认客户端调用的是getDevicePushTokenAsync。 - Expo Go 限制:自 SDK 53 起 Expo Go 移除了推送通知支持,直连链路应在你自己的开发构建中验证;payload 里的
scopeKey/experienceId对 Expo Go 场景才生效。 - 文档明确说明 APNs 示例是最小实现,不含错误处理与连接池;生产环境可参考仓库中的
sendNotificationToAPNS示例,或使用node-apn这类封装库替代裸http2调用。
更多问题(如令牌失效、收不到通知等)可查阅 FAQ。
【免费下载链接】expoAn open-source framework for making universal native apps with React. Expo runs on Android, iOS, and the web.项目地址: https://gitcode.com/GitHub_Trending/ex/expo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考