課程:RN 跨平台開發基礎 第 10 堂:原生功能整合
推播通知完整架構
為什麼推播通知(Push Notifications)被認為是行動 App 開發中最令人頭痛的環節之一?
想像一下,如果你要開發一個像 AuraFlow 這樣的 App,當管理員發布「站方公告」或「新功能上線」時,你希望幾萬名使用者的手機同時亮起。這聽起來像是一個簡單的 API 呼叫,但背後涉及的卻是 Apple、Google 兩大科技巨頭對其作業系統功耗的極致控制,以及複雜的權限、Token 代換與網路協議。
如果你直接去串接原生推播,你需要處理 Apple 的 APNs(Apple Push Notification service)和 Google 的 FCM(Firebase Cloud Messaging),這意味著你需要維護兩套不同的憑證、兩套資料格式,以及兩套錯誤處理機制。而這正是 Expo Push Notifications 存在的價值——它像是一個「通用遙控器」,讓你用同一種姿勢,控制兩台完全不同的電視。
為什麼推播需要兩套系統?(APNs 與 FCM)
在深入 Expo 的機制之前,我們必須先理解行動裝置的底層限制。
為了節省電力,手機的作業系統(iOS 或 Android)不允許每個 App 都自己在背景維持一個與伺服器的長連接(Long Connection)。如果手機裡 50 個 App 都在背景連線等訊息,電池不到兩小時就會耗盡。因此,作業系統接管了這件事:
- iOS: Apple 規定所有的通知必須經過其唯一的閘道器 APNs。iOS 系統層會維持一條極低功耗的連線到 APN 伺服器。
- Android: Google 同樣規定透過 FCM(或是中國市場常見的各家廠商推送服務)來達成類似目的。
這創造了一個技術斷層:你的後端伺服器沒辦法直接「聯絡」使用者的手機,它必須先聯絡 Apple 或 Google 的伺服器,再由它們轉發。
Expo Push Service 的角色
Expo 提供了一層抽象化的伺服器,將這個流程簡化為:
你的後端 → Expo Push Service → (轉發給 APNs/FCM) → 終端裝置
這意味著你不需要在後端去判斷這個使用者是用 iPhone 還是 Android,你只需要統一將訊息發送給 Expo,並攜帶一個叫做 Expo Push Token 的身分證字號,剩下的髒活(例如處理 Apple 的 HTTP/2 協議或 Google 的 JSON 格式)都交給 Expo 處理。
Expo Push Token 的生命週期與管理
在推播的世界裡,最重要的資產就是 Token。沒有 Token,你就找不到使用者的裝置。
1. 原生 Device Token vs. Expo Push Token
這是一個常見的混淆點:
- 原生 Device Token: 是由 iOS/Android 系統核發的原始字串。它是 APNs 或 FCM 辨識裝置的唯一標記。
- Expo Push Token: 形如
ExponentPushToken[xxxxxxxxxxxx]。它是 Expo 針對該裝置生成的「代號」。
為什麼不直接用原生 Token? 因為如果你直接用原生 Token,你的後端就必須自己去處理 APNs 與 FCM 的差異。透過 Expo Token,你的後端可以維持一致的格式,這對開發者來說是巨大的負擔減輕。
2. Token 的取得流程
在 AuraFlow 中,取得 Token 的標準動作通常發生在「使用者登入後」或「App 啟動時」。
- 權限檢查: 首先檢查使用者是否允許通知。
- 呼叫 API: 使用
Notifications.getExpoPushTokenAsync()。 - 上傳後端: 這是最重要的步驟。你必須把這個 Token 發送到你的 API,並將其與「使用者 ID」綁定。
3. 多裝置情境下的 Token 管理
以 AuraFlow 為例,如果一個使用者同時在 iPad 和 Android 手機上登入同一個帳號,你的資料庫應該如何設計?
錯誤的設計: User Table 中只有一欄 push_token。這樣當使用者在 iPad 登入時,Android 手機的 Token 就會被蓋掉。
正確的設計: 一個 User 對應多個 Token 的「一對多」關係。
User: Aria (ID: 101)
Tokens:
- ExponentPushToken[iOS_iPad_123] (Last used: 2023-10-27)
- ExponentPushToken[Android_Phone_456] (Last used: 2023-10-26)
當你要發送「新功能通知」給 Aria 時,後端應該撈出所有與該 ID 綁定的 Token,並分別發送。
4. Token 什麼時候會失效?
Token 並非永恆不變的。以下場景可能導致 Token 失效:
- App 重新安裝: 系統通常會核發新的 Token。
- 使用者在設定中關閉權限: 雖然 Token 可能還在,但發送會失敗。
- 清除資料(Android): 可能導致 Token 重置。
策略建議: 每次 App 啟動時,都應該重新執行一次「獲取 Token 並同步到後端」的動作。如果後端發現該 Token 已存在,則更新其「最後活動時間」;如果是新的,則新增。
App 三種狀態下的通知行為差異
這是在開發中最常遇到「為什麼通知沒反應?」的地方。你的 App 處於什麼狀態,決定了誰來處理這則通知。
1. 已關閉(Killed / Terminated)
這是最單純的情況。作業系統完全接管。
- 行為: 手機上方彈出 Banner(標籤),播放系統預設音效。
- 處理: 當使用者點擊通知時,作業系統會「啟動」你的 App。
2. 背景(Background)
App 縮小,但在任務切換器中還看得到。
- 行為: 作業系統顯示 Banner。
- 處理: 點擊通知會將 App 喚至前景。
3. 前景(Foreground)
App 正在畫面中運行。
- 行為: iOS 與 Android 預設「不會」顯示 Banner! 這是為了避免干擾正在使用 App 的人。
- 處理: 你的程式碼必須主動監聽
addNotificationReceivedListener。
在 AuraFlow 中,如果你在 App 開著的時候收到「新功能修正」的通知,你可能不希望彈出一個擋住畫面的大標籤,而是希望在畫面頂端顯示一個柔和的小通知列,或者在「設定」分頁顯示一個紅點。這就需要利用監聽器來實作:
// 在前景收到通知時的處理邏輯
Notifications.addNotificationReceivedListener(notification => {
const { title, body, data } = notification.request.content;
// 在這裡你可以決定是要顯示自定義的彈窗,還是靜默更新 UI
console.log("前景收到推播:", title);
});
點擊通知後的跳頁邏輯:整合 Deep Linking
推播通知不只是為了「告知」,通常是為了「導流」。 例如:AuraFlow 發送一則「新的人類圖報告已生成」通知,使用者點擊後,應該直接進入「報告詳情頁」,而不是 App 的首頁。
這需要結合我們在 Topic 3 學過的 Deep Linking 與導覽架構。
1. Payload 資料設計
當你從後端發送通知給 Expo 時,你可以附帶一個 data 物件:
{
"to": "ExponentPushToken[...]",
"title": "新報告生成",
"body": "點擊查看你的人類圖深度解析",
"data": {
"url": "auraflow://report/12345"
}
}
2. 點擊監聽(Response Listener)
你需要監聽 addNotificationResponseReceivedListener。這個監聽器專門處理「使用者與通知互動」(通常是點擊)的動作。
Notifications.addNotificationResponseReceivedListener(response => {
const url = response.notification.request.content.data.url;
if (url) {
// 使用 Linking API 開啟 URL,
// React Navigation 會根據 Topic 3 設定好的 linking config 自動跳轉
Linking.openURL(url);
}
});
這裡最常踩的坑是:如果 App 是被點擊通知從「已關閉」狀態啟動的,監聽器可能還沒掛載完成。
這就是為什麼我們在導覽架構中推薦將 Deep Linking 配置在 NavigationContainer 的 linking 屬性中,因為它會自動處理 App 啟動時的 URL 解析。
避雷指南:跨平台的細節與坑
Android 13+ 的權限陷阱
從 Android 13(API 33)開始,Google 效仿 iOS,將通知權限改為「選擇性加入」。你必須在 AndroidManifest.xml(或 Expo 的 app.json)中聲明 POST_NOTIFICATIONS 權限,並在 App 運行時明確詢問使用者。如果你忽略這一步,你的 App 在最新的 Android 手機上將永遠保持安靜。
模擬器的限制
- iOS 模擬器: 雖然現在部分支援推播測試,但行為與實體機仍有差異。強烈建議使用實體裝置測試推播。
- Android 模擬器: 必須包含 Google Play Services 才能運行推播。
廣播式通知的成本
如果你要發送給 10 萬名使用者,千萬不要在後端用一個 for 迴圈跑 10 萬次發送請求。這會導致你的伺服器超載。
Expo 支援 Batch Sending(分批發送)。你可以一次發送 100 個 Token 給 Expo,讓 Expo 的伺服器去處理負載與排隊。
核心心智模型總結
推播通知的架構可以總結為以下這條路徑:
- 註冊階段: App 啟動 → 檢查權限 → 取得 Expo Push Token → 儲存在你自己的後端資料庫(與 UserID 綁定)。
- 發送階段: 管理員觸發公告 → 後端撈出 UserIDs 對應的所有 Tokens → POST 給 Expo Push Service。
- 接收階段:
- 前景: App JS 層監聽並處理(不顯示標籤)。
- 背景/關閉: 系統顯示標籤 → 使用者點擊 → 觸發 Deep Link 跳轉至指定頁面。
理解了這個流程,你就不會再被「為什麼 iOS 收得到但 Android 收不到」這種問題困擾,因為你可以精確地判斷:是 Token 沒上傳?還是後端沒發送給 Expo?或者是權限沒開?
學習回顧:從檔案到推播
在上一 Part 中,我們討論了如何處理「由內而外」的資料流(從 App 匯出文件到系統)。這一 Part 我們則攻克了「由外而內」的訊息流。兩者的共同點在於:它們都涉及 JS 層與原生作業系統服務的頻繁溝通。
推播通知的成功率很大程度取決於 Token 的清潔度(清理無效 Token)以及對權限請求時機的把握。在接下來的最後一個部分,我們將整合這些原生功能的實踐,探討如何建立一個系統性的「權限請求策略」,以及在媒體類 App 中極為關鍵的「語音朗讀(TTS)」整合邏輯,包含如何在背景播放時處理音訊中斷。