将您的 Parse iOS 应用迁移到 Firebase

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

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

Google Analytics(分析)

Google Analytics(分析)是一款免费的应用衡量解决方案,可提供关于应用使用情况和用户互动度的数据分析。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 实时数据库

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

请参阅 Firebase 实时数据库文档了解详情。

与 Parse 数据的差异

对象

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

所有 Firebase 实时数据库数据均被存储为 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
  }
}];
如需了解更多详情,请参阅在 iOS 上读取和写入数据指南。

数据之间的关系

一个 PFObject 可与另一个 PFObject 建立关系,即任何对象都可使用其他对象作为值。

在 Firebase 实时数据库中,最好使用将数据拆分至不同路径的平展型数据结构来表现数据关系,以便通过不同的调用高效地下载数据。

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

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);
}];
如需详细了解可用的事件侦听器类型及如何对数据排序和过滤,请参阅在 iOS 上读取和写入数据指南。

建议的迁移策略

重新评估数据

Firebase 实时数据库经过优化,可针对所有连接的客户端以毫秒级速度同步数据,其最终数据结构与 Parse 核心数据有很大不同。这意味着,迁移的第一步是考虑您的数据需要进行哪些更改,其中包括:

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

迁移数据

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

后台同步

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

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

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

双写

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

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

Firebase 身份验证

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

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

与 Parse 身份验证的差异

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

FIRUser 拥有一组固定的基本属性(唯一身份 ID、主电子邮件地址、名称和照片网址),这些属性存储在一个单独的项目的用户数据库中,用户可以更新这些属性。您无法向 FIRUser 对象直接添加其他属性,但可以在您的 Firebase 实时数据库中存储额外的属性。

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

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 实时数据库。如果您按照帐号迁移部分所述的流程迁移帐号,那么您的 Firebase 帐号会使用与 Parse 帐号相同的 ID,方便您轻松迁移和复制由用户 ID 键控的任何关系。

Firebase 云消息传递

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

如需了解详情,请参阅 Firebase 云消息传递文档

与 Parse 推送通知的区别

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

通知编辑器根据应用、应用版本、设备语言等信息提供预定义的用户细分。您可以使用 Google Analytics(分析)事件和属性构建更复杂的用户细分,从而创建受众群体。如需了解详情,请参阅受众群体帮助指南。这些定位信息不会出现在 Firebase 实时数据库中。

建议的迁移策略

迁移设备令牌

Parse 使用 APNs 设备令牌来定位那些安装了应用并要求收取通知的用户,而 FCM 则使用映射至 APNs 设备令牌的 FCM 注册令牌来实现这一点。您只需将 FCM SDK 添加至您的 iOS 应用,后者就会自动提取 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 (succedeed) {
    [[FIRMessaging messaging] subscribeToTopic:@"/topics/Giants"];
  } else {
    // Something went wrong unsubscribing
  }
}];

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

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

Firebase 远程配置

Firebase 远程配置是一种云服务,让您可以更改应用的行为和外观,而无需用户下载应用更新。使用远程配置时,您可以创建应用内默认值,从而实现对应用行为和外观的控制。之后,您便可以使用 Firebase 控制台为所有用户或细分用户群重写应用内默认值。

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

如需详细了解 Firebase 远程配置功能,请参阅远程配置简介

与 Parse 配置的差异

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

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

建议的迁移策略

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

如果您想对 Parse 配置和 Firebase 远程配置进行实验,可部署一个使用两种 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;