Cloud Firestore Web Codelab

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

一、概述

目标

在此 Codelab 中,您将构建一个由Cloud Firestore提供支持的餐厅推荐 Web 应用。

img5.png

你会学到什么

  • 从网络应用读取数据并将数据写入 Cloud Firestore
  • 实时监听 Cloud Firestore 数据的变化
  • 使用 Firebase 身份验证和安全规则来保护 Cloud Firestore 数据
  • 编写复杂的 Cloud Firestore 查询

你需要什么

在开始此 Codelab 之前,请确保您已安装:

2. 创建并设置 Firebase 项目

创建一个 Firebase 项目

  1. Firebase 控制台中,单击添加项目,然后将 Firebase 项目命名为FriendlyEats

记住您的 Firebase 项目的项目 ID。

  1. 单击创建项目

我们要构建的应用程序使用了一些 Web 上可用的 Firebase 服务:

  • Firebase 身份验证可轻松识别您的用户
  • Cloud Firestore将结构化数据保存在云端,并在数据更新时获得即时通知
  • Firebase 托管来托管和提供您的静态资产

对于这个特定的代码实验室,我们已经配置了 Firebase 托管。但是,对于 Firebase 身份验证和 Cloud Firestore,我们将引导您使用 Firebase 控制台完成服务的配置和启用。

启用匿名身份验证

尽管身份验证不是此 Codelab 的重点,但在我们的应用程序中具有某种形式的身份验证很重要。我们将使用匿名登录——这意味着用户将在没有提示的情况下静默登录。

您需要启用匿名登录。

  1. 在 Firebase 控制台中,找到左侧导航中的Build部分。
  2. 点击Authentication ,然后点击Sign-in method选项卡(或点击此处直接前往)。
  3. 启用匿名登录提供程序,然后单击保存

img7.png

这将允许应用程序在您的用户访问 Web 应用程序时静默登录。随意阅读匿名身份验证文档以了解更多信息。

启用 Cloud Firestore

该应用程序使用 Cloud Firestore 来保存和接收餐厅信息和评级。

您需要启用 Cloud Firestore。在 Firebase 控制台的Build部分,点击Firestore Database 。单击 Cloud Firestore 窗格中的创建数据库

对 Cloud Firestore 中数据的访问由安全规则控制。我们稍后将在此 Codelab 中详细讨论规则,但首先我们需要为数据设置一些基本规则才能开始。在 Firebase 控制台的规则选项卡中添加以下规则,然后单击发布

service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      //
      // WARNING: These rules are insecure! We will replace them with
      // more secure rules later in the codelab
      //
      allow read, write: if request.auth != null;
    }
  }
}

上述规则限制了登录用户的数据访问权限,从而防止未经身份验证的用户读取或写入。这比允许公共访问要好,但仍然远非安全,我们将在后面的代码实验室中改进这些规则。

3.获取示例代码

从命令行克隆GitHub 存储库

git clone https://github.com/firebase/friendlyeats-web

示例代码应该已经被克隆到 📁friendlyeats friendlyeats-web目录中。从现在开始,请确保从此目录运行所有命令:

cd friendlyeats-web

导入入门应用

使用您的 IDE(WebStorm、Atom、Sublime、Visual Studio Code...)打开或导入 📁friendlyeats friendlyeats-web目录。此目录包含 codelab 的起始代码,其中包含一个尚未功能的餐厅推荐应用程序。我们将使其在整个 Codelab 中发挥作用,因此您需要尽快在该目录中编辑代码。

4. 安装 Firebase 命令行界面

Firebase 命令行界面 (CLI) 允许您在本地提供 Web 应用程序并将您的 Web 应用程序部署到 Firebase 托管。

  1. 通过运行以下 npm 命令安装 CLI:
npm -g install firebase-tools
  1. 通过运行以下命令验证 CLI 是否已正确安装:
firebase --version

确保 Firebase CLI 的版本是 v7.4.0 或更高版本。

  1. 通过运行以下命令授权 Firebase CLI:
firebase login

我们已设置 Web 应用模板,以便从应用的本地目录和文件中提取应用的 Firebase 托管配置。但要做到这一点,我们需要将您的应用与您的 Firebase 项目相关联。

  1. 确保您的命令行正在访问您的应用程序的本地目录。
  2. 通过运行以下命令将您的应用与 Firebase 项目相关联:
firebase use --add
  1. 出现提示时,选择您的Project ID ,然后为您的 Firebase 项目指定一个别名。

如果您有多个环境(生产、登台等),则别名很有用。但是,对于这个 codelab,我们只使用default的别名。

  1. 按照命令行中的其余说明进行操作。

5.运行本地服务器

我们已经准备好开始我们的应用程序了!让我们在本地运行我们的应用程序!

  1. 运行以下 Firebase CLI 命令:
firebase emulators:start --only hosting
  1. 您的命令行应显示以下响应:
hosting: Local server: http://localhost:5000

我们正在使用Firebase 托管模拟器在本地为我们的应用提供服务。 Web 应用程序现在应该可以从http://localhost:5000获得。

  1. http://localhost:5000打开您的应用程序。

您应该会看到已连接到 Firebase 项目的 FriendlyEats 副本。

该应用程序已自动连接到您的 Firebase 项目,并以匿名用户身份以静默方式登录。

img2.png

6. 将数据写入 Cloud Firestore

在本节中,我们将向 Cloud Firestore 写入一些数据,以便我们可以填充应用的 UI。这可以通过Firebase 控制台手动完成,但我们将在应用程序本身中完成,以演示基本的 Cloud Firestore 编写。

数据模型

Firestore 数据分为集合、文档、字段和子集合。我们将把每家餐馆作为一个文档存储在一个名为restaurants的顶级集合中。

img3.png

稍后,我们会将每条ratings存储在每个餐厅下的名为 rating 的子集合中。

img4.png

将餐厅添加到 Firestore

我们应用程序中的主要模型对象是餐厅。让我们编写一些代码,将餐厅文档添加到restaurants集合中。

  1. 从您下载的文件中,打开scripts/FriendlyEats.Data.js
  2. 找到函数FriendlyEats.prototype.addRestaurant
  3. 用以下代码替换整个函数。

FriendlyEats.Data.js

FriendlyEats.prototype.addRestaurant = function(data) {
  var collection = firebase.firestore().collection('restaurants');
  return collection.add(data);
};

上面的代码向restaurants集合添加了一个新文档。文档数据来自一个普通的 JavaScript 对象。为此,我们首先获取对 Cloud Firestore 集合restaurants的引用,然后add数据。

让我们添加餐厅!

  1. 在浏览器中返回您的 FriendlyEats 应用程序并刷新它。
  2. 单击添加模拟数据

该应用程序将自动生成一组随机的餐馆对象,然后调用您的addRestaurant函数。但是,您还不会在实际的 Web 应用程序中看到数据,因为我们仍然需要实现检索数据(代码实验室的下一部分)。

但是,如果您导航到 Firebase 控制台中的Cloud Firestore 选项卡,您现在应该会在restaurants集合中看到新文档!

img6.png

恭喜,您刚刚从 Web 应用将数据写入 Cloud Firestore!

在下一部分中,您将了解如何从 Cloud Firestore 检索数据并将其显示在您的应用中。

7. 显示来自 Cloud Firestore 的数据

在本节中,您将了解如何从 Cloud Firestore 检索数据并将其显示在您的应用中。两个关键步骤是创建查询和添加快照侦听器。此侦听器将收到与查询匹配的所有现有数据的通知,并将实时接收更新。

首先,让我们构建将提供默认的、未过滤的餐馆列表的查询。

  1. 返回文件scripts/FriendlyEats.Data.js
  2. 找到函数FriendlyEats.prototype.getAllRestaurants
  3. 用以下代码替换整个函数。

FriendlyEats.Data.js

FriendlyEats.prototype.getAllRestaurants = function(renderer) {
  var query = firebase.firestore()
      .collection('restaurants')
      .orderBy('avgRating', 'desc')
      .limit(50);

  this.getDocumentsInQuery(query, renderer);
};

在上面的代码中,我们构建了一个查询,它将从名为 Restaurants 的顶级集合中检索多达 50 家restaurants ,这些餐馆按平均评分排序(目前全为零)。在我们声明这个查询之后,我们将它传递给负责加载和呈现数据的getDocumentsInQuery()方法。

我们将通过添加快照侦听器来做到这一点。

  1. 返回文件scripts/FriendlyEats.Data.js
  2. 找到函数FriendlyEats.prototype.getDocumentsInQuery
  3. 用以下代码替换整个函数。

FriendlyEats.Data.js

FriendlyEats.prototype.getDocumentsInQuery = function(query, renderer) {
  query.onSnapshot(function(snapshot) {
    if (!snapshot.size) return renderer.empty(); // Display "There are no restaurants".

    snapshot.docChanges().forEach(function(change) {
      if (change.type === 'removed') {
        renderer.remove(change.doc);
      } else {
        renderer.display(change.doc);
      }
    });
  });
};

在上面的代码中, query.onSnapshot将在每次查询结果发生变化时触发其回调。

  • 第一次,回调是由查询的整个结果集触发的——这意味着来自 Cloud Firestore 的整个restaurants集合。然后它将所有单独的文档传递给renderer.display函数。
  • 删除文档时, change.type等于removed 。所以在这种情况下,我们将调用一个从 UI 中删除餐厅的函数。

现在我们已经实现了这两种方法,刷新应用并验证我们之前在 Firebase 控制台中看到的餐厅现在是否在应用中可见。如果您成功完成了本部分,那么您的应用现在正在使用 Cloud Firestore 读取和写入数据!

随着您的餐厅列表发生变化,此侦听器将不断自动更新。尝试转到 Firebase 控制台并手动删除餐厅或更改其名称 - 您会立即看到更改显示在您的网站上!

img5.png

8. 获取()数据

到目前为止,我们已经展示了如何使用onSnapshot来实时检索更新;然而,这并不总是我们想要的。有时只获取一次数据更有意义。

我们希望实现一个方法,当用户点击您应用中的特定餐厅时触发该方法。

  1. 回到你的文件scripts/FriendlyEats.Data.js
  2. 找到函数FriendlyEats.prototype.getRestaurant
  3. 用以下代码替换整个函数。

FriendlyEats.Data.js

FriendlyEats.prototype.getRestaurant = function(id) {
  return firebase.firestore().collection('restaurants').doc(id).get();
};

实施此方法后,您将能够查看每家餐厅的页面。只需单击列表中的餐厅,您应该会看到餐厅的详细信息页面:

img1.png

目前,您无法添加评分,因为我们稍后仍需要在 codelab 中实现添加评分。

9. 排序和过滤数据

目前,我们的应用程序显示餐厅列表,但用户无法根据需要进行过滤。在本部分中,您将使用 Cloud Firestore 的高级查询来启用过滤。

这是获取所有Dim Sum餐厅的简单查询示例:

var filteredQuery = query.where('category', '==', 'Dim Sum')

顾名思义, where()方法将使我们的查询仅下载其字段满足我们设置的限制的集合成员。在这种情况下,它只会下载categoryDim Sum的餐馆。

在我们的应用程序中,用户可以链接多个过滤器来创建特定查询,例如“旧金山的披萨”或“洛杉矶的海鲜按人气订购”。

我们将创建一个方法来构建一个查询,该查询将根据用户选择的多个条件过滤我们的餐厅。

  1. 回到你的文件scripts/FriendlyEats.Data.js
  2. 找到函数FriendlyEats.prototype.getFilteredRestaurants
  3. 用以下代码替换整个函数。

FriendlyEats.Data.js

FriendlyEats.prototype.getFilteredRestaurants = function(filters, renderer) {
  var query = firebase.firestore().collection('restaurants');

  if (filters.category !== 'Any') {
    query = query.where('category', '==', filters.category);
  }

  if (filters.city !== 'Any') {
    query = query.where('city', '==', filters.city);
  }

  if (filters.price !== 'Any') {
    query = query.where('price', '==', filters.price.length);
  }

  if (filters.sort === 'Rating') {
    query = query.orderBy('avgRating', 'desc');
  } else if (filters.sort === 'Reviews') {
    query = query.orderBy('numRatings', 'desc');
  }

  this.getDocumentsInQuery(query, renderer);
};

上面的代码添加了多个where过滤器和一个orderBy子句来构建基于用户输入的复合查询。我们的查询现在将只返回符合用户要求的餐厅。

在浏览器中刷新您的 FriendlyEats 应用程序,然后验证您是否可以按价格、城市和类别进行过滤。测试时,您会在浏览器的 JavaScript 控制台中看到如下所示的错误:

The query requires an index. You can create it here: https://console.firebase.google.com/project/.../database/firestore/indexes?create_index=...

这些错误是因为 Cloud Firestore 需要大多数复合查询的索引。对查询要求索引可让 Cloud Firestore 保持大规模快速运行。

打开错误消息中的链接将自动在 Firebase 控制台中打开索引创建 UI,并填写正确的参数。在下一节中,我们将编写和部署此应用程序所需的索引。

10.部署索引

如果我们不想探索应用程序中的每条路径并遵循每个索引创建链接,我们可以使用 Firebase CLI 轻松地一次部署多个索引。

  1. 在您应用下载的本地目录中,您会找到一个firestore.indexes.json文件。

该文件描述了所有可能的过滤器组合所需的所有索引。

firestore.indexes.json

{
 "indexes": [
   {
     "collectionGroup": "restaurants",
     "queryScope": "COLLECTION",
     "fields": [
       { "fieldPath": "city", "order": "ASCENDING" },
       { "fieldPath": "avgRating", "order": "DESCENDING" }
     ]
   },

   ...

 ]
}
  1. 使用以下命令部署这些索引:
firebase deploy --only firestore:indexes

几分钟后,您的索引将生效,错误消息将消失。

11.在事务中写入数据

在本节中,我们将添加用户向餐厅提交评论的功能。到目前为止,我们所有的写入都是原子的并且相对简单。如果其中任何一个出错,我们可能只会提示用户重试,否则我们的应用程序会自动重试写入。

我们的应用程序将有许多用户想要为餐厅添加评分,因此我们需要协调多次读取和写入。首先必须提交评论本身,然后需要更新餐厅的评分countaverage rating 。如果其中一个失败但另一个失败,我们将处于不一致的状态,即我们数据库的一部分中的数据与另一部分中的数据不匹配。

幸运的是,Cloud Firestore 提供了事务功能,允许我们在单个原子操作中执行多个读取和写入,确保我们的数据保持一致。

  1. 回到你的文件scripts/FriendlyEats.Data.js
  2. 找到函数FriendlyEats.prototype.addRating
  3. 用以下代码替换整个函数。

FriendlyEats.Data.js

FriendlyEats.prototype.addRating = function(restaurantID, rating) {
  var collection = firebase.firestore().collection('restaurants');
  var document = collection.doc(restaurantID);
  var newRatingDocument = document.collection('ratings').doc();

  return firebase.firestore().runTransaction(function(transaction) {
    return transaction.get(document).then(function(doc) {
      var data = doc.data();

      var newAverage =
          (data.numRatings * data.avgRating + rating.rating) /
          (data.numRatings + 1);

      transaction.update(document, {
        numRatings: data.numRatings + 1,
        avgRating: newAverage
      });
      return transaction.set(newRatingDocument, rating);
    });
  });
};

在上面的块中,我们触发了一个事务来更新餐厅文档中的avgRatingnumRatings的数值。同时,我们将新rating添加到ratings子集合中。

12. 保护您的数据

在本 Codelab 开始时,我们设置了应用程序的安全规则,以完全开放数据库以进行任何读取或写入。在实际应用程序中,我们希望设置更细粒度的规则来防止不需要的数据访问或修改。

  1. 在 Firebase 控制台的Build部分,点击Firestore Database
  2. 单击 Cloud Firestore 部分中的规则选项卡(或单击此处直接前往那里)。
  3. 将默认值替换为以下规则,然后单击Publish

firestore.rules

rules_version = '2';
service cloud.firestore {

  // Determine if the value of the field "key" is the same
  // before and after the request.
  function unchanged(key) {
    return (key in resource.data) 
      && (key in request.resource.data) 
      && (resource.data[key] == request.resource.data[key]);
  }

  match /databases/{database}/documents {
    // Restaurants:
    //   - Authenticated user can read
    //   - Authenticated user can create/update (for demo purposes only)
    //   - Updates are allowed if no fields are added and name is unchanged
    //   - Deletes are not allowed (default)
    match /restaurants/{restaurantId} {
      allow read: if request.auth != null;
      allow create: if request.auth != null;
      allow update: if request.auth != null
                    && (request.resource.data.keys() == resource.data.keys()) 
                    && unchanged("name");
      
      // Ratings:
      //   - Authenticated user can read
      //   - Authenticated user can create if userId matches
      //   - Deletes and updates are not allowed (default)
      match /ratings/{ratingId} {
        allow read: if request.auth != null;
        allow create: if request.auth != null
                      && request.resource.data.userId == request.auth.uid;
      }
    }
  }
}

这些规则限制访问以确保客户端仅进行安全更改。例如:

  • 对餐厅文档的更新只能更改评级,而不是名称或任何其他不可变数据。
  • 仅当用户 ID 与登录用户匹配时才能创建评级,从而防止欺骗。

除了使用 Firebase 控制台之外,您还可以使用 Firebase CLI 将规则部署到您的 Firebase 项目。工作目录中的firestore.rules文件已经包含上面的规则。要从本地文件系统(而不是使用 Firebase 控制台)部署这些规则,您需要运行以下命令:

firebase deploy --only firestore:rules

13. 结论

在此 Codelab 中,您了解了如何使用 Cloud Firestore 执行基本和高级读取和写入,以及如何使用安全规则保护数据访问。您可以在quickstarts-js 存储库中找到完整的解决方案。

要了解有关 Cloud Firestore 的更多信息,请访问以下资源: