备注: 本教程已由 Attila Hegedüs 更新适配 iOS 10 和 Swift 3,原教程由David East 创作。
原文:https://www.raywenderlich.com/139322/firebase-tutorial-getting-started-2
翻译:JoeyChang 转载请标明出处
Firebase 是一个移动后台服务,它可以帮助我们创建具有优秀特性的移动 apps。Firebase 提供以下三个主要服务: a realtime database, user authentication and hosting。通过集成 Firebase iOS SDK, 你几乎不用写一行代码就能创建出非常棒的应用。
Firebase 具有数据库实时性这样的独特性能。
你曾经使用过 pull-to-refresh 去拉新数据么?有了 Firebase,现在你可以忽略那种刷新数据方法了。
当 Firebase 数据库更新时,所有的连接者可以实时获取更新,这就意味着你的 app 可以不用用户交互就能获取数据库当前最新值。
本篇 Firebase 教程中,我们将通过创建一个名叫 Grocr 的具有协作性的grocery list app , 来学习Firebase 的一些基本原理。当我们添加一个项目到列表时,它将实时出现在用户的其它设备中,但是我们并不满足于此,我们还将调整 Grocr 让它可以离线工作,以致即使仅有一个 grocery 数据连接,列表也能保持同步。
通过本文,你将学习到以下技能:
- 保存数据到Firebase数据库
- 从 Firebase 实时同步数据
- 验证 users
- 在线监控 users
- 实现离线支持
开始,下载初始项目 Grocr-starter. 它使用 CocoaPods 管理 Firebase 。
在 Xcode 中打开 Grocr.xcworkspace,该项目包含三个view controllers:
-
LoginViewController.swift.
现在登录功能还是使用的硬编码 user credentials,稍后我们将优化它。 -
GroceryListTableViewController.swift.
这个 controller 是 UITableViewController 子类,它通过 UIAlertController 添加 items 到本地数据库的 list 表格。 -
OnlineUsersTableViewController.swift.
该 controller 使用 Firebase’s presence feature 展示所有当前在线 users。
此外,还有两个模型类 GroceryItem.swift 和 User.swift 。它们做为 app 的数据模型。
Build and run, 你将看到如下这样效果:
Grocr-Starter注: 当 build 工程时,我们将看到一些 ‘nullability’ 编译警告。它们来自Firebase,暂时我们先忽略它们,稍后解决。
我们可以点击 Login 进行登录,这将使用一个写死的 user 数据,现在该 app 还只能使用本地数据。接下来我们将调用 Firebase 数据使 app 生动起来。
创建 Firebase 账号
有两个重要步骤:
- 创建免费 Firebase 账号
- 获取你第一个 app 的 URL
我们可以访问 Getting Started page 进行注册。当我们使用我们谷歌账号共享登录进入 firebase, 我们将看到一个干净的 Firebase 控制台。不要担心费用问题,现在 Firebase 免费版本已经足够强大,够用了。
01-firebase-welcome创建我们的第一个工程,点击 CREATE NEW PROJECT 。在弹出的对话框中输入项目名称以及你的首选 国家/地区: 02-firebase-create-project
点击 CREATE PROJECT, 我们就可以通过控制面板来管理我们的项目了。
03-firebase-dashboard这将作为所有 Firebase 服务的容器,我们用它存储数据和授权用户。
选择 Add Firebase to your iOS app 开始我们的项目。本项目的 bundle ID 是 rw.firebase.gettingstarted,所以添加此 id 到 iOS bundle ID 文本框。
点击 ADD APP ,将下载一个 GoogleService-Info.plist 文件。将该文件拖拽到 Xcode 中的 Grocr 项目。
04-firebase-add-ios-app-2点击 CONTINUE. 接下来一页描述怎样安装 Firebase SDK。
04-firebase-add-ios-app-3本项目已经替我们集成好了,所以点击 CONTINUE 继续。最后一页说明当 app 启动时怎样连接到 Firebase。
04-firebase-add-ios-app-4点击 FINISH ,查看新项目细节。
04-firebase-add-ios-app-5在 Xcode 打开 GroceryListTableViewController.swift ,添加如下代码,创建 Firebase 连接。
let ref = FIRDatabase.database().reference(withPath: "grocery-items")
这个 Firebase 连接使用已提供的 path。在 documentation 中,这些 Firebase 属性被称为 references ,它们指定 Firebase 的位置。
简言之,这些属性可以实现保存和同步数据到给定的位置。
我们发现,base URL 不是必须的,相反,它使用 grocery-items 的 child path。Firebase 数据库是 JSON NoSQL 数据库,所以数据都是保存为 JSON 格式。
JSON 是分等级的 key-value 数据结构 -- keys 指的是可以根据它获取其它对象格式的 values 值。JSON data 是一个简单的 key value 对儿树形结构。
在 Firebase 中,key 是一个 URL,value是形如 number, string, boolean , object 的随意的数据。
Structuring Data
无论客户端是什么数据格式,保存到 Firebase 的是 JSON 格式。下面是一个 JSON 示例:
// The root of the tree
{ // grocery-items
"grocery-items": {
// grocery-items/milk
"milk": {
// grocery-items/milk/name
"name": "Milk",
// grocery-items/milk/addedByUser
"addedByUser": "David"
},
"pizza": {
"name": "Pizza",
"addedByUser": "Alice"
},
}
}
在上面的 JSON 中,你可以看到每对儿数据都是以键值对儿形式出现的。我们可以继续遍历树并在更深的位置检索数据。
在上面的例子中,我们可以通过路径检索所有的 grocery item。
grocery-items
如果你想获取第一个 grocery item ,你可以通过以下路径获取:
grocery-items/milk
因为所有的 Firebase keys 对应paths,所以 key 的名字选择很重要。
Understanding Firebase References
一个基本的原则是,Firebase 引用指向 Firebase 中数据存储的位置。如果我们创建多引用,那么这些引用共享同一个连接。
看如下代码:
// 1
let rootRef = FIRDatabase.database().reference()
// 2
let childRef = FIRDatabase.database().reference(withPath: "grocery-items")
// 3
let itemsRef = rootRef.child("grocery-items")
// 4
let milkRef = itemsRef.child("milk")
// 5
print(rootRef.key) // prints: ""
print(childRef.key) // prints: "grocery-items"
print(itemsRef.key) // prints: "grocery-items"
print(milkRef.key) // prints: "milk"
下面我们解释下:
- 我们创建一个到 Firebase 数据库 root 引用。
- 使用一个 URL ,我们可以创建一个引用到 Firebase 数据库的子路径。
- 通过给 rootRef 传递子路径,我们可以使用 child(_:) 创建子引用,这个引用和上面的引用是一样意思。
- 使用 itemsRef ,我们可以创建到 milk 的子引用。
- 每个引用都有 key 属性。这个属性和 Firebase 数据库关键字的名字一样。
我们不需要在同一个项目中都添加这样的代码,这里只是出于展示目的进行列举。
Adding New Items to the List
在 GroceryListTableViewController.swift 的底部,找到 addButtonDidTouch(_:) 方法。
在这里我们要实现通过 UIAlertController 的方式添加一个新的 item 。
在 saveAction 方法内,现在仅仅保存数据到一个本地 array,因此 saveAction 不能同步不同客户端的数据,而且在下次启动 app 时,保存的数据将丢失。
没有人会使用不能记录或者同步他们 grocery 清单数据的 app ! 让我们完善 saveAction 方法:
let saveAction = UIAlertAction(title: "Save",
style: .default) { _ in
// 1
guard let textField = alert.textFields?.first,
let text = textField.text else { return }
// 2
let groceryItem = GroceryItem(name: text,
addedByUser: self.user.email,
completed: false)
// 3
let groceryItemRef = self.ref.child(text.lowercased())
// 4
groceryItemRef.setValue(groceryItem.toAnyObject())
}
注释如下:
-
从 alert controller 获取 text field 和它的内容。
-
使用当前用户数据创建一个新的 GroceryItem 。
-
使用 child(_:) 创建一个子引用,这个引用的 key 是 item 的小写名称,因此如果我们添加一个复制的 item (即使使用大写字母,或者使用混合字母),数据库只保存最后一个。
-
使用 setValue(_:) 保存数据到数据库。这个方法期望一个字典格式。GroceryItem 有个 toAnyObject() 方法,可以转换对象为字典格式。
在你可以连接数据库之前,我们还需要配置它。找到 AppDelegate.swift ,并在 application(_:didFinishLaunchingWithOptions:) 返回 true 之前添加如下代码:
FIRApp.configure()
默认情况,Firebase 数据库需要用户授权读写权限。在浏览器进入 Firebase 控制面板,选中左边的 Database 选项,设置 RULES 如下:
firebase-db-rules{
"rules": {
".read": true,
".write": true
}
}
修改后,选择 PUBLISH 按钮进行保存设置。
Build and run. 在 Firebase 控制面板,选择 DATA 标签,并将浏览器窗口紧挨模拟器。当我们在模拟器中添加 item ,我们将看到它会出现在控制面板。
现在,我们就有了一个可以实时添加数据到 Firebase 的活生生的 grocery list app!但是虽然 key 特性已经可以运行完好了,但是没有数据添加到table view。
那么我们怎样才能将数据从数据库同步到 table view 呢?
Retrieving Data
我们可以通过 observeEventType(_:withBlock:) 方法异步检索 Firebase 中的数据。
在 GroceryListTableViewController.swift 的 viewDidLoad() 下添加如下方法:
ref.observe(.value, with: { snapshot in
print(snapshot.value)
})
该方法有两个参数:FIRDataEventType 的一个实例以及一个闭包。
event type 确定我们要监听的事件,.value 监听诸如 add, removed, changed 这样的 Firebase 数据库重点数据改变。
当改变发生,数据库使用最新数据更新 app 显示。
app 在闭包方法中通过接受到的 FIRDataSnapshot 一个实例获知数据改变。snapshot,代表某个特定时间点的数据快照。我们可以通过 value 那个属性获取到 snapshot 的数据。
Build and run,我们将看到,在控制台会有 items 列表数据被打印出来。
Optional({
pizza = {
addedByUser = "hungry@person.food";
completed = 0;
name = Pizza;
};
})
Synchronizing Data to the Table View
注意打印日志--现在在 table view 中可以看到 grocery 列表了。
在 GroceryListTableViewController.swift, 替换之前的代码片段为如下代码:
// 1
ref.observe(.value, with: { snapshot in
// 2
var newItems: [GroceryItem] = []
// 3
for item in snapshot.children {
// 4
let groceryItem = GroceryItem(snapshot: item as! FIRDataSnapshot)
newItems.append(groceryItem)
}
// 5
self.items = newItems
self.tableView.reloadData()
})
以上代码的诸行解释:
-
添加一个监听器监听 grocery-items 改变了什么。
-
存储最近一次版本数据到闭包中本地的一个变量中。
-
监听者闭包返回最近数据的一个 snapshot,这个 snapshot 包含所有的 grocery items,而不是仅仅包含改变的 items。使 snapshot.children ,我们可以循环获取 grocery items 。
-
GroceryItem 结构有一个常用的实例化器,它使用 FIRDataSnapshot
来填充它的属性。snapshot 的值可以为任意类型,可以是 dictionary, array, number, or string。当创建好一个 GroceryItem 实例,它被添加到一个包含最近一次版本数据的数组中。 -
将最新版本的数据赋值给 items,然后更新 table view,使它展示最新数据。
Build and run. 添加一个 pizza item 怎么样? 它将显示到 table view。
不用刷新,就可以及时获取到更新后的数据。 realtime-updates
Removing Items From the Table View
table view 将同步我们所有的改变数据, 但是当我们想删除 pizza 时,现在还不能更新。
为了通知数据库删除数据,我们需要设置一个 Firebase reference,当用户轻扫时候删除 item。
定位到 tableView(_:commit:forRowAt:)。现在,该方法使用 index 移除 array 中的 grocery item。这可以实现功能,但我们还有更好的解决方法。替换为如下实现方式:
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let groceryItem = items[indexPath.row]
groceryItem.ref?.removeValue()
}
}
Firebase 遵从单向数据流模型,因此 viewDidLoad() 的 listener 监听 grocery list 的最新数据。清除 item 触发数据改变。
index path 的 row 被用来获取相关的 grocery item。每个 GroceryItem 拥有一个名为 ref 的 Firebase reference property,调用 它的 removeValue() 将移除我们在 viewDidLoad() 定义的 listener。该listener有一个闭包,它使用最新的数据重新加载表视图。
Build and run. 轻扫 item ,点击删除,我们发现 app 和 Firebase 的数据都消失了。
fb-deleteNice work! 我们 items 可以实时删除了。
Checking Off Items
现在我们知道了怎么添加、删除以及同步 items ,这很酷。但是当我们实际购物时候会怎样呢?我们会删除我们刚购买的物品么,或者当我们添加购物车时给物品打个标记是否更好?
在以前的纸质时代,人们过去常常把东西从购物清单上划掉,因为我们也将在我们的 app 用现代的方式模仿这个行为。
grocery-list打开 GroceryListTableViewController.swift ,找到 toggleCellCheckbox(_:isCompleted:) 方法,该方法可以根据 item 是否完成来切换UITableViewCell 的必要视图属性。
当 table view 第一次加载后,刚方法在tableView (_:cellForRowAtIndexPath:) 中会被调用,以及当用户点击 cell 时也会被调用。
替换 tableView(_:didSelectRowAt:) 方法为如下:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// 1
guard let cell = tableView.cellForRow(at: indexPath) else { return }
// 2
let groceryItem = items[indexPath.row]
// 3
let toggledCompletion = !groceryItem.completed
// 4
toggleCellCheckbox(cell, isCompleted: toggledCompletion)
// 5
groceryItem.ref?.updateChildValues([
"completed": toggledCompletion
])
}
以下为详细注解:
- 使用 cellForRow(at:) 确定用户点击的 cell。
- 根据 index path 的 row 获取对应的 GroceryItem。
- 改变 grocery item 的 completed 的状态。
- 调用 toggleCellCheckbox(_:isCompleted:) 更新 cell 的属性。
- 在 updateChildValues(:) 方法中,通过传递字典参数,更新Firebase。该方法与 setValue(:) 不同,因为它只应用更新,而setValue(_:) 具有破坏性,并在该引用中替换整个值。
Build and run. 点击一个 item,我们就可以看到该行被勾号标记并排序。
fb-toggle恭喜,我们已经完成了一个相当漂亮的 grocery list app 。
Sorting the Grocery List
如果把 ice cream 放在未排序的标记里面,有时我们可能会忘记它。现在让我们进行些优化。
如果可以把已选中的 items 自动移动到列表底部,我们的 app 将更加令人喜欢。这样,未被标记的 items 可以更容易被我们发现。
使用 Firebase queries, 我们可以根据任意属性对列表进行排序,在GroceryListTableViewController.swift, 更新 viewDidLoad() 方法:
ref.queryOrdered(byChild: "completed").observe(.value, with: { snapshot in
var newItems: [GroceryItem] = []
for item in snapshot.children {
let groceryItem = GroceryItem(snapshot: item as! FIRDataSnapshot)
newItems.append(groceryItem)
}
self.items = newItems
self.tableView.reloadData()
})
通过关键词 “ completed”,使用 Firebase 引用 queryOrdered(byChild:) 对数据进行排序。
由于列表需要完成顺序,所以 completed 键将传递给查询。然后,queryOrdered(byChild:)返回一个引用,通知服务器以有序的方式返回数据。
Build and run. 点击一行,使其置换为已完成状态,我们将看到,它神奇地自动移动到了最后一行。
fb-order哇! 我们现在真的让购物变得更容易了。跨多个用户同步数据,似乎应该足够简单,例如,与一个重要的其他用户或 housemate。这听起来像…身份验证!
Authenticating Users
Firebase 有一个 authentication service,它允许 apps 验证不同的提供者,我们可以使用 Google, Twitter, Facebook, Github, email & password, 匿名, 甚至 custom backends 这些方式。这里我们使用邮箱和密码方式进行身份认证,因为这种方式是最简单的。
进入 Firebase dashboard ,点击 Auth,激活邮箱密码认证。
fb-auth-1选中 SIGN-IN METHOD 标签栏,再在 Sign-in providers 那一节选中Email/Password 行,切换 Enable 并点击 SAVE:
authenticationFirebase 存储账户信息到 keychain,因此最后一步,在项目中,切换到 target’s Capabilities 打开 Keychain Sharing 开关。
keychain-sharing现在,我们已经可以使用邮箱和密码进行身份认证了。
Registering Users
在 LoginViewController.swift,找到 signUpDidTouch(_:) 方法,这里会弹出 UIAlertController 让用户注册账号,定位到 saveAction 方法,添加以下代码到方法块儿。
// 1
let emailField = alert.textFields![0]
let passwordField = alert.textFields![1]
// 2
FIRAuth.auth()!.createUser(withEmail: emailField.text!,
password: passwordField.text!) { user, error in
if error == nil {
// 3
FIRAuth.auth()!.signIn(withEmail: self.textFieldLoginEmail.text!,
password: self.textFieldLoginPassword.text!)
}
}
以上代码解释:
- 从弹框中获取邮箱和密码。
- 调用 Firebase 方法 createUser(withEmail:password:),传递邮箱和密码给它。
- 如果执行没有错误,用户账号即被创建。但是,我们还要再进行一下登录操作 signIn(withEmail:password:) ,同样需要传递邮箱和密码。
Build and run. 点击 Sign up ,键入邮箱和密码,点击保存。现在 view controller 还不能在登录成功后导航到其它地方。我们刷新 Firebase Login & Auth ,我们将看到新建的用户。
fb-register-user喔!我们的 app 现在可以让用户注册并进行登录了,不过我们先不要庆祝,我们还需要再做些优化,好使用户更好的使用它。
Logging Users In
Sign up 按钮可以注册和登录,然而 Login 现在还什么都做不了,因为我们还没有给它绑定验证。
到 LoginViewController.swift, 找到 loginDidTouch(_:) 方法,修改如下:
@IBAction func loginDidTouch(_ sender: AnyObject) {
FIRAuth.auth()!.signIn(withEmail: textFieldLoginEmail.text!,
password: textFieldLoginPassword.text!)
}
当用户点击 Login 时,这些代码将验证用户信息。
我们接下来需要在用户登录成功后导航到下一个页面。
Observing Authentication State
Firebase 有可以监控用户验证状态的观察者。这里是添加 segue 最好的地方。在 LoginViewController: 添加如下代码:
override func viewDidLoad() {
super.viewDidLoad()
// 1
FIRAuth.auth()!.addStateDidChangeListener() { auth, user in
// 2
if user != nil {
// 3
self.performSegue(withIdentifier: self.loginToList, sender: nil)
}
}
}
注释如下:
-
使用 addStateDidChangeListener(_:) 创建验证观察者。该 block 被传入两个参数:auth 和 user。
-
测试 user 的值,如果验证通过,返回用户信息,如果验证失败,返回 nil 。
-
验证成功,进行页面跳转。传输 sender 为 nil 。这看起来有些奇怪,但是稍后我们将在 GroceryListTableViewController.swift 进行设置。
Setting the User in the Grocery List
在 GroceryListTableViewController.swift 文件 viewDidLoad(): 方法底部添加如下代码:
FIRAuth.auth()!.addStateDidChangeListener { auth, user in
guard let user = user else { return }
self.user = User(authData: user)
}
这里我们添加了一个 Firebase auth object 的验证观察者,当用户成功登录时,依次分配用户属性。
Build and run. 如果用户已经登录,app 将跳过 LoginViewController 直接导航到 GroceryListTableViewController. 当用户添加 items ,他们的 email 将显示到 cell 的详情里面。
fb-user-addSuccess! app 现在已经有了基本的用户验证功能。
Monitoring Users’ Online Status
现在既然我们的 app 已经拥有了用户验证功能,那是时候添加监控哪个用户在线功能了。打开 GroceryListTableViewController.swift ,添加如下 property:
let usersRef = FIRDatabase.database().reference(withPath: "online")
这是一个指向存储在线用户列表的在线位置的Firebase引用。
下一步,在 viewDidLoad() 方法下添加如下代码到 addStateDidChangeListener(_:) 闭包的下面。
// 1
let currentUserRef = self.usersRef.child(self.user.uid)
// 2
currentUserRef.setValue(self.user.email)
// 3
currentUserRef.onDisconnectRemoveValue()
注释如下:
- 使用用户的 uid 创建一个 child 引用,当 Firebase 创建一个账号时,这个引用会被生成。
- 使用这个引用保存当前用户的 email.
- 当 Firebase 连接关闭的时候,例如用户退出 app , 调用 currentUserRef 的 onDisconnectRemoveValue(),删除位置引用的值。这可以完美监控离线用户。
Build and run. 当 view 加载时,当前用户的电子邮件,会被添加在当前在线位置的一个子节点。
fb-monitoringGreat! 现在当用户数量增加时,是时候改变 bar button item 的个数了。
Updating the Online User Count
仍然在 GroceryListTableViewController.swift 的 viewDidLoad() 方法下添加如下代码:
usersRef.observe(.value, with: { snapshot in
if snapshot.exists() {
self.userCountBarButtonItem?.title = snapshot.childrenCount.description
} else {
self.userCountBarButtonItem?.title = "0"
}
})
这创建一个观察者监控在线用户,当用户在线或者离线,userCountBarButtonItem 的 title 随之更新。
Displaying a List of Online Users
打开 OnlineUsersTableViewController.swift,在 class 的 property section 添加一个本地引用到 Firebase 的在线用户记录。
let usersRef = FIRDatabase.database().reference(withPath: "online")
然后,在viewDidLoad(), 替换代码
currentUsers.append("hungry@person.food")
为如下:
// 1
usersRef.observe(.childAdded, with: { snap in
// 2
guard let email = snap.value as? String else { return }
self.currentUsers.append(email)
// 3
let row = self.currentUsers.count - 1
// 4
let indexPath = IndexPath(row: row, section: 0)
// 5
self.tableView.insertRows(at: [indexPath], with: .top)
})
代码注释如下:
-
创建一个 children added 监听器,添加到被 usersRef 管理的位置。这与值侦听器不同,因为只有添加的 child 被传递到闭包。
-
从 snapshot 获取值,并赋值给本地变量 array。
-
因为 table view 的坐标从 0 开始计算,当前的 row 总是等于 array 的个数 -1。
-
使用当前 row index 创建一个 NSIndexPath.
-
使用动画从顶部添加一行到 table view.
这将只渲染添加的条目,而不是重新加载整个列表,而且还可以指定一个漂亮的动画。:]
由于用户可以脱机,table 需要对被删除的用户做出反应。在我们刚刚添加的代码下面添加以下内容:
usersRef.observe(.childRemoved, with: { snap in
guard let emailToFind = snap.value as? String else { return }
for (index, email) in self.currentUsers.enumerated() {
if email == emailToFind {
let indexPath = IndexPath(row: index, section: 0)
self.currentUsers.remove(at: index)
self.tableView.deleteRows(at: [indexPath], with: .fade)
}
}
})
这只是添加了一个观察者,它侦听被删除的 usersRef 引用的子元素。它在本地数组中搜索电子邮件的值,以找到相应的子条目,一旦找到,它就从表中删除相关的行。
Build and run.
在 Firebase 用户仪表板上点击 Online ,当前用户的电子邮件将出现在表格中。使用一些技巧,可以在网上添加一个用户,一旦你做了,它就会显示在列表中。在仪表板上单击删除按钮,用户就会从 table 中消失….
fb-users-tableBooyah! 当用户被添加和删除的时候,table 随之更新了。
monintoring-usersEnabling Offline
杂货店因不稳定的数据连接而臭名昭著。你会认为他们现在都有了Wi-Fi,但是没有!
不过没关系,我们只需设置数据库离线工作。打开 * AppDelegate*,在(_:didFinishLaunchingWithOptions:) 底部方法返回 true 之前,添加如下代码:
FIRDatabase.database().persistenceEnabled = true
是的,就是这样! 就像我们的应用能够离线运行一样。当 app 重启,一旦建立网络连接,离线更新也将作用于我们的 Firebase 数据库。Oooh-ahhhh !
Where To Go From Here?
我们可以在这里下载 Grocr-final完整项目。
注意:下载完后,我们仍需要添加自己的 GoogleService-Info.plist 和 设置允许Keychain sharing 。
在这个Firebase教程中,我们通过构建一个协作的购物清单 app 了解了Firebase的基础知识,我们已经实现了将数据保存到一个 Firebase 数据库、实时同步数据、认证用户、监视在线用户状态以及实现了离线支持。所有这些都是在没有写一行服务器代码的情况下完成的! :]
如果你对 Firebase 感兴趣,请查看文档 documentation,以及 Firebase 提供的示例。
如果您对这个Firebase教程、Firebase或示例应用有任何意见或问题,请加入下面的论坛讨论!
上海 虹桥V1
2017.09.06 19:02
网友评论