将您的 Parse iOS 应用迁移到 Firebase

如果您是一位 Parse 用户,正在寻找一个“后端即服务”解决方案来替代现在的解决方案,Firebase 可能是您的 iOS 应用的理想选择。

本指南介绍如何将特定服务集成到您的应用中。如需查看 Firebase 的基本设置说明,请参阅 iOS+ 设置指南。

Google Analytics

Google Analytics 是一款免费的应用衡量解决方案,可提供关于应用使用情况和用户互动度的数据分析。Google Analytics 与各种 Firebase 功能集成,可以就多达 500 种不同事件(您可以利用 Firebase SDK 定义这些事件)无限制地向您提供报告。

如需了解详情,请参阅 Google Analytics 文档

建议的迁移策略

使用不同的分析提供程序是非常普遍的现象,Google Analytics 能够轻松适应这种情况。只需将 Analytics 添加至您的应用,它即会自动收集事件和用户属性(如首次打开、应用更新、设备型号、年龄等),让您从中受益。

对于自定义的事件和用户属性,您可采取双写策略,同时使用 Parse Analytics 和 Google Analytics 记录事件和属性,进而逐步过渡到新解决方案。

代码比较

Parse 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];

Google Analytics

// 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 Realtime Database

Firebase Realtime Database 是一种 NoSQL 云端托管数据库。数据以 JSON 格式存储并实时同步到所连接的每个客户端。

如需了解详情,请参阅 Firebase Realtime Database 文档

与 Parse 数据的差异

对象

在 Parse 中,您存储的是一个 PFObject 或其子类,其中包含 JSON 兼容数据的键值对。数据是无架构的,这意味着您无需指定在每个 PFObject 上存在什么键。

所有 Firebase Realtime Database 数据均被存储为 JSON 对象,没有与 PFObject 对应的形式;您只需将数据写入与可用 JSON 类型对应的不同类型的 JSON 树值。

以下示例说明如何保存一款游戏的最高得分。

Parse
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
  }
}];
Firebase
// 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 Realtime Database 中,最好使用将数据拆分至不同路径的平展型数据结构来表现数据关系,以便通过不同的调用高效地下载数据。

以下示例说明如何组织博客应用中博文与其作者之间的关系:

Parse
// 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];
Firebase
// 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"
    }
    ...
  }
}
如需了解详情,请参阅设计数据库的数据结构指南。

读取数据

在 Parse 中,您可使用特定 Parse 对象的 ID 来读取数据,或使用 PFQuery 执行查询来读取数据。

在 Firebase 中,您可将异步侦听器附加到数据库引用,以此检索数据。该侦听器会针对数据的初始状态触发一次,以后只要数据有更改就会再次触发,因此您无需添加任何代码即可判断数据是否发生变化。

以下示例说明如何根据“对象”部分介绍的示例来检索某个游戏玩家的分数:

Parse
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]);
  }
}];
Firebase
// 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 Realtime Database 经过优化,可在所有连接的客户端中以毫秒级速度同步数据,其最终数据结构与 Parse 核心数据有很大不同。这意味着,迁移的第一步是考虑您的数据需要进行哪些更改,其中包括:

  • 您的 Parse 对象应如何映射到 Firebase 数据
  • 如果您的数据有父子关系,如何将数据拆分到不同路径,以便通过不同调用高效地下载。

迁移数据

在决定如何在 Firebase 中组织数据之后,您需要计划如何应对您的应用需要向两种数据库写入数据的过渡期。您可以选择以下方案:

后台同步

在此方案中,您有两个应用版本:一个是使用 Parse 的旧版本,另一个是使用 Firebase 的新版本。然后,通过 Parse Cloud Code 处理这两种数据库之间的同步(Parse 到 Firebase),同时,您的代码将在 Firebase 上侦听变化,并与 Parse 同步这些变化。在开始使用新版本之前,您必须:

  • 将现有的 Parse 数据转换为新的 Firebase 结构,并将其写入 Firebase Realtime Database。
  • 编写 Parse Cloud Code 函数,以便使用 Firebase REST API 将旧版客户端在 Parse 数据中产生的变化写入 Firebase Realtime Database。
  • 编写和部署代码,以便侦听 Firebase 上的变化并将其同步到 Parse 数据库。

此方案能确保完全隔离新旧代码,避免让客户端变得复杂。此方案的两大挑战是:处理初始导出的庞大数据集以及保证双向同步不会引发无限递归。

双写

在此方案中,您需要编写一个同时使用 Firebase 和 Parse 的新版应用,使用 Parse Cloud Code 将旧版客户端产生的变化从 Parse 数据同步到 Firebase Realtime Database。当有足够多的用户从 Parse 专用版应用迁移后,您就可以从双写版本中移除 Parse 代码。

此方案不需要任何服务器端代码,但是它的缺点是,未访问的数据不会迁移,并且您的应用大小会因使用两种 SDK 而增大。

Firebase 身份验证

Firebase 身份验证可使用密码和深受欢迎的联合用户身份提供方(如 Google、Facebook 和 Twitter)对用户进行身份验证。它还提供了界面代码库,让您在跨所有平台实现和维护应用的全面身份验证体验方面节省相当可观的必要投资。

请参阅 Firebase 身份验证文档了解详情。

与 Parse 身份验证的差异

Parse 提供一个称为 PFUser 的专用用户类,可自动处理用户账号管理所需的功能。PFUserPFObject 的一个子类,这意味着,用户数据包含在 Parse 数据中,并可像任何其他 PFObject 一样通过额外的字段进行扩展。

FIRUser 具有一组固定的基本属性,即唯一 ID、主电子邮件地址、用户名和照片网址。这些属性存储在单独的项目的用户数据库中,并可由用户更新。您无法向 FIRUser 对象直接添加其他属性,但可以将更多属性存储在您的 Firebase Realtime Database 数据库中。

以下示例说明如何注册一个用户并额外添加一个电话号码字段。

Parse
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"];
  }
}];
Firebase
[[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。同时,对来自 Parse 的 bcryptPassword 值进行 base64 编码,并将其用在 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 Realtime Database。如果您按照账号迁移部分所述的流程迁移账号,那么您的 Firebase 账号会使用与 Parse 账号相同的 ID,方便您轻松迁移和复制由用户 ID 键控的任何关系。

Firebase Cloud Messaging

Firebase Cloud Messaging (FCM) 是一款跨平台消息传递解决方案,让您可以免费且可靠地传递消息和通知。Notifications Composer 是一项在 Firebase Cloud Messaging 的基础上构建的免费服务,可帮助移动应用开发者发送有针对性的用户通知。

如需了解详情,请参阅 Firebase Cloud Messaging 文档

与 Parse 推送通知的区别

在设备上安装并注册收取通知的每个 Parse 应用都有一个关联的 Installation 对象,您可在其中存储定位通知所需要的所有数据。InstallationPFUser 的一个子类,这意味着您可以向 Installation 实例添加所需的任何其他数据。

Notifications Composer 可根据应用、应用版本、设备语言等信息进行预定义的用户细分。您可以使用 Google Analytics 事件和属性构建更复杂的用户细分,以创建受众群体。如需了解详情,请参阅受众群体帮助指南。这些定位信息不会出现在 Firebase Realtime Database 中。

建议的迁移策略

迁移设备令牌

Parse 使用 APNs 设备令牌来定位那些安装了应用并要求收取通知的用户,而 FCM 则使用映射至 APNs 设备令牌的 FCM 注册令牌来实现这一点。您只需将 FCM SDK 添加至您的 Apple 应用,后者就会自动提取 FCM 令牌

将渠道迁移至 FCM 主题

如果您在使用 Parse 渠道来发送通知,则可迁移至 FCM 主题,后者可以提供同样的“发布者-订阅者”模式。要处理从 Parse 向 FCM 的过渡,您可编写一个新版本的应用,让新应用使用 Parse SDK 退订 Parse 渠道并使用 FCM SDK 订阅对应的 FCM 主题。

例如,如果您的用户订阅了“Giants”主题,您可执行如下所示的操作:

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

通过此策略,您可以向 Parse 渠道和相应的 FCM 主题发送消息,同时支持使用新版和旧版应用的用户。当有足够多的用户从 Parse 专用版应用迁移后,您就可以弃用该版本,开始只使用 FCM 发送消息。

如需了解详情,请参阅 FCM 主题文档

Firebase Remote Config

Firebase Remote Config 是一种云服务,让您可以更改应用的行为和外观,而无需用户下载应用更新。使用 Remote Config 时,您可以创建应用内默认值以控制应用的行为和外观。之后,您便可以使用 Firebase 控制台为所有应用用户或用户群细分替换应用内默认值。

如果您要在迁移过程中测试不同的解决方案,并希望能够将更多客户端动态转移到不同的服务提供方,Firebase Remote Config 就可以派上大用场。例如,如果您的某个应用版本同时使用了 Firebase 和 Parse 来处理数据,则您可以使用随机百分位规则来确定哪些客户端从 Firebase 读取数据,并逐渐扩大相应比例。

如需详细了解 Firebase Remote Config 功能,请参阅 Remote Config 简介

与 Parse 配置的差异

通过 Parse 配置,您可以在 Parse 配置信息中心向您的应用添加键值对,然后在客户端提取 PFConfig。您得到的每个 PFConfig 实例始终是不可变的。您将来从网络检索到新 PFConfig 时,它不会修改任何现有 PFConfig 实例,但会创建一个新实例并通过 currentConfig 提供该实例。

而使用 Firebase Remote Config 时,您会为键值对创建应用内默认值,并可从 Firebase 控制台重写这些默认值,这样一来,您可使用规则和条件向不同的细分用户群提供不同的应用用户体验。Firebase Remote Config 通过实现一个单例类,在您的应用中提供相应键值对。最初,单例类会返回您在应用内定义的默认值。之后,您可以随时在适当的时候为您的应用从服务器提取一组新值,成功提取该组新值后,您就可以选择在何时激活并向应用提供这些新值。

建议的迁移策略

迁移到 Firebase Remote Config 的方法是,将您的 Parse 配置的键值对复制到 Firebase 控制台,然后部署一个使用 Firebase Remote Config 的新版应用。

如果您想试验同时使用 Parse 配置和 Firebase Remote Config,可部署一个使用两种 SDK 的新版应用,直至有足够多的用户从 Parse 专用版本迁移为止。

代码比较

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!";
  }
}];

Firebase

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;