(可选)使用 Firebase Local Emulator Suite 制作原型并进行测试
在讨论您的应用程序如何读取和写入实时数据库之前,让我们介绍一组可用于原型设计和测试实时数据库功能的工具:Firebase Local Emulator Suite。如果您正在尝试不同的数据模型、优化您的安全规则,或者努力寻找与后端交互的最具成本效益的方式,那么能够在不部署实时服务的情况下在本地工作可能是一个好主意。
Realtime Database 模拟器是 Local Emulator Suite 的一部分,它使您的应用程序能够与您的模拟数据库内容和配置以及可选的模拟项目资源(函数、其他数据库和安全规则)进行交互。
使用实时数据库模拟器只需几个步骤:
- 在您的应用程序的测试配置中添加一行代码以连接到模拟器。
- 从本地项目目录的根目录运行
firebase emulators:start
。 - 像往常一样使用实时数据库平台 SDK 或使用实时数据库 REST API 从应用程序的原型代码进行调用。
提供了涉及实时数据库和云函数的详细演练。您还应该看看Local Emulator Suite introduction 。
获取数据库引用
要从数据库读取或写入数据,您需要一个firebase.database.Reference
实例:
Web version 9
import { getDatabase } from "firebase/database"; const database = getDatabase();
Web version 8
var database = firebase.database();
写数据
本文档介绍了检索数据的基础知识以及如何排序和过滤 Firebase 数据。
通过将异步侦听器附加到 firebase.database.Reference 来检索firebase.database.Reference
数据。侦听器针对数据的初始状态触发一次,并在数据更改时再次触发。
基本写操作
对于基本的写操作,您可以使用set()
将数据保存到指定的引用,替换该路径中的任何现有数据。例如,社交博客应用程序可能会使用set()
添加用户,如下所示:
Web version 9
import { getDatabase, ref, set } from "firebase/database"; function writeUserData(userId, name, email, imageUrl) { const db = getDatabase(); set(ref(db, 'users/' + userId), { username: name, email: email, profile_picture : imageUrl }); }
Web version 8
function writeUserData(userId, name, email, imageUrl) { firebase.database().ref('users/' + userId).set({ username: name, email: email, profile_picture : imageUrl }); }
使用set()
覆盖指定位置的数据,包括任何子节点。
读取数据
监听值事件
要读取路径上的数据并监听变化,请使用onValue()
来观察事件。您可以使用此事件读取给定路径中内容的静态快照,因为它们在事件发生时存在。当监听器被附加时,这个方法被触发一次,每次数据(包括孩子)发生变化时再次触发。向事件回调传递一个快照,其中包含该位置的所有数据,包括子数据。如果没有数据,快照将在调用exists()
时返回false
,在调用val()
时返回null
。
以下示例演示了一个社交博客应用程序从数据库中检索帖子的星级计数:
Web version 9
import { getDatabase, ref, onValue} from "firebase/database"; const db = getDatabase(); const starCountRef = ref(db, 'posts/' + postId + '/starCount'); onValue(starCountRef, (snapshot) => { const data = snapshot.val(); updateStarCount(postElement, data); });
Web version 8
var starCountRef = firebase.database().ref('posts/' + postId + '/starCount'); starCountRef.on('value', (snapshot) => { const data = snapshot.val(); updateStarCount(postElement, data); });
侦听器收到一个snapshot
,其中包含事件发生时数据库中指定位置的数据。您可以使用val()
方法检索snapshot
中的数据。
一次读取数据
使用 get() 读取一次数据
SDK 旨在管理与数据库服务器的交互,无论您的应用程序是在线还是离线。
通常,您应该使用上述值事件技术来读取数据,以便从后端获得数据更新的通知。侦听器技术可减少您的使用和计费,并经过优化以在用户在线和离线时为他们提供最佳体验。
如果只需要一次数据,可以使用get()
从数据库中获取数据的快照。如果出于任何原因get()
无法返回服务器值,客户端将探测本地存储缓存并在仍未找到该值时返回错误。
不必要地使用get()
会增加带宽的使用并导致性能损失,这可以通过使用如上所示的实时侦听器来防止。
Web version 9
import { getDatabase, ref, child, get } from "firebase/database"; const dbRef = ref(getDatabase()); get(child(dbRef, `users/${userId}`)).then((snapshot) => { if (snapshot.exists()) { console.log(snapshot.val()); } else { console.log("No data available"); } }).catch((error) => { console.error(error); });
Web version 8
const dbRef = firebase.database().ref(); dbRef.child("users").child(userId).get().then((snapshot) => { if (snapshot.exists()) { console.log(snapshot.val()); } else { console.log("No data available"); } }).catch((error) => { console.error(error); });
用观察者读取一次数据
在某些情况下,您可能希望立即返回本地缓存中的值,而不是检查服务器上的更新值。在这些情况下,您可以使用once()
立即从本地磁盘缓存中获取数据。
这对于只需要加载一次并且预计不会频繁更改或不需要主动侦听的数据很有用。例如,前面示例中的博客应用程序使用此方法在用户开始创作新帖子时加载用户的个人资料:
Web version 9
import { getDatabase, ref, onValue } from "firebase/database"; import { getAuth } from "firebase/auth"; const db = getDatabase(); const auth = getAuth(); const userId = auth.currentUser.uid; return onValue(ref(db, '/users/' + userId), (snapshot) => { const username = (snapshot.val() && snapshot.val().username) || 'Anonymous'; // ... }, { onlyOnce: true });
Web version 8
var userId = firebase.auth().currentUser.uid; return firebase.database().ref('/users/' + userId).once('value').then((snapshot) => { var username = (snapshot.val() && snapshot.val().username) || 'Anonymous'; // ... });
更新或删除数据
更新特定字段
要同时写入一个节点的特定子节点而不覆盖其他子节点,请使用update()
方法。
调用update()
时,您可以通过指定键的路径来更新较低级别的子值。如果数据存储在多个位置以更好地扩展,您可以使用数据扇出更新该数据的所有实例。
例如,社交博客应用程序可能会创建一个帖子,并使用如下代码同时将其更新为最近的活动提要和发帖用户的活动提要:
Web version 9
import { getDatabase, ref, child, push, update } from "firebase/database"; function writeNewPost(uid, username, picture, title, body) { const db = getDatabase(); // A post entry. const postData = { author: username, uid: uid, body: body, title: title, starCount: 0, authorPic: picture }; // Get a key for a new Post. const newPostKey = push(child(ref(db), 'posts')).key; // Write the new post's data simultaneously in the posts list and the user's post list. const updates = {}; updates['/posts/' + newPostKey] = postData; updates['/user-posts/' + uid + '/' + newPostKey] = postData; return update(ref(db), updates); }
Web version 8
function writeNewPost(uid, username, picture, title, body) { // A post entry. var postData = { author: username, uid: uid, body: body, title: title, starCount: 0, authorPic: picture }; // Get a key for a new Post. var newPostKey = firebase.database().ref().child('posts').push().key; // Write the new post's data simultaneously in the posts list and the user's post list. var updates = {}; updates['/posts/' + newPostKey] = postData; updates['/user-posts/' + uid + '/' + newPostKey] = postData; return firebase.database().ref().update(updates); }
此示例使用push()
在包含所有用户在/posts/$postid
的帖子的节点中创建帖子,并同时检索密钥。然后可以使用该密钥在/user-posts/$userid/$postid
的用户帖子中创建第二个条目。
使用这些路径,您可以通过一次调用update()
对 JSON 树中的多个位置执行同步更新,例如本示例如何在两个位置创建新帖子。以这种方式进行的同时更新是原子的:要么所有更新都成功,要么所有更新都失败。
添加完成回调
如果您想知道您的数据何时提交,您可以添加一个完成回调。 set()
和update()
都采用可选的完成回调,当写入已提交到数据库时调用。如果调用不成功,回调将传递一个错误对象,指示失败发生的原因。
Web version 9
import { getDatabase, ref, set } from "firebase/database"; const db = getDatabase(); set(ref(db, 'users/' + userId), { username: name, email: email, profile_picture : imageUrl }) .then(() => { // Data saved successfully! }) .catch((error) => { // The write failed... });
Web version 8
firebase.database().ref('users/' + userId).set({ username: name, email: email, profile_picture : imageUrl }, (error) => { if (error) { // The write failed... } else { // Data saved successfully! } });
删除数据
删除数据的最简单方法是在对该数据位置的引用上调用remove()
。
您还可以通过将null
指定为另一个写入操作(例如set()
或update()
的值来删除。您可以将此技术与update()
结合使用,以在单个 API 调用中删除多个子项。
收到Promise
要知道您的数据何时提交到 Firebase 实时数据库服务器,您可以使用Promise
。 set()
和update()
都可以返回一个Promise
,您可以使用它来了解写入何时提交到数据库。
分离监听器
通过在 Firebase 数据库引用上调用off()
方法来删除回调。
您可以通过将其作为参数传递给off()
来删除单个侦听器。在不带参数的位置上调用off()
会删除该位置的所有侦听器。
在父监听器上调用off()
不会自动删除在其子节点上注册的监听器;还必须对任何子侦听器调用off()
以删除回调。
将数据保存为事务
当处理可能被并发修改破坏的数据时,例如增量计数器,您可以使用事务操作。您可以为该操作提供一个更新函数和一个可选的完成回调。 update 函数将数据的当前状态作为参数并返回您想要写入的新的所需状态。如果另一个客户端在您的新值成功写入之前写入该位置,则会使用新的当前值再次调用您的更新函数,然后重试写入。
例如,在示例社交博客应用程序中,您可以允许用户为帖子加注星标和取消加注星标,并跟踪帖子收到的星标数量,如下所示:
Web version 9
import { getDatabase, ref, runTransaction } from "firebase/database"; function toggleStar(uid) { const db = getDatabase(); const postRef = ref(db, '/posts/foo-bar-123'); runTransaction(postRef, (post) => { if (post) { if (post.stars && post.stars[uid]) { post.starCount--; post.stars[uid] = null; } else { post.starCount++; if (!post.stars) { post.stars = {}; } post.stars[uid] = true; } } return post; }); }
Web version 8
function toggleStar(postRef, uid) { postRef.transaction((post) => { if (post) { if (post.stars && post.stars[uid]) { post.starCount--; post.stars[uid] = null; } else { post.starCount++; if (!post.stars) { post.stars = {}; } post.stars[uid] = true; } } return post; }); }
如果多个用户同时为同一个帖子加注星标或者客户的数据过时,使用事务可以防止加星计数不正确。如果事务被拒绝,服务器将当前值返回给客户端,客户端使用更新后的值再次运行事务。重复此过程,直到交易被接受或您中止交易。
原子服务器端增量
在上面的用例中,我们向数据库写入了两个值:对帖子加星/取消加星的用户 ID,以及增加的星数。如果我们已经知道用户正在为帖子加注星标,我们可以使用原子增量操作而不是事务。
function addStar(uid, key) { const updates = {}; updates[`posts/${key}/stars/${uid}`] = true; updates[`posts/${key}/starCount`] = firebase.database.ServerValue.increment(1); updates[`user-posts/${key}/stars/${uid}`] = true; updates[`user-posts/${key}/starCount`] = firebase.database.ServerValue.increment(1); firebase.database().ref().update(updates); }
此代码不使用事务操作,因此如果存在更新冲突,它不会自动重新运行。但是,由于增量操作直接发生在数据库服务器上,因此不会发生冲突。
如果您想检测并拒绝特定于应用程序的冲突,例如用户为他们之前已经加星标的帖子加注星标,您应该为该用例编写自定义安全规则。
离线处理数据
如果客户端失去网络连接,您的应用程序将继续正常运行。
每个连接到 Firebase 数据库的客户端都维护其自己的任何活动数据的内部版本。写入数据时,先写入这个本地版本。然后,Firebase 客户端会在“尽力”的基础上将该数据与远程数据库服务器和其他客户端同步。
因此,在将任何数据写入服务器之前,所有对数据库的写入都会立即触发本地事件。这意味着无论网络延迟或连接如何,您的应用程序都会保持响应。
重新建立连接后,您的应用程序会收到一组适当的事件,以便客户端与当前服务器状态同步,而无需编写任何自定义代码。
我们将在了解有关在线和离线功能的更多信息中更多地讨论离线行为。
下一步
,(可选)使用 Firebase Local Emulator Suite 制作原型并进行测试
在讨论您的应用程序如何读取和写入实时数据库之前,让我们介绍一组可用于原型设计和测试实时数据库功能的工具:Firebase Local Emulator Suite。如果您正在尝试不同的数据模型、优化您的安全规则,或者努力寻找与后端交互的最具成本效益的方式,那么能够在不部署实时服务的情况下在本地工作可能是一个好主意。
Realtime Database 模拟器是 Local Emulator Suite 的一部分,它使您的应用程序能够与您的模拟数据库内容和配置以及可选的模拟项目资源(函数、其他数据库和安全规则)进行交互。
使用实时数据库模拟器只需几个步骤:
- 在您的应用程序的测试配置中添加一行代码以连接到模拟器。
- 从本地项目目录的根目录运行
firebase emulators:start
。 - 像往常一样使用实时数据库平台 SDK 或使用实时数据库 REST API 从应用程序的原型代码进行调用。
提供了涉及实时数据库和云函数的详细演练。您还应该看看Local Emulator Suite introduction 。
获取数据库引用
要从数据库读取或写入数据,您需要一个firebase.database.Reference
实例:
Web version 9
import { getDatabase } from "firebase/database"; const database = getDatabase();
Web version 8
var database = firebase.database();
写数据
本文档介绍了检索数据的基础知识以及如何排序和过滤 Firebase 数据。
通过将异步侦听器附加到 firebase.database.Reference 来检索firebase.database.Reference
数据。侦听器针对数据的初始状态触发一次,并在数据更改时再次触发。
基本写操作
对于基本的写操作,您可以使用set()
将数据保存到指定的引用,替换该路径中的任何现有数据。例如,社交博客应用程序可能会使用set()
添加用户,如下所示:
Web version 9
import { getDatabase, ref, set } from "firebase/database"; function writeUserData(userId, name, email, imageUrl) { const db = getDatabase(); set(ref(db, 'users/' + userId), { username: name, email: email, profile_picture : imageUrl }); }
Web version 8
function writeUserData(userId, name, email, imageUrl) { firebase.database().ref('users/' + userId).set({ username: name, email: email, profile_picture : imageUrl }); }
使用set()
覆盖指定位置的数据,包括任何子节点。
读取数据
监听值事件
要读取路径上的数据并监听变化,请使用onValue()
来观察事件。您可以使用此事件读取给定路径中内容的静态快照,因为它们在事件发生时存在。当监听器被附加时,这个方法被触发一次,每次数据(包括孩子)发生变化时再次触发。向事件回调传递一个快照,其中包含该位置的所有数据,包括子数据。如果没有数据,快照将在调用exists()
时返回false
,在调用val()
时返回null
。
以下示例演示了一个社交博客应用程序从数据库中检索帖子的星级计数:
Web version 9
import { getDatabase, ref, onValue} from "firebase/database"; const db = getDatabase(); const starCountRef = ref(db, 'posts/' + postId + '/starCount'); onValue(starCountRef, (snapshot) => { const data = snapshot.val(); updateStarCount(postElement, data); });
Web version 8
var starCountRef = firebase.database().ref('posts/' + postId + '/starCount'); starCountRef.on('value', (snapshot) => { const data = snapshot.val(); updateStarCount(postElement, data); });
侦听器收到一个snapshot
,其中包含事件发生时数据库中指定位置的数据。您可以使用val()
方法检索snapshot
中的数据。
一次读取数据
使用 get() 读取一次数据
SDK 旨在管理与数据库服务器的交互,无论您的应用程序是在线还是离线。
通常,您应该使用上述值事件技术来读取数据,以便从后端获得数据更新的通知。侦听器技术可减少您的使用和计费,并经过优化以在用户在线和离线时为他们提供最佳体验。
如果只需要一次数据,可以使用get()
从数据库中获取数据的快照。如果出于任何原因get()
无法返回服务器值,客户端将探测本地存储缓存并在仍未找到该值时返回错误。
不必要地使用get()
会增加带宽的使用并导致性能损失,这可以通过使用如上所示的实时侦听器来防止。
Web version 9
import { getDatabase, ref, child, get } from "firebase/database"; const dbRef = ref(getDatabase()); get(child(dbRef, `users/${userId}`)).then((snapshot) => { if (snapshot.exists()) { console.log(snapshot.val()); } else { console.log("No data available"); } }).catch((error) => { console.error(error); });
Web version 8
const dbRef = firebase.database().ref(); dbRef.child("users").child(userId).get().then((snapshot) => { if (snapshot.exists()) { console.log(snapshot.val()); } else { console.log("No data available"); } }).catch((error) => { console.error(error); });
用观察者读取一次数据
在某些情况下,您可能希望立即返回本地缓存中的值,而不是检查服务器上的更新值。在这些情况下,您可以使用once()
立即从本地磁盘缓存中获取数据。
这对于只需要加载一次并且预计不会频繁更改或不需要主动侦听的数据很有用。例如,前面示例中的博客应用程序使用此方法在用户开始创作新帖子时加载用户的个人资料:
Web version 9
import { getDatabase, ref, onValue } from "firebase/database"; import { getAuth } from "firebase/auth"; const db = getDatabase(); const auth = getAuth(); const userId = auth.currentUser.uid; return onValue(ref(db, '/users/' + userId), (snapshot) => { const username = (snapshot.val() && snapshot.val().username) || 'Anonymous'; // ... }, { onlyOnce: true });
Web version 8
var userId = firebase.auth().currentUser.uid; return firebase.database().ref('/users/' + userId).once('value').then((snapshot) => { var username = (snapshot.val() && snapshot.val().username) || 'Anonymous'; // ... });
更新或删除数据
更新特定字段
要同时写入一个节点的特定子节点而不覆盖其他子节点,请使用update()
方法。
调用update()
时,您可以通过指定键的路径来更新较低级别的子值。如果数据存储在多个位置以更好地扩展,您可以使用数据扇出更新该数据的所有实例。
例如,社交博客应用程序可能会创建一个帖子,并使用如下代码同时将其更新为最近的活动提要和发帖用户的活动提要:
Web version 9
import { getDatabase, ref, child, push, update } from "firebase/database"; function writeNewPost(uid, username, picture, title, body) { const db = getDatabase(); // A post entry. const postData = { author: username, uid: uid, body: body, title: title, starCount: 0, authorPic: picture }; // Get a key for a new Post. const newPostKey = push(child(ref(db), 'posts')).key; // Write the new post's data simultaneously in the posts list and the user's post list. const updates = {}; updates['/posts/' + newPostKey] = postData; updates['/user-posts/' + uid + '/' + newPostKey] = postData; return update(ref(db), updates); }
Web version 8
function writeNewPost(uid, username, picture, title, body) { // A post entry. var postData = { author: username, uid: uid, body: body, title: title, starCount: 0, authorPic: picture }; // Get a key for a new Post. var newPostKey = firebase.database().ref().child('posts').push().key; // Write the new post's data simultaneously in the posts list and the user's post list. var updates = {}; updates['/posts/' + newPostKey] = postData; updates['/user-posts/' + uid + '/' + newPostKey] = postData; return firebase.database().ref().update(updates); }
此示例使用push()
在包含所有用户在/posts/$postid
的帖子的节点中创建帖子,并同时检索密钥。然后可以使用该密钥在/user-posts/$userid/$postid
的用户帖子中创建第二个条目。
使用这些路径,您可以通过一次调用update()
对 JSON 树中的多个位置执行同步更新,例如本示例如何在两个位置创建新帖子。以这种方式进行的同时更新是原子的:要么所有更新都成功,要么所有更新都失败。
添加完成回调
如果您想知道您的数据何时提交,您可以添加一个完成回调。 set()
和update()
都采用可选的完成回调,当写入已提交到数据库时调用。如果调用不成功,回调将传递一个错误对象,指示失败发生的原因。
Web version 9
import { getDatabase, ref, set } from "firebase/database"; const db = getDatabase(); set(ref(db, 'users/' + userId), { username: name, email: email, profile_picture : imageUrl }) .then(() => { // Data saved successfully! }) .catch((error) => { // The write failed... });
Web version 8
firebase.database().ref('users/' + userId).set({ username: name, email: email, profile_picture : imageUrl }, (error) => { if (error) { // The write failed... } else { // Data saved successfully! } });
删除数据
删除数据的最简单方法是在对该数据位置的引用上调用remove()
。
您还可以通过将null
指定为另一个写入操作(例如set()
或update()
的值来删除。您可以将此技术与update()
结合使用,以在单个 API 调用中删除多个子项。
收到Promise
要知道您的数据何时提交到 Firebase 实时数据库服务器,您可以使用Promise
。 set()
和update()
都可以返回一个Promise
,您可以使用它来了解写入何时提交到数据库。
分离监听器
通过在 Firebase 数据库引用上调用off()
方法来删除回调。
您可以通过将其作为参数传递给off()
来删除单个侦听器。在不带参数的位置上调用off()
会删除该位置的所有侦听器。
在父监听器上调用off()
不会自动删除在其子节点上注册的监听器;还必须对任何子侦听器调用off()
以删除回调。
将数据保存为事务
当处理可能被并发修改破坏的数据时,例如增量计数器,您可以使用事务操作。您可以为该操作提供一个更新函数和一个可选的完成回调。 update 函数将数据的当前状态作为参数并返回您想要写入的新的期望状态。如果另一个客户端在您的新值成功写入之前写入该位置,则会使用新的当前值再次调用您的更新函数,然后重试写入。
例如,在示例社交博客应用程序中,您可以允许用户为帖子加注星标和取消加注星标,并跟踪帖子收到的星标数量,如下所示:
Web version 9
import { getDatabase, ref, runTransaction } from "firebase/database"; function toggleStar(uid) { const db = getDatabase(); const postRef = ref(db, '/posts/foo-bar-123'); runTransaction(postRef, (post) => { if (post) { if (post.stars && post.stars[uid]) { post.starCount--; post.stars[uid] = null; } else { post.starCount++; if (!post.stars) { post.stars = {}; } post.stars[uid] = true; } } return post; }); }
Web version 8
function toggleStar(postRef, uid) { postRef.transaction((post) => { if (post) { if (post.stars && post.stars[uid]) { post.starCount--; post.stars[uid] = null; } else { post.starCount++; if (!post.stars) { post.stars = {}; } post.stars[uid] = true; } } return post; }); }
如果多个用户同时为同一个帖子加注星标或者客户的数据过时,使用事务可以防止加星计数不正确。如果事务被拒绝,服务器将当前值返回给客户端,客户端使用更新后的值再次运行事务。重复此过程,直到交易被接受或您中止交易。
原子服务器端增量
在上面的用例中,我们向数据库写入了两个值:对帖子加星/取消加星的用户 ID,以及增加的星数。如果我们已经知道用户正在为帖子加注星标,我们可以使用原子增量操作而不是事务。
function addStar(uid, key) { const updates = {}; updates[`posts/${key}/stars/${uid}`] = true; updates[`posts/${key}/starCount`] = firebase.database.ServerValue.increment(1); updates[`user-posts/${key}/stars/${uid}`] = true; updates[`user-posts/${key}/starCount`] = firebase.database.ServerValue.increment(1); firebase.database().ref().update(updates); }
此代码不使用事务操作,因此如果存在更新冲突,它不会自动重新运行。但是,由于增量操作直接发生在数据库服务器上,因此不会发生冲突。
如果您想检测并拒绝特定于应用程序的冲突,例如用户为他们之前已经加星标的帖子加注星标,您应该为该用例编写自定义安全规则。
离线处理数据
如果客户端失去网络连接,您的应用程序将继续正常运行。
每个连接到 Firebase 数据库的客户端都维护其自己的任何活动数据的内部版本。写入数据时,先写入这个本地版本。然后,Firebase 客户端会在“尽力”的基础上将该数据与远程数据库服务器和其他客户端同步。
因此,在将任何数据写入服务器之前,所有对数据库的写入都会立即触发本地事件。这意味着无论网络延迟或连接如何,您的应用程序都会保持响应。
重新建立连接后,您的应用程序会收到一组适当的事件,以便客户端与当前服务器状态同步,而无需编写任何自定义代码。
我们将在了解有关在线和离线功能的更多信息中更多地讨论离线行为。