获取我们在 Firebase 峰会上发布的所有信息,了解 Firebase 可如何帮助您加快应用开发速度并满怀信心地运行应用。了解详情

在 Web 上读取和写入数据

使用集合让一切井井有条 根据您的偏好保存内容并对其进行分类。

(可选)使用 Firebase 本地模拟器套件进行原型设计和测试

在讨论您的应用如何读取和写入实时数据库之前,让我们介绍一组可用于原型化和测试实时数据库功能的工具:Firebase Local Emulator Suite。如果您正在尝试不同的数据模型,优化您的安全规则,或努力寻找与后端交互的最具成本效益的方式,那么无需部署实时服务即可在本地工作可能是一个好主意。

实时数据库模拟器是本地模拟器套件的一部分,它使您的应用程序能够与您的模拟数据库内容和配置以及您的模拟项目资源(函数、其他数据库和安全规则)进行交互。

使用实时数据库模拟器只需几个步骤:

  1. 在应用程序的测试配置中添加一行代码以连接到模拟器。
  2. 从本地项目目录的根目录运行firebase emulators:start
  3. 像往常一样使用实时数据库平台 SDK 或使用实时数据库 REST API 从应用程序的原型代码进行调用。

提供了涉及实时数据库和云函数的详细演练。您还应该查看Local Emulator Suite 介绍

获取数据库参考

要从数据库读取或写入数据,您需要一个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()时,您可以通过指定键的路径来更新较低级别的子值。如果数据存储在多个位置以更好地扩展,您可以使用data fan-out更新该数据的所有实例。

例如,社交博客应用程序可能会创建一个帖子,并同时使用以下代码将其更新为最近的活动源和发布用户的活动源:

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 实时数据库服务器,您可以使用Promiseset()update()都可以返回一个Promise ,您可以使用它来了解何时将写入提交到数据库。

分离监听器

通过在 Firebase 数据库引用上调用off()方法来删​​除回调。

您可以通过将单个侦听器作为参数传递给off()来删除它。在不带参数的位置调用off()会删除该位置的所有侦听器。

在父监听器上调用off()不会自动删除在其子节点上注册的监听器;还必须在任何子侦听器上调用off()以删除回调。

将数据保存为事务

当处理可能被并发修改破坏的数据时,例如增量计数器,您可以使用事务操作。你可以给这个操作一个更新函数和一个可选的完成回调。更新函数将数据的当前状态作为参数,并返回您想要写入的新状态。如果另一个客户端在您的新值成功写入之前写入该位置,则使用新的当前值再次调用您的更新函数,并重试写入。

例如,在示例社交博客应用程序中,您可以允许用户为帖子加注星标和取消加注星标,并跟踪帖子获得的星数,如下所示:

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 客户端会在“尽力而为”的基础上将该数据与远程数据库服务器和其他客户端同步。

因此,在任何数据写入服务器之前,对数据库的所有写入都会立即触发本地事件。这意味着无论网络延迟或连接如何,您的应用都会保持响应。

重新建立连接后,您的应用程序会收到一组适当的事件,以便客户端与当前服务器状态同步,而无需编写任何自定义代码。

我们将在了解有关在线和离线功能的更多信息中详细讨论离线行为。

下一步