[自動版] Expo SDK 53 Firebase FCM 自動化配置指南

本文是 Expo SDK 53 Firebase Cloud Messaging (FCM) 完整修復指南 的延續版本,提供完全自動化的解決方案。

前言:從手動到自動化的進化

在我之前的文章中,我們解決了 Expo SDK 53 中 Firebase FCM 的配置問題,但那個方案需要手動修改原生文件。這種方法雖然有效,但存在以下問題:

  • 不可重現:每次 expo prebuild 都會覆蓋手動修改
  • 團隊協作困難:其他開發者需要記住手動步驟
  • 版本控制混亂:原生文件變更難以追蹤
  • 維護成本高:容易忘記修改步驟

今天,我將分享一個完全自動化的解決方案,讓你告別手動修改的煩惱!

💡 核心問題回顧

正如原文所述,Expo SDK 53 中 Firebase FCM 無法正常工作的根本原因是:

  1. CocoaPods modular headers 衝突
  2. Firebase 初始化時機不正確
  3. @react-native-firebase/app 插件無法識別 Expo 的 AppDelegate 結構

🚀 完全自動化解決方案

步驟 1: 正確配置 app.json

{
  "expo": {
    "plugins": [
      // ... 你的其他插件 (expo-router, expo-font 等)
      [
        "@react-native-firebase/app",
        {
          "googleServicesFile": "./GoogleService-Info.plist",
          "ios": {
            "bundleId": "com.yourapp.bundle"
          }
        }
      ],
      "@react-native-firebase/messaging",
      [
        "expo-build-properties",
        {
          "ios": {
            "useFrameworks": "static"
          }
        }
      ]
    ]
  }
}

關鍵改進:

  • ✅ 使用 expo-build-propertiesuseFrameworks: "static" 從根本解決 modular headers 問題
  • ✅ 明確指定 googleServicesFile 路徑讓插件能找到配置文件
  • 只添加必要的 Firebase 相關插件,不強制使用其他非必需插件

步驟 2: 創建自動化插件

創建 plugins/ 資料夾並添加以下插件:

plugins/withFirebaseAppDelegate.js

const { withAppDelegate } = require('@expo/config-plugins');

function withFirebaseAppDelegate(config) {
  return withAppDelegate(config, config => {
    if (config.modResults.language === 'swift') {
      let contents = config.modResults.contents;

      // 自動注入 Firebase 初始化
      if (!contents.includes('FirebaseApp.configure()')) {
        const functionStart = contents.indexOf('didFinishLaunchingWithOptions launchOptions:');
        if (functionStart !== -1) {
          const openBrace = contents.indexOf('{', functionStart);
          if (openBrace !== -1) {
            const insertPoint = openBrace + 1;
            const firebaseConfig = '\n    FirebaseApp.configure()\n    ';
            contents = contents.slice(0, insertPoint) + firebaseConfig + contents.slice(insertPoint);
            config.modResults.contents = contents;
          }
        }
      }

      // 自動添加 FCM delegate
      if (!contents.includes('UNUserNotificationCenter.current().delegate = self')) {
        const superCall = contents.indexOf('return super.application(application, didFinishLaunchingWithOptions: launchOptions)');
        if (superCall !== -1) {
          const insertPoint = superCall;
          const delegateConfig = '\n        if #available(iOS 10.0, *) {\n      UNUserNotificationCenter.current().delegate = self\n    }\n\n    ';
          contents = contents.slice(0, insertPoint) + delegateConfig + contents.slice(insertPoint);
          config.modResults.contents = contents;
        }
      }
    }
    return config;
  });
}

module.exports = withFirebaseAppDelegate;

plugins/withFirebasePodfile.js

const { withPodfile } = require('@expo/config-plugins');

function withFirebasePodfile(config) {
  return withPodfile(config, config => {
    const podfileContents = config.modResults.contents;

    // 檢查是否已經有 Firebase modular headers 配置
    if (!podfileContents.includes('pod \'GoogleUtilities\', :modular_headers => true')) {
      // 在 target 'YourApp' do 後添加 Firebase modular headers
      const targetRegex = /target\s+['"][^'"]+['"]\s+do/;
      const match = podfileContents.match(targetRegex);

      if (match) {
        const insertIndex = podfileContents.indexOf(match[0]) + match[0].length;
        const firebaseConfig = `
  # Firebase modular headers fix for Expo SDK 53+
  pod 'GoogleUtilities', :modular_headers => true
  pod 'FirebaseCore', :modular_headers => true
  pod 'FirebaseCoreInternal', :modular_headers => true
  pod 'FirebaseMessaging', :modular_headers => true
  pod 'FirebaseInstallations', :modular_headers => true
`;

        config.modResults.contents = 
          podfileContents.slice(0, insertIndex) + 
          firebaseConfig + 
          podfileContents.slice(insertIndex);
      }
    }

    return config;
  });
}

module.exports = withFirebasePodfile;

步驟 3: 更新完整的 app.json

{
  "expo": {
    "plugins": [
      // ... 你現有的插件 (根據項目需求)
      [
        "@react-native-firebase/app",
        {
          "googleServicesFile": "./GoogleService-Info.plist",
          "ios": {
            "bundleId": "com.yourapp.bundle"
          }
        }
      ],
      "@react-native-firebase/messaging",
      [
        "expo-build-properties", 
        {
          "ios": {
            "useFrameworks": "static"
          }
        }
      ],
      "./plugins/withFirebaseAppDelegate.js",
      "./plugins/withFirebasePodfile.js"
    ]
  }
}

重要說明: 只需要添加與 Firebase FCM 相關的插件,如果你原本就用到 expo-routerexpo-font 等插件,請在你原本的設定後面加上。

步驟 4: 完全自動化部署

# 一鍵重建整個專案
rm -rf ios
npx expo prebuild --platform ios
npx expo run:ios

✨ 自動化方案的優勢

🎯 完全可重現

  • 任何團隊成員執行 expo prebuild 都會得到相同配置
  • 不需要記憶任何手動步驟

🚀 零維護成本

  • 插件會在每次 prebuild 時自動運行
  • 配置邏輯版本控制化,易於維護

👥 團隊友好

  • 新團隊成員只需 npm install + expo prebuild
  • 所有配置都在代碼庫中

🛡️ 版本升級保險

  • Expo SDK 升級時,只需調整插件邏輯
  • 不會意外覆蓋手動修改

🔧 進階配置選項

條件性配置

// 只在特定環境下啟用某些功能
const isDevelopment = process.env.NODE_ENV === 'development';

if (isDevelopment) {
  // 開發環境特定配置
  contents = contents.replace(
    'aps-environment": "production"',
    'aps-environment": "development"'
  );
}

多環境支持

// 根據不同環境使用不同的 Firebase 配置
const environment = process.env.EXPO_PUBLIC_ENVIRONMENT || 'development';
const googleServicesFile = `./GoogleService-Info-${environment}.plist`;

🚨 常見問題排除

問題 1: 插件沒有執行

解決方案: 確保插件路徑正確,且文件有正確的 module.exports

問題 2: Firebase 初始化位置不對

解決方案: 檢查 AppDelegate 結構,調整插件中的文本匹配邏輯

問題 3: CocoaPods 衝突

解決方案: 確保 expo-build-propertiesuseFrameworks: "static" 配置正確

📊 方案比較

特性 手動方案 自動化方案
設置複雜度 簡單 中等
維護成本
團隊協作 困難 簡單
可重現性 完美
錯誤風險

🎉 結論

這個自動化方案徹底解決了 Expo SDK 53 Firebase FCM 的配置問題,同時提供了:

  • 完全自動化:無需任何手動步驟
  • 完全可重現:任何時候都能重建相同環境
  • 團隊友好:新成員零學習成本
  • 未來兼容:易於適應 SDK 升級

告別手動修改的時代,擁抱自動化配置的未來!


技術背景

理論上,只配置 @react-native-firebase/appexpo-build-properties 就應該能自動適配所有必要設定。但在當前版本中,由於 @react-native-firebase/app 插件對 Expo 的Swift AppDelegate 結構識別有限制,我們需要額外的自定義插件來補足這個缺陷。隨著 Expo 和 Firebase 插件的版本更新,未來可能會簡化這個流程

相關資源