將 Parse iOS 應用程序遷移到 Firebase

如果您是 Parse 用戶,正在尋找替代的後端即服務解決方案,那麼 Firebase 可能是您 iOS 應用程序的理想選擇。

本指南描述瞭如何將特定服務集成到您的應用程序中。有關基本 Firebase 設置說明,請參閱iOS+ 設置指南。

谷歌分析

Google Analytics 是一種免費的應用衡量解決方案,可提供有關應用使用情況和用戶參與度的洞察。 Analytics 集成了多個 Firebase 功能,可為您提供最多 500 個不同事件的無限報告,您可以使用 Firebase SDK 定義這些事件。

請參閱Google Analytics 文檔以了解更多信息。

建議的遷移策略

使用不同的分析提供程序是很容易應用於 Google Analytics 的常見方案。只需將其添加到您的應用中,即可從 Analytics 自動收集的事件和用戶屬性中受益,例如首次打開、應用更新、設備型號、年齡。

對於自定義事件和用戶屬性,您可以使用 Parse Analytics 和 Google Analytics 的雙重寫入策略來記錄事件和屬性,這使您可以逐步推出新的解決方案。

代碼比較

解析分析

// Start collecting data
[PFAnalytics trackAppOpenedWithLaunchOptions:launchOptions];

NSDictionary *dimensions = @{
  // Define ranges to bucket data points into meaningful segments
  @"priceRange": @"1000-1500",
  // Did the user filter the query?
  @"source": @"craigslist",
  // Do searches happen more often on weekdays or weekends?
  @"dayType": @"weekday"
};
// Send the dimensions to Parse along with the 'search' event
[PFAnalytics trackEvent:@"search" dimensions:dimensions];

谷歌分析

// Obtain the AppMeasurement instance and start collecting data
[FIRApp configure];

// Send the event with your params
[FIRAnalytics logEventWithName:@"search" parameters:@{
  // Define ranges to bucket data points into meaningful segments
  @"priceRange": @"1000-1500",
  // Did the user filter the query?
  @"source": @"craigslist",
  // Do searches happen more often on weekdays or weekends?
  @"dayType": @"weekday"
}];

Firebase 實時數據庫

Firebase 實時數據庫是一個 NoSQL 雲託管數據庫。數據以 JSON 格式存儲並實時同步到每個連接的客戶端。

請參閱Firebase 實時數據庫文檔以了解更多信息。

解析數據的差異

對象

在 Parse 中,您存儲一個PFObject或其子類,其中包含與 JSON 兼容的數據的鍵值對。數據是無模式的,這意味著您不需要指定每個PFObject上存在哪些鍵。

所有 Firebase 實時數據庫數據都存儲為 JSON 對象, PFObject沒有等價物;您只需寫入與可用 JSON 類型相對應的類型的 JSON 樹值。

以下是如何保存遊戲高分的示例。

解析
PFObject *gameScore = [PFObject objectWithClassName:@"GameScore"];
gameScore[@"score"] = @1337;
gameScore[@"playerName"] = @"Sean Plott";
gameScore[@"cheatMode"] = @NO;
[gameScore saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
  if (succeeded) {
    // The object has been saved.
  } else {
    // There was a problem, check error.description
  }
}];
火力基地
// Create a reference to the database
FIRDatabaseReference *ref = [[FIRDatabase database] reference];
NSString *key = [[ref child:@"scores"] childByAutoId].key;
NSDictionary *score = @{@"score": @1337,
                        @"playerName": @"Sean Plott",
                        @"cheatMode": @NO};
[key setValue:score withCompletionBlock:^(NSError *error,  FIRDatabaseReference *ref) {
  if (error) {
    // The object has been saved.
  } else {
    // There was a problem, check error.description
  }
}];
有關更多詳細信息,請查看 Apple 平台上的讀取和寫入數據指南。

數據之間的關係

一個PFObject可以與另一個PFObject有關係:任何對像都可以使用其他對像作為值。

在 Firebase 實時數據庫中,使用平面數據結構更好地表達關係,將數據拆分為單獨的路徑,以便可以在單獨的調用中有效地下載它們。

以下示例說明瞭如何構建博客應用程序中的帖子與其作者之間的關係。

解析
// Create the author
PFObject *myAuthor = [PFObject objectWithClassName:@"Author"];
myAuthor[@"name"] = @"Grace Hopper";
myAuthor[@"birthDate"] = @"December 9, 1906";
myAuthor[@"nickname"] = @"Amazing Grace";

// Create the post
PFObject *myPost = [PFObject objectWithClassName:@"Post"];
myPost[@"title"] = @"Announcing COBOL, a New Programming Language";

// Add a relation between the Post and the Author
myPost[@"parent"] = myAuthor;

// This will save both myAuthor and myPost
[myPost saveInBackground];
火力基地
// Create a reference to the database
FIRDatabaseReference *ref = [[FIRDatabase database] reference];

// Create the author
NSString *myAuthorKey = @"ghopper";
NSDictionary *author = @{@"name": @"Grace Hopper",
                         @"birthDate": @"December 9, 1906",
                         @"nickname": @"Amazing Grace"};
// Save the author
[[ref child:myAuthorKey] setValue:author]

// Create and save the post
NSString *key = [[ref child:@"posts"] childByAutoId].key;
NSDictionary *post = @{@"author": myAuthorKey,
                       @"title": @"Announcing COBOL, a New Programming Language"};
[key setValue:post]

以下數據佈局是結果。

{
  // Info about the authors
  "authors": {
    "ghopper": {
      "name": "Grace Hopper",
      "date_of_birth": "December 9, 1906",
      "nickname": "Amazing Grace"
    },
    ...
  },
  // Info about the posts: the "author" fields contains the key for the author
  "posts": {
    "-JRHTHaIs-jNPLXOQivY": {
      "author": "ghopper",
      "title": "Announcing COBOL, a New Programming Language"
    }
    ...
  }
}
有關更多詳細信息,請查看Structure Your Database指南。

讀取數據

在 Parse 中,您可以使用特定 Parse 對象的 ID 讀取數據,或者使用PFQuery執行查詢。

在 Firebase 中,您通過將異步偵聽器附加到數據庫引用來檢索數據。偵聽器會針對數據的初始狀態觸發一次,並在數據更改時再次觸發,因此您無需添加任何代碼來確定數據是否已更改。

以下是如何根據“對象”部分中提供的示例檢索特定玩家的分數的示例。

解析
PFQuery *query = [PFQuery queryWithClassName:@"GameScore"];
[query whereKey:@"playerName" equalTo:@"Dan Stemkoski"];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
  if (!error) {
    for (PFObject *score in objects) {
      NSString *gameScore = score[@"score"];
      NSLog(@"Retrieved: %@", gameScore);
    }
  } else {
    // Log details of the failure
    NSLog(@"Error: %@ %@", error, [error userInfo]);
  }
}];
火力基地
// Create a reference to the database
FIRDatabaseReference *ref = [[FIRDatabase database] reference];

// This type of listener is not one time, and you need to cancel it to stop
// receiving updates.
[[[[ref child:@"scores"] queryOrderedByChild:@"playerName"] queryEqualToValue:@"Dan Stemkoski"]
    observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
  // This will fire for each matching child node.
  NSDictionary *score = snapshot.value;
  NSString gameScore = score[@"score"];
  NSLog(@"Retrieved: %@", gameScore);
}];
有關可用事件偵聽器類型以及如何排序和過濾數據的更多詳細信息,請查看 Apple 平台上的讀取和寫入數據指南。

建議的遷移策略

重新思考您的數據

Firebase 實時數據庫經過優化,可以在所有連接的客戶端之間以毫秒為單位同步數據,並且生成的數據結構與 Parse 核心數據不同。這意味著遷移的第一步是考慮您的數據需要進行哪些更改,包括:

  • Parse 對象應如何映射到 Firebase 數據
  • 如果您有父子關係,如何將數據拆分到不同的路徑,以便可以在單獨的調用中有效下載。

遷移您的數據

在您決定如何在 Firebase 中構建數據後,您需要計劃如何處理您的應用需要寫入兩個數據庫的時間段。您的選擇是:

後台同步

在這種情況下,您有兩個版本的應用程序:使用 Parse 的舊版本和使用 Firebase 的新版本。兩個數據庫之間的同步由 Parse Cloud Code(Parse 到 Firebase)處理,您的代碼監聽 Firebase 上的更改並將這些更改與 Parse 同步。在開始使用新版本之前,您必須:

  • 將現有的 Parse Data 轉換為新的 Firebase 結構,並將其寫入 Firebase 實時數據庫。
  • 編寫使用 Firebase REST API 的 Parse Cloud Code 函數將舊客戶端在 Parse Data 中所做的更改寫入 Firebase 實時數據庫。
  • 編寫和部署代碼以偵聽 Firebase 上的更改並將它們同步到 Parse 數據庫。

這個場景確保了新舊代碼的清晰分離,並使客戶端保持簡單。這種情況的挑戰是在初始導出中處理大數據集,並確保雙向同步不會產生無限遞歸。

雙寫

在這種情況下,您編寫一個同時使用 Firebase 和 Parse 的應用程序的新版本,使用 Parse Cloud Code 將舊客戶端所做的更改從 Parse 數據同步到 Firebase 實時數據庫。當足夠多的人從應用程序的僅 Parse 版本遷移時,您可以從雙重寫入版本中刪除 Parse 代碼。

此方案不需要任何服務器端代碼。它的缺點是未訪問的數據不會遷移,並且您的應用程序的大小會因使用兩個 SDK 而增加。

Firebase 身份驗證

Firebase 身份驗證可以使用密碼和流行的聯合身份提供商(如 Google、Facebook 和 Twitter)對用戶進行身份驗證。它還提供 UI 庫,以節省您在所有平台上為您的應用實施和維護完整身份驗證體驗所需的大量投資。

請參閱Firebase 身份驗證文檔以了解更多信息。

與 Parse Auth 的區別

Parse 提供了一個名為PFUser的專用用戶類,它自動處理用戶帳戶管理所需的功能。 PFUserPFObject的子類,這意味著用戶數據在 Parse Data 中可用,並且可以像任何其他PFObject一樣使用其他字段進行擴展。

FIRUser具有一組固定的基本屬性——唯一 ID、主電子郵件地址、姓名和照片 URL——存儲在單獨項目的用戶數據庫中;這些屬性可以由用戶更新。您不能直接向FIRUser對象添加其他屬性;相反,您可以將其他屬性存儲在 Firebase 實時數據庫中。

以下是您如何註冊用戶並添加附加電話號碼字段的示例。

解析
PFUser *user = [PFUser user];
user.username = @"my name";
user.password = @"my pass";
user.email = @"email@example.com";

// other fields can be set just like with PFObject
user[@"phone"] = @"415-392-0202";

[user signUpInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
  if (!error) {
    // Hooray! Let them use the app now.
  } else {
    // Something went wrong
    NSString *errorString = [error userInfo][@"error"];
  }
}];
火力基地
[[FIRAuth auth] createUserWithEmail:@"email@example.com"
                           password:@"my pass"
                         completion:^(FIRUser *_Nullable user, NSError *_Nullable error) {
  if (!error) {
    FIRDatabaseReference *ref = [[FIRDatabase database] reference];
    [[[[ref child:@"users"] child:user.uid] child:@"phone"] setValue:@"415-392-0202"
  } else {
    // Something went wrong
    NSString *errorString = [error userInfo][@"error"];
  }
}];

建議的遷移策略

遷移帳戶

要將用戶帳戶從 Parse 遷移到 Firebase,請將您的用戶數據庫導出為 JSON 或 CSV 文件,然後使用 Firebase CLI 的auth:import命令將該文件導入您的 Firebase 項目。

首先,從 Parse 控制台或自託管數據庫導出用戶數據庫。例如,從 Parse 控制台導出的 JSON 文件可能如下所示:

{ // Username/password user
  "bcryptPassword": "$2a$10$OBp2hxB7TaYZgKyTiY48luawlTuYAU6BqzxJfpHoJMdZmjaF4HFh6",
  "email": "user@example.com",
  "username": "testuser",
  "objectId": "abcde1234",
  ...
},
{ // Facebook user
  "authData": {
    "facebook": {
      "access_token": "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
      "expiration_date": "2017-01-02T03:04:05.006Z",
      "id": "1000000000"
    }
  },
  "username": "wXyZ987654321StUv",
  "objectId": "fghij5678",
  ...
}

然後,將導出的文件轉換為 Firebase CLI 所需的格式。使用 Parse 用戶的objectId作為 Firebase 用戶的localId 。此外,base64 對 Parse 中的bcryptPassword值進行編碼,並在passwordHash字段中使用它們。例如:

{
  "users": [
    {
      "localId": "abcde1234",  // Parse objectId
      "email": "user@example.com",
      "displayName": "testuser",
      "passwordHash": "JDJhJDEwJE9CcDJoeEI3VGFZWmdLeVRpWTQ4bHVhd2xUdVlBVTZCcXp4SmZwSG9KTWRabWphRjRIRmg2",
    },
    {
      "localId": "fghij5678",  // Parse objectId
      "displayName": "wXyZ987654321StUv",
      "providerUserInfo": [
        {
          "providerId": "facebook.com",
          "rawId": "1000000000",  // Facebook ID
        }
      ]
    }
  ]
}

最後,使用 Firebase CLI 導入轉換後的文件,指定 bcrypt 作為哈希算法:

firebase auth:import account_file.json --hash-algo=BCRYPT

遷移用戶數據

如果您要為用戶存儲其他數據,則可以使用數據遷移部分中描述的策略將其遷移到 Firebase 實時數據庫。如果您使用帳戶遷移部分中描述的流程遷移帳戶,則您的 Firebase 帳戶具有與 Parse 帳戶相同的 ID,從而使您可以輕鬆遷移和重現由用戶 ID 鍵入的任何關係。

Firebase 雲消息傳遞

Firebase 雲消息傳遞 (FCM) 是一種跨平台消息傳遞解決方案,可讓您免費可靠地傳遞消息和通知。 Notifications composer 是基於 Firebase Cloud Messaging 構建的免費服務,可為移動應用程序開發人員提供有針對性的用戶通知。

請參閱Firebase 雲消息傳遞文檔以了解更多信息。

解析推送通知的區別

安裝在註冊通知的設備上的每個 Parse 應用程序都有一個關聯的Installation對象,您可以在其中存儲目標通知所需的所有數據。 InstallationPFUser的子類,這意味著您可以將所需的任何其他數據添加到Installation實例中。

通知編輯器根據應用、應用版本和設備語言等信息提供預定義的用戶細分。您可以使用 Google Analytics 事件和屬性構建更複雜的用戶細分來構建受眾。請參閱受眾幫助指南以了解更多信息。這些定位信息在 Firebase 實時數據庫中不可見。

建議的遷移策略

遷移設備令牌

Parse 使用 APNs 設備令牌來定位通知的安裝,而 FCM 使用映射到 APNs 設備令牌的 FCM 註冊令牌。只需將 FCM SDK 添加到您的 Apple 應用程序,它就會自動獲取 FCM 令牌

將頻道遷移到 FCM 主題

如果您使用 Parse 通道發送通知,則可以遷移到提供相同發布者-訂閱者模型的 FCM 主題。要處理從 Parse 到 FCM 的轉換,您可以編寫一個新版本的應用程序,使用 Parse SDK 取消訂閱 Parse 頻道,並使用 FCM SDK 訂閱相應的 FCM 主題。

例如,如果您的用戶訂閱了“巨人”主題,您將執行以下操作:

PFInstallation *currentInstallation = [PFInstallation currentInstallation];
[currentInstallation removeObject:@"Giants" forKey:@"channels"];
[currentInstallation saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
  if (succedeed) {
    [[FIRMessaging messaging] subscribeToTopic:@"/topics/Giants"];
  } else {
    // Something went wrong unsubscribing
  }
}];

使用此策略,您可以向 Parse 通道和相應的 FCM 主題發送消息,支持新舊版本的用戶。當足夠多的用戶從應用程序的僅解析版本遷移時,您可以取消該版本並開始僅使用 FCM 發送。

請參閱FCM 主題文檔以了解更多信息。

Firebase 遠程配置

Firebase 遠程配置是一項云服務,可讓您更改應用的行為和外觀,而無需用戶下載應用更新。使用遠程配置時,您可以創建控制應用行為和外觀的應用內默認值。然後,您可以稍後使用 Firebase 控制台為所有應用用戶或您的用戶群部分覆蓋應用內默認值。

如果您想測試不同的解決方案並能夠動態地將更多客戶端轉移到不同的提供商,Firebase 遠程配置在您的遷移過程中非常有用。例如,如果您的應用版本同時使用 Firebase 和 Parse 來獲取數據,則可以使用隨機百分位規則來確定哪些客戶端從 Firebase 讀取,並逐漸增加百分比。

要了解有關 Firebase 遠程配置的更多信息,請參閱遠程配置介紹

與 Parse Config 的區別

使用 Parse config,您可以在 Parse Config Dashboard 上將鍵/值對添加到您的應用程序,然後在客戶端上獲取PFConfig 。您獲得的每個PFConfig實例始終是不可變的。當您將來從網絡中檢索新的PFConfig時,它不會修改任何現有的PFConfig實例,而是會創建一個新實例並通過currentConfig使其可用。

使用 Firebase 遠程配置,您可以為鍵/值對創建應用內默認值,您可以從 Firebase 控制台覆蓋這些默認值,並且您可以使用規則和條件為您的用戶群的不同部分提供不同的應用用戶體驗。 Firebase 遠程配置實現了一個單例類,使您的應用可以使用鍵/值對。最初,單例返回您在應用程序中定義的默認值。您可以隨時從服務器獲取一組新的值,以便您的應用程序方便;成功獲取新集合後,您可以選擇何時激活它以使新值可用於應用程序。

建議的遷移策略

您可以通過將 Parse 配置的鍵/值對複製到 Firebase 控制台,然後部署使用 Firebase 遠程配置的新版本應用程序來遷移到 Firebase 遠程配置。

如果您想同時使用 Parse Config 和 Firebase 遠程配置進行試驗,您可以部署使用這兩種 SDK 的新版本應用,直到有足夠多的用戶從僅 Parse 版本遷移。

代碼比較

解析

[PFConfig getConfigInBackgroundWithBlock:^(PFConfig *config, NSError *error) {
  if (!error) {
    NSLog(@"Yay! Config was fetched from the server.");
  } else {
    NSLog(@"Failed to fetch. Using Cached Config.");
    config = [PFConfig currentConfig];
  }

  NSString *welcomeMessage = config[@"welcomeMessage"];
  if (!welcomeMessage) {
    NSLog(@"Falling back to default message.");
    welcomeMessage = @"Welcome!";
  }
}];

火力基地

FIRRemoteConfig remoteConfig = [FIRRemoteConfig remoteConfig];
// Set defaults from a plist file
[remoteConfig setDefaultsFromPlistFileName:@"RemoteConfigDefaults"];

[remoteConfig fetchWithCompletionHandler:^(FIRRemoteConfigFetchStatus status, NSError *error) {
  if (status == FIRRemoteConfigFetchStatusSuccess) {
    NSLog(@"Yay! Config was fetched from the server.");
    // Once the config is successfully fetched it must be activated before newly fetched
    // values are returned.
    [self.remoteConfig activateFetched];
  } else {
    NSLog(@"Failed to fetch. Using last fetched or default.");
  }
}];

// ...

// When this is called, the value of the latest fetched and activated config is returned;
// if there's none, the default value is returned.
NSString welcomeMessage = remoteConfig[@"welcomeMessage"].stringValue;