Web 版 Cloud Firestore Codelab

1.概览

目标

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

img5.png

学习内容

  • 通过 Web 应用从 Cloud Firestore 读取数据和向其中写入数据
  • 实时监听 Cloud Firestore 数据的变化
  • 使用 Firebase Authentication 和安全规则保护 Cloud Firestore 数据
  • 编写复杂的 Cloud Firestore 查询

所需条件

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

2. 创建和设置 Firebase 项目

创建 Firebase 项目

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

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

  1. 点击创建项目

我们将要构建的应用使用 Web 上提供的多种 Firebase 服务:

  • Firebase Authentication:用于轻松识别用户
  • Cloud Firestore:用于在云端保存结构化数据,并在数据更新时即时收到通知
  • Firebase Hosting:用于托管和提供您的静态资产

对于此特定 Codelab,我们已经配置了 Firebase Hosting。但是对于 Firebase Auth 和 Cloud Firestore,我们将引导您完成使用 Firebase 控制台配置和启用服务的过程。

启用匿名身份验证

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

您需要启用匿名登录

  1. 在 Firebase 控制台中,在左侧导航栏中找到构建部分。
  2. 点击 Authentication,然后点击登录方法标签页(或点击此处直接转到标签页)。
  3. 启用匿名登录服务提供方,然后点击保存

img7.png

这样,应用就可以在用户访问 Web 应用时让其静默登录。如需了解详情,请参阅匿名身份验证文档

启用 Cloud Firestore

该应用使用 Cloud Firestore 保存并接收餐馆信息和评分。

您需要启用 Cloud Firestore。在 Firebase 控制台的构建部分中,点击 Firestore 数据库。点击 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;
    }
  }
}

上述规则会限制已登录用户对数据的访问,以防止未经身份验证的用户执行读写操作。这种方式要好于允许公开访问,但仍然不够安全,我们会在本 Codelab 的稍后部分中改进这些规则。

3. 获取示例代码

从命令行克隆 GitHub 代码库

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

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

cd friendlyeats-web/vanilla-js

导入 starter 应用

使用 IDE(WebStorm、Atom、Sublime、Visual Studio Code...)打开或导入 📁friendlyeats-web 目录。此目录包含此 Codelab 的起始代码,其中包含一个目前还无法正常运行的餐馆推荐应用。我们将通过此 Codelab 使其能够正常运行,因此您需要尽快修改该目录中的代码。

4. 安装 Firebase 命令行界面

借助 Firebase 命令行界面 (CLI),您可以在本地提供 Web 应用,并将您的 Web 应用部署到 Firebase Hosting。

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

确保 Firebase CLI 版本为 7.4.0 或更高版本。

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

我们已设置 Web 应用模板,以便从应用的本地目录和文件中提取应用的 Firebase Hosting 配置。为此,我们需要将您的应用与 Firebase 项目相关联。

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

如果您有多个环境(生产、预演等),别名会非常有用。不过,在此 Codelab 中,我们直接使用 default 这个别名。

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

5. 运行本地服务器

我们已准备好真正开始处理应用了!首先,我们在本地运行应用!

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

我们使用 Firebase Hosting 模拟器在本地提供应用。Web 应用现在应可通过 http://localhost:5000 访问。

  1. 通过 http://localhost:5000 打开您的应用。

您应该会看到已与您的 Firebase 项目关联的 FriendlyEats 副本。

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

img2.png

6. 将数据写入 Cloud Firestore

在本部分中,我们将一些数据写入 Cloud Firestore,以填充应用的界面。此操作可通过 Firebase 控制台手动完成,但我们会在应用内执行此操作,以演示基本的 Cloud Firestore 写入过程。

数据模型

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

img3.png

之后,我们会将每条评价存储在各餐馆下名为 ratings 的子集合中。

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. 点击添加 Mock 数据

该应用会自动生成一组随机的餐馆对象,然后调用 addRestaurant 函数。不过,您不会在实际 Web 应用中看到数据,因为我们还需要实施数据检索(在 Codelab 的下一部分中实施)。

不过,如果您转到 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 家餐馆,并按平均评分(当前均为零)排序。声明此查询后,我们会将其传递给负责加载和呈现数据的 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。在本示例中,我们将调用一个从界面中移除餐馆的函数。

现在,我们已实现这两个方法,接下来需要刷新应用,并验证我们之前在 Firebase 控制台中看到的餐馆现在是否可以在应用中显示。如果您成功完成了这一部分,您的应用现在将使用 Cloud Firestore 读取和写入数据!

随着餐馆列表的变化,此监听器会自动更新。请尝试转到 Firebase 控制台并手动删除一家餐馆或更改其名称,您会发现相应的更改会立即显示在您的网站上!

img5.png

8. Get() data

到目前为止,我们演示了如何使用 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 的餐馆。

在我们的应用中,用户可以串连多个过滤条件来创建特定查询,例如“Pizza in San Francisco”或“Seafood in Los Angeles ordered by Popularity”。

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

  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/project-id/database/firestore/indexes?create_composite=...

之所以会出现这些错误,是因为 Cloud Firestore 要求为大多数复合查询建立索引。要求为查询建立索引可确保 Cloud Firestore 大规模地快速运行。

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

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 控制台的构建部分中,点击 Firestore 数据库
  2. 点击 Cloud Firestore 部分中的规则标签页(或点击此处直接前往标签页)。
  3. 将默认设置替换为以下规则,然后点击发布

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,请参阅以下资源:

14. [可选] 使用 App Check 强制执行

Firebase App Check 有助于验证并防止发送到您的应用的无效流量,从而提供保护。在此步骤中,您将使用 reCAPTCHA Enterprise 添加 App Check,从而保护对服务的访问。

首先,您需要启用 App Check 和 reCAPTCHA。

启用 reCAPTCHA Enterprise

  1. 在 Cloud 控制台中,在“安全性”下找到并选择 reCaptcha Enterprise
  2. 按照提示启用该服务,然后点击创建密钥
  3. 按照提示输入显示名称,然后选择网站作为平台类型。
  4. 将部署的网址添加到网域列表,并确保选中“使用复选框验证”选项处于未选中状态。
  5. 点击创建密��,然后将生成的密钥存储在安全的地方。您将在本步骤的后面部分需要它。

启用 App Check

  1. 在 Firebase 控制台中,找到左侧面板中的构建部分。
  2. 点击 App Check,然后点击 Get Started 按钮(或直接重定向到控制台)。
  3. 点击注册,并在出现提示时输入您的 reCAPTCHA Enterprise 密钥,然后点击保存
  4. 在 API 视图中,选择存储,然后点击强制执行。对 Cloud Firestore 执行相同的操作。

现在,应强制执行 App Check!刷新您的应用,然后尝试创建/查看餐馆。您应该会收到以下错误消息:

Uncaught Error in snapshot listener: FirebaseError: [code=permission-denied]: Missing or insufficient permissions.

这意味着 App Check 默认会屏蔽未经验证的请求。现在,让我们为您的应用添加验证。

前往 FriendlyEats.View.js 文件,更新 initAppCheck 函数并添加 reCaptcha 密钥以初始化 App Check。

FriendlyEats.prototype.initAppCheck = function() {
    var appCheck = firebase.appCheck();
    appCheck.activate(
    new firebase.appCheck.ReCaptchaEnterpriseProvider(
      /* reCAPTCHA Enterprise site key */
    ),
    true // Set to true to allow auto-refresh.
  );
};

appCheck 实例使用包含您的密钥的 ReCaptchaEnterpriseProvider 进行初始化,isTokenAutoRefreshEnabled 允许令牌在您的应用中自动刷新。

如需启用本地测试,请在 FriendlyEats.js 文件中找到用于初始化应用的部分,然后将以下代码行添加到 FriendlyEats.prototype.initAppCheck 函数:

if(isLocalhost) {
  self.FIREBASE_APPCHECK_DEBUG_TOKEN = true;
}

这会在本地 Web 应用的控制台中记录一个调试令牌,类似于以下内容:

App Check debug token: 8DBDF614-649D-4D22-B0A3-6D489412838B. You will need to add it to your app's App Check settings in the Firebase console for it to work.

现在,前往 Firebase 控制台中 App Check 的应用视图

点击溢出菜单,然后选择管理调试令牌

然后,点击添加调试令牌,并按照提示粘贴控制台中的调试令牌。

恭喜!App Check 现在应该可以在您的应用中运行了。