React-和-Firebase-无服务器-Web-应用-全-

React 和 Firebase 无服务器 Web 应用(全)

原文:zh.annas-archive.org/md5/330929BAB4D0F44DAFAC93D065193C41

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

实时应用程序多年来一直主导着 Web 应用程序领域。实时不仅仅限于在数据可用时立即显示数据;当与交互式体验一起使用时,它展现出真正的力量,用户和系统可以立即相互通信。借助虚拟 DOM 和声明性视图等功能,React 被证明更适合这样的实时应用程序。Firebase 通过让您专注于应用程序的行为和外观,而不会陷入实时开发的更繁琐的部分,使构建和快速原型设计这种应用程序变得更简单。

本书将涵盖 Firebase 的功能,如云存储、云功能、托管和实时数据库集成 React,以开发丰富、协作、实时的应用程序,仅使用客户端代码。我们还可以看到如何使用 Firebase 身份验证和数据库安全规则来保护我们的应用程序。我们还利用 Redux 的力量来组织前端的数据。Redux 试图通过对状态变化施加一定的限制,使状态变化可预测。在本书的最后,您将通过认识 Firebase 的潜力来提高您的 React 技能,从而创建实时无服务器 Web 应用程序。

本书提供的是更多实用的见解,而不仅仅是理论概念,并包括从 hello world 到实时 Web 应用程序的基础到高级示例。

本书的受众

本书的理念是帮助开发人员使用 React 和 Firebase 更快地创建实时无服务器应用程序。我们为那些想要使用 Firebase 验证业务理念创建最小可行产品(MVP)的开发人员编写了本书。本书旨在为那些具有 HTML、CSS、React 和 JavaScript 基础到中级知识的开发人员提供实用知识,并希望了解更多关于 React、Redux 和 Firebase 集成的内容。本书还面向那些不想浪费时间搜索数百个 React、Redux 和 Firebase 教程的开发人员,并希望在一个地方拥有真实示例,快速提高生产力。本书适合任何对学习 Firebase 感兴趣的人。

最后,如果您想开发无服务器应用程序,并想了解从设计到托管的端到端过程,并获得逐步说明,那么本书适合您。

要充分利用本书

您应该具有 React、HTML、CSS 和 JavaScript 的基本编程经验,才能有利于阅读本书。假定您已经知道Node Package Managernpm)如何工作以安装任何依赖项,并且对 ES6 语法有基本了解。

下载示例代码文件

您可以从www.packtpub.com的帐户中下载本书的示例代码文件。如果您在其他地方购买了本书,可以访问www.packtpub.com/support并注册,以便文件直接发送到您的邮箱。

您可以按照以下步骤下载代码文件:

  1. www.packtpub.com上登录或注册。

  2. 选择“SUPPORT”选项卡。

  3. 单击“代码下载和勘误”。

  4. 在搜索框中输入书名,然后按照屏幕上的说明操作。

下载文件后,请确保使用最新版本的解压软件解压文件夹:

  • WinRAR/7-Zip 适用于 Windows

  • Zipeg/iZip/UnRarX 适用于 Mac

  • 7-Zip/PeaZip 适用于 Linux

该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Serverless-Web-Applications-with-React-and-Firebase。如果代码有更新,将在现有的 GitHub 存储库上进行更新。

我们还有来自我们丰富的书籍和视频目录的其他代码包,可在github.com/PacktPublishing/上找到。去看看吧!

使用的约定

本书中使用了许多文本约定。

CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。这是一个例子:"将下载的WebStorm-10*.dmg磁盘映像文件挂载为系统中的另一个磁盘。"

代码块设置如下:

constructor(props) {
 super(props);
 this.state = {
 value: props.initialValue
 };
 }

任何命令行输入或输出都以以下形式书写:

node -v

粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词会以这种形式出现在文本中。这是一个例子:"从管理面板中选择系统信息。"

警告或重要说明会出现在这样的形式中。提示和技巧会出现在这样的形式中。

联系我们

我们随时欢迎读者的反馈。

一般反馈:发送电子邮件至feedback@packtpub.com,并在主题中提及书名。如果您对本书的任何方面有疑问,请通过questions@packtpub.com与我们联系。

勘误表:尽管我们已经尽最大努力确保内容的准确性,但错误是难免的。如果您在本书中发现了错误,我们将不胜感激地接受您的报告。请访问www.packtpub.com/submit-errata,选择您的书籍,点击“勘误提交表格”链接,并输入详细信息。

盗版:如果您在互联网上发现我们作品的任何非法副本,请向我们提供位置地址或网站名称,我们将不胜感激。请通过copyright@packtpub.com与我们联系,并附上材料链接。

如果您有兴趣成为作者:如果您在某个专业领域有专长,并且有兴趣撰写或为一本书做出贡献,请访问authors.packtpub.com

评论

请留下评论。阅读并使用本书后,为什么不在购买它的网站上留下评论呢?潜在读者可以看到并使用您的客观意见来做出购买决定,我们在 Packt 可以了解您对我们产品的看法,我们的作者也可以看到您对他们书籍的反馈。谢谢!

有关 Packt 的更多信息,请访问packtpub.com

第一章:使用 Firebase 和 React 入门

实时 Web 应用程序被认为包括对用户的超快速响应的好处,并且具有高度的互动性,这增加了用户的参与度。在现代 Web 中,有许多可用于开发实时应用程序的框架和工具。JavaScript 是用于构建 Web 应用程序的最流行的脚本语言之一。本书向您介绍了 ReactJS 和 Firebase,这两者在您学习现代 Web 应用程序开发时可能会遇到。它们都用于构建快速、可扩展和实时的用户界面,这些界面使用数据,并且可以随时间变化而无需重新加载页面。

React 以模型-视图-控制器MVC)模式中的视图而闻名,并且可以与其他 JavaScript 库或框架一起在 MVC 中使用。为了管理 React 应用程序中的数据流,我们可以使用 Flux 或 Redux。在本书中,我们还将介绍如何将 redux 与 React 和 firebase 应用程序实现。

Redux 是 Flux 的替代品。它具有相同的关键优势。Redux 与 React 配合特别好,用于管理 UI 的状态。如果你曾经使用过 flux,那么使用 Redux 也很容易。

在开始编码之前,让我们复习一下 ReactJS 的知识,并了解我们可以如何使用 Firebase 及其功能,以了解 Firebase 的强大功能。

以下是本节中我们将涵盖的主题列表:

  • React 简介

  • React 组件生命周期

这将让您更好地理解处理 React 组件。

React

React 是一个开源的 JavaScript 库,提供了一个视图层,用于将数据呈现为 HTML,以创建交互式 UI 组件。组件通常用于呈现包含自定义 HTML 标记的其他组件的 React 视图。当数据发生变化时,React 视图会高效地更新和重新呈现组件,而无需重新加载页面。它为您提供了一个虚拟 DOM,强大的视图而无需模板,单向数据流和显式突变。这是一种非常系统化的方式,在数据发生变化时更新 HTML 文档,并在现代单页面应用程序中提供了组件的清晰分离。

React 组件完全由 Javascript 构建,因此很容易通过应用程序传递丰富的数据。在 React 中创建组件可以将 UI 分割为可重用和独立的部分,这使得您的应用程序组件可重用、可测试,并且易于关注点分离。

React 只关注 MVC 中的视图,但它也有有状态的组件,它记住了this.state中的所有内容。它处理从输入到状态更改的映射,并渲染组件。让我们来看看 React 的组件生命周期及其不同的级别。

组件生命周期

在 React 中,每个组件都有自己的生命周期方法。每个方法都可以根据您的要求进行重写。

当数据发生变化时,React 会自动检测变化并重新渲染组件。此外,我们可以在错误处理阶段捕获错误。

以下图片显示了 React 组件的各个阶段:

方法信息

让我们快速看一下前面的方法。

constructor()方法

当组件挂载时,React 组件的构造函数首先被调用。在这里,我们可以设置组件的状态。

这是一个在React.Component中的构造函数示例:

constructor(props) {
 super(props);
 this.state = {
 value: props.initialValue
 };
 }

在构造函数中使用this.props,我们需要调用super(props)来访问和调用父级的函数;否则,你会在构造函数中得到this.props未定义,因为 React 在调用构造函数后立即从外部设置实例上的.props,但当你在 render 方法中使用this.props时,它不会受到影响。

render()方法

render()方法是必需的,用于渲染 UI 组件并检查this.propsthis.state,并返回以下类型之一:

  • React 元素

  • 字符串和数字

  • 门户

  • null

  • 布尔值

componentWillMount()方法

此方法在componentDidMount之前立即调用。它在render()方法之前触发。

componentDidMount()方法

此方法在组件挂载后立即调用。我们可以使用此方法从远程端点加载数据以实例化网络请求。

componentWillReceiveProps()方法

当挂载的组件接收到新的 props 时,将调用此方法。此方法还允许比较当前值和下一个值,以确保 props 的更改。

shouldComponentUpdate()方法

shouldComponentUpdate()方法在组件接收到新的 props 和 state 时被调用。默认值是true;如果返回false,React 会跳过组件的更新。

componentWillUpdate()方法

componentWillUpdate()方法在渲染之前立即被调用,当接收到新的 prop 或 state 时。我们可以使用这个方法在组件更新之前执行操作。

如果shouldComponentUpdate()返回false,这个方法将不会被调用。

componentDidUpdate()方法

componentDidUpdate()方法在组件更新后立即被调用。这个方法不会在初始渲染时被调用。

类似于componentWillUpdate(),如果shouldComponentUpdate()返回 false,这个方法也不会被调用。

componentWillUnmount()方法

这个方法在 React 组件被卸载和销毁之前立即被调用。在这里,我们可以执行任何必要的清理,比如取消网络请求或清理在componentDidMount中创建的任何订阅。

componentDidCatch()方法

这个方法允许我们在 React 组件中捕获 JavaScript 错误。我们可以记录这些错误,并显示另一个备用 UI,而不是崩溃的组件树。

现在我们对 React 组件中可用的组件方法有了清晰的了解。

观察以下 JavaScript 代码片段:

<section>
<h2>My First Example</h2>
</section>
<script>
 var root = document.querySelector('section').createShadowRoot();
 root.innerHTML = '<style>h2{ color: red; }</style>' +'<h2>Hello World!</h2>';
</script>

现在,观察以下 ReactJS 代码片段:

var sectionStyle = {
 color: 'red'
};
var MyFirstExample = React.createClass({
render: function() {
 return (<section><h2 style={sectionStyle}>
 Hello World!</h2></section>
 )}
})
ReactDOM.render(<MyFirstExample />, renderedNode);

现在,在观察了前面的 React 和 JavaScript 示例之后,我们将对普通 HTML 封装和 ReactJS 自定义 HTML 标签有一个清晰的了解。

React 不是一个 MVC 框架;它是一个用于构建可组合用户界面和可重用组件的库。React 在 Facebook 的生产阶段使用,并且instagram.com完全基于 React 构建。

Firebase

Firebase 平台帮助您开发高质量的应用程序并专注于用户。

Firebase 是由 Google 支持的移动和 Web 应用程序开发平台。它是开发高质量移动和 Web 应用程序的一站式解决方案。它包括各种产品,如实时数据库、崩溃报告、云 Firestore、云存储、云功能、身份验证、托管、Android 测试实验室和 iOS 性能监控,可以用来开发和测试实时应用程序,专注于用户需求,而不是技术复杂性。

它还包括产品,如云消息传递、Google 分析、动态链接、远程配置、邀请、应用索引、AdMob 和 AdWords,这些产品可以帮助您扩大用户群体,同时增加受众的参与度。

Firebase 提供多个 Firebase 服务。我们可以使用 Firebase 命名空间访问每个服务:

  • firebase.auth() - 认证

  • firebase.storage() - 云存储

  • firebase.database() - 实时数据库

  • firebase.firestore() - 云 Firestore

我们将在接下来的章节中涵盖所有前述的服务。在本章中,我们将简要地介绍前述产品/服务,以便对 Firebase 平台的所有功能有一个基本的了解。在接下来的章节中,我们将更详细地探索可以与 React 平台集成的与 web 相关的产品。

以下是我们将在本节中涵盖的主题列表:

  • Firebase 及其功能简介

  • Firebase 功能列表以及如何使用它

  • 云 Firestore

  • 使用 JavaScript 设置 Firebase 项目

  • 使用 Firebase 和 JavaScript 创建“Hello World”示例应用程序

正如您所看到的,Firebase 提供了两种类型的云数据库和实时数据库,两者都支持实时数据同步。我们可以在同一个应用程序或项目中同时使用它们。好的,让我们深入了解并了解更多。

实时数据库

对于任何实时应用程序,我们都需要一个实时数据库。Firebase 实时数据库是一个云托管的 NoSQL 数据库,可以将数据实时同步到每个连接的客户端。Firebase 数据库使用同步机制,而不是典型的请求-响应模型,它可以在毫秒内将数据同步到所有连接的设备上。另一个关键功能是它的离线功能。Firebase SDK 将数据持久保存在磁盘上;因此,即使用户失去互联网连接,应用程序仍然可以响应。一旦重新建立连接,它会自动同步数据。它支持 iOS、Android、Web、C++和 Unity 平台。我们将在接下来的章节中详细介绍这一点。

Firebase 实时数据库可以在单个数据库中支持约 100,000 个并发连接和每秒 1,000 次写入。

以下屏幕截图显示了左侧 Firebase 中可用的功能列表,我们已经在数据库部分选择了实时数据库。在该部分,我们有四个选项卡可用:

  • 数据

  • 规则

  • 备份

  • 用法

数据库规则

Firebase 数据库规则是保护数据的唯一方法。Firebase 为开发人员提供了灵活性和基于表达式的规则语言,具有类似 JavaScript 的语法,用于定义数据的结构、索引方式以及用户何时可以读取和写入数据。您还可以将身份验证服务与此结合,以定义谁可以访问哪些数据,并保护用户免受未经授权的访问。为了验证数据,我们需要在规则中使用.validate来单独添加规则。

考虑以下示例:

{
"rules": {
".write": true,
"ticket": {
// a valid ticket must have attributes "email" and "status"
".validate": "newData.hasChildren(['email', 'status'])",
"status": {
// the value of "status" must be a string and length greater then 0 and less then 10
".validate": "newData.isString() && newData.val().length > 0 && newData.val().length < 10"
},
"email": {
// the value of "email" must valid with "@"
".validate": "newData.val().contains('@')"
}
}
}
}

以下是在“规则”选项卡中应用规则的其他示例代码块:

默认:身份验证的规则配置:

{
 "rules": {
 ".read": "auth != null",
 ".write": "auth != null"
 }}

公共:这些规则允许每个人完全访问,即使是您应用的非用户。它们允许读取和写入数据库:

{
 "rules": {
 ".read": true,
 ".write": true
 }}

用户:这些规则授权访问与 Firebase 身份验证令牌中用户 ID 匹配的节点:

{
 "rules": {
   "users": {
       "$uid": {
             ".read": "$uid === auth.uid",
             ".write": "$uid === auth.uid"
         }
       }
    }
}

私有:这些规则配置不允许任何人读取和写入数据库:

{
 "rules": {
    ".read": false,
    ".write": false
  }
}

我们还可以使用 Firebase 秘钥代码的 REST API 来通过向/.settings/rules.json路径发出PUT请求来编写和更新 Firebase 应用的规则,并且它将覆盖现有规则。

例如,curl -X PUT -d '{ "rules": { ".read": true } }' 'https://docs-examples.firebaseio.com/.settings/rules.json?auth=FIREBASE_SECRET'

备份

Firebase 允许我们保存数据库的每日备份,但这仅在 Blaze 计划中可用。它还会自动应用安全规则以保护您的数据。

用法

Firebase 允许通过分析图表查看数据库的使用情况。它实时显示了我们的 Firebase 数据库中的连接、存储、下载和负载:

Cloud Firestore

Cloud Firestore 也是一种云托管的 NoSQL 数据库。您可能会认为我们已经有了实时数据库,它也是一种 NoSQL 数据库,那么为什么我们需要 Firestore 呢?对这个问题的答案是,Firestore 可以被视为提供实时同步和离线支持以及高效数据查询的实时数据库的高级版本。它可以全球扩展,并让您专注于开发应用,而不必担心服务器管理。它可以与 Android、iOS 和 Web 平台一起使用。

我们可以在同一个 Firebase 应用程序或项目中使用这两个数据库。两者都是 NoSQL 数据库,可以存储相同类型的数据,并且具有类似方式工作的客户端库。

如果您想在云 Firestore 处于测试版时尝试它,请使用我们的指南开始使用:

  • 转到console.firebase.google.com/

  • 选择您的项目,DemoProject

  • 单击左侧导航栏中的数据库,然后选择 Cloud Firestore 数据库:

一旦我们选择数据库,它会提示您在创建数据库之前应用安全规则。

安全规则

在 Cloud Firestore 中创建数据库和集合之前,它会提示您为我们的数据库应用安全规则。

看一下以下的截图:

以下是 Firestore 规则的一些代码示例:

公共

service cloud.firestore {
    match /databases/{database}/documents {
           match /{document=**} {
           allow read, write;
        }
    }
}

用户

service cloud.firestore {
    match /databases/{database}/documents {
        match /users/{userId} {
           allow read, write: if request.auth.uid == userId;
        }
    }
}

私有

service cloud.firestore {
    match /databases/{database}/documents {
       match /{document=**} {
          allow read, write: if false;
       }
    }
}

实时数据库和云 Firestore 之间的区别

我们已经看到实时数据库和云 Firestore 都是具有实时数据同步功能的 NoSQL 数据库。因此,让我们根据功能来看看它们之间的区别。

数据模型

这两个数据库都是云托管的 NoSQL 数据库,但两个数据库的数据模型是不同的:

实时数据库 云 Firestore

|

  • 简单的数据非常容易存储。

  • 复杂的分层数据在规模上更难组织。

|

  • 简单的数据很容易存储在类似 JSON 的文档中。

  • 使用子集合在文档中更容易地组织复杂和分层数据。

  • 需要较少的去规范化和数据扁平化。

|

实时和离线支持

两者都具有面向移动端的实时 SDK,并且都支持本地数据存储,以便离线就绪的应用程序:

实时数据库 云 Firestore
仅 iOS 和 Android 移动客户端的离线支持。 iOS、Android 和 Web 客户端的离线支持。

查询

通过查询从任一数据库中检索、排序和过滤数据:

实时数据库 云 Firestore

| 具有有限排序和过滤功能的深度查询:

  • 您只能在一个属性上进行排序或过滤,而不能在一个属性上进行排序和过滤。

  • 查询默认是深度的。它们总是返回整个子树。

| 具有复合排序和过滤的索引查询:

  • 您可以在单个查询中链接过滤器并结合过滤和对属性进行排序。

  • 为子集合编写浅层查询;您可以查询文档内的子集合,而不是整个集合,甚至是整个文档。

  • 查询默认进行索引。查询性能与结果集的大小成正比,而不是数据集的大小。

|

可靠性和性能

当我们为项目选择数据库时,可靠性和性能是我们首先考虑的最重要部分:

Realtime Database Cloud Firestore

| Realtime Database 是一个成熟的产品:

  • 您可以期望从经过严格测试和验证的产品中获得的稳定性。

  • 延迟非常低,因此非常适合频繁的状态同步。

  • 数据库仅限于单个区域的区域可用性。

| Cloud Firestore 目前处于 beta 版:

  • 在 beta 产品中的稳定性并不总是与完全推出的产品相同。

  • 将您的数据存储在不同地区的多个数据中心,确保全球可扩展性和强大的可靠性。

  • 当 Cloud Firestore 从 beta 版毕业时,它的可靠性将比 Realtime Database 更强。

|

可扩展性

当我们开发大规模应用程序时,我们必须知道我们的数据库可以扩展到多大程度:

Realtime Database Cloud Firestore
扩展需要分片:在单个数据库中扩展到大约 100,000 个并发连接和每秒 1,000 次写入。超出这一范围需要在多个数据库之间共享数据。 扩展将是自动的:完全自动扩展(在 beta 版之后),这意味着您不需要在多个实例之间共享数据。

安全性

就安全性而言,每个数据库都有不同的方式来保护数据免受未经授权的用户访问:

来源firebase.google.com/docs/firestore/rtdb-vs-firestore?authuser=0

Realtime Database Cloud Firestore

| 需要单独验证的级联规则。

  • Firebase 数据库规则是唯一的安全选项。

  • 读写规则会级联。

  • 您需要使用.validate在规则中单独验证数据。

| 更简单,更强大的移动端、Web 端和服务器端 SDK 安全性。

  • 移动端和 Web 端 SDK 使用 Cloud Firestore 安全规则,服务器端 SDK 使用身份和访问管理IAM)。

  • 除非使用通配符,否则规则不会级联。

  • 数据验证会自动进行。

  • 规则可以限制查询;如果查询结果可能包含用户无权访问的数据,则整个查询将失败。

|

截至目前,Cloud Firestore 仅提供测试版;因此,在本书中,我们只关注实时数据库。

崩溃报告

崩溃报告服务可帮助您诊断 Android 和 iOS 移动应用中的问题。它会生成详细的错误和崩溃报告,并将其发送到配置的电子邮件地址,以便快速通知问题。它还提供了一个丰富的仪表板,您可以在其中监视应用的整体健康状况。

身份验证

Firebase 身份验证提供了一个简单而安全的解决方案,用于管理移动和 Web 应用的用户身份验证。它提供多种身份验证方法,包括使用电子邮件和密码进行传统的基于表单的身份验证,使用 Facebook 或 Twitter 等第三方提供商,以及直接使用现有的帐户系统。

用于 Web 的 FirebaseUI 身份验证

Firebase UI 是完全开源的,并且可以轻松定制以适应您的应用程序,其中包括一些库。它允许您快速将 UI 元素连接到 Firebase 数据库以进行数据存储,允许视图实时更新,并且还提供了用于常见任务的简单接口,例如显示项目列表或集合。

FirebaseUI Auth 是在 Firebase 应用程序中添加身份验证的推荐方法,或者我们可以使用 Firebase 身份验证 SDK 手动执行。它允许用户为使用电子邮件和密码、电话号码以及包括 Google 和 Facebook 登录在内的最流行的身份提供者添加完整的 UI 流程。

FirebaseUI 可在opensource.google.com/projects/firebaseui上找到。

我们将在接下来的章节中详细探讨身份验证。

云函数

云函数允许您拥有无服务器应用程序;您可以在没有服务器的情况下运行自定义应用程序后端逻辑。您的自定义函数可以在特定事件上执行,这些事件可以通过集成以下 Firebase 产品来触发:

  • Cloud Firestore 触发器

  • 实时数据库触发器

  • Firebase 身份验证触发器

  • Firebase 的 Google Analytics 触发器

  • 云存储触发器

  • 云 Pub/Sub 触发器

  • HTTP 触发器

它是如何工作的?

一旦编写并部署函数,Google 的服务器立即开始监听这些函数,即监听事件并在触发时运行函数。随着应用程序的负载增加或减少,它会通过快速扩展所需的虚拟服务器实例数量来响应。如果函数被删除、空闲或由您更新,那么实例将被清理并替换为新实例。在删除的情况下,它还会删除函数与事件提供者之间的连接。

这里列出了云函数支持的事件:

  • onWrite(): 当实时数据库中的数据被创建、销毁或更改时触发

  • onCreate(): 当实时数据库中创建新数据时触发

  • onUpdate(): 当实时数据库中的数据更新时触发

  • onDelete(): 当实时数据库中的数据被删除时触发

这是一个云函数makeUppercase的代码示例:

exports.makeUppercase = functions.database.ref('/messages/{pushId}/original')
 .onWrite(event => {
 // Grab the current value of what was written to the Realtime Database.
 const original = event.data.val();
 console.log('Uppercasing', event.params.pushId, original);
 const uppercase = original.toUpperCase();
 // You must return a Promise when performing asynchronous tasks inside a Functions such as
 // writing to the Firebase Realtime Database.
 // Setting an "uppercase" sibling in the Realtime Database returns a Promise.
 return event.data.ref.parent.child('uppercase').set(uppercase);
 });

编写云函数后,我们还可以测试和监视我们的函数。

云存储

任何移动应用或 Web 应用都需要一个存储空间,以安全且可扩展的方式存储用户生成的内容,如文档、照片或视频。云存储是根据相同的要求设计的,并帮助您轻松存储和提供用户生成的内容。它提供了一个强大的流媒体机制,以获得最佳的最终用户体验。

以下是我们如何配置 Firebase 云存储:

// Configuration for your app
 // TODO: Replace with your project's config object
 var config = {
 apiKey: '<your-api-key>',
 authDomain: '<your-auth-domain>',
 databaseURL: '<your-database-url>',
 storageBucket: '<your-storage-bucket>'
 };
 firebase.initializeApp(config);
  // Get a reference to the storage service
 var storage = firebase.storage();
// Points to the root reference  var storageRef = storage.ref(); // Points to 'images'  var imagesRef = storageRef.child('images');  // Points to 'images/sprite.jpg'  // Note that you can use variables to create child values  var fileName =  'sprite.jpg';  var spaceRef = imagesRef.child(fileName);  // File path is 'images/sprite.jpg'  var path = spaceRef.fullPath // File name is 'sprite.jpg'  var name = spaceRef.name // Points to 'images'  var imagesRef = spaceRef.parent; 

reference.fullPath的总长度必须在 1 到 1,024 字节之间,不能包含回车或换行字符。

避免使用#、[、]、*或?,因为这些在其他工具(如 Firebase 实时数据库)中效果不佳。

托管

Firebase 提供了一个托管服务,您可以通过简单的命令轻松部署您的 Web 应用和静态内容。您的 Web 内容将部署在全球交付网络GDN)上,因此无论最终用户的位置如何,都可以快速交付。它为您的域名提供免费的 SSL,以通过安全连接提供内容。它还提供完整的版本控制和一键回滚的发布管理。

Android 的测试实验室

我们使用不同的 Android API 版本在各种设备上测试我们的 Android 应用程序,以确保最终用户可以在任何 Android 设备上使用我们的应用程序而不会出现任何问题。但是,很难让所有不同的设备都可供测试团队使用。为了克服这些问题,我们可以使用 Test Lab,它提供了云托管基础设施,以便使用各种设备测试应用程序。它还可以轻松收集带有日志、视频和截图的测试结果。它还会自动测试您的应用程序,以识别可能的崩溃。

性能监控

Firebase 性能监控专门为 iOS 应用程序的性能测试而设计。您可以使用性能跟踪轻松识别应用程序的性能瓶颈。它还提供了一个自动化环境来监视 HTTP 请求,有助于识别网络问题。性能跟踪和网络数据可以更好地了解您的应用程序的性能。

以下产品类别用于增加用户群体并更好地吸引他们。

Google Analytics

Google Analytics 是一个非常知名的产品,我认为没有开发人员需要介绍它。Firebase 的 Google Analytics 是一个免费的分析解决方案,用于衡量用户对您的应用的参与度。它还提供有关应用使用情况的见解。分析报告可以帮助您了解用户行为,因此可以更好地做出关于应用营销和性能优化的决策。您可以根据不同的参数生成报告,例如设备类型、自定义事件、用户位置和其他属性。分析可以配置为 Android、iOS 和 C++和 Unity 应用程序。

云消息传递

任何实时应用程序都需要发送实时通知。Firebase Cloud Messaging(FCM)提供了一个平台,帮助您实时向应用用户发送消息和通知。您可以免费在不同平台上发送数百亿条消息:Android、iOS 和 Web。我们还可以安排消息的交付 - 立即或在将来。通知消息与 Firebase Analytics 集成,因此无需编码即可监控用户参与度。

以下浏览器支持服务工作者:

  • Chrome:50+

  • Firefox:44+

  • Opera Mobile:37+

// Retrieve Firebase Messaging object.
const messaging = firebase.messaging();
messaging.requestPermission()
.then(function() {
 console.log('Notification permission granted.');
 // Retrieve the Instance ID token for use with FCM.
 // ...
})
.catch(function(err) {
 console.log('Unable to get permission to notify.', err);
});

FCM SDK 仅在 HTTPS 页面上受支持,因为服务工作者仅在 HTTPS 站点上可用。

动态链接

动态链接是帮助您将用户重定向到移动应用程序或 Web 应用程序中特定内容位置的 URL。如果用户在桌面浏览器中打开动态链接,将打开相应的网页,但如果用户在 Android 或 iOS 中打开它,用户将被重定向到 Android 或 iOS 中的相应位置。此外,动态链接在应用之间起作用;如果应用尚未安装,用户将被提示安装应用。动态链接增加了将移动 Web 用户转化为原生应用用户的机会。动态链接作为在线社交网络活动的一部分也增加了应用的安装,并且永久免费。

远程配置

在不重新部署应用程序到应用商店的情况下更改应用程序的颜色主题有多酷?是的,通过 Firebase 远程配置,可以对应用程序进行即时更改。您可以通过服务器端参数管理应用程序的行为和外观。例如,您可以根据地区为特定的受众提供一定的折扣,而无需重新部署应用程序。

邀请

一般来说,每个人都会向朋友和同事推荐好的应用程序。我们通过复制和粘贴应用链接来做到这一点。然而,由于许多原因,它并不总是有效,例如,链接是针对安卓的,所以 iOS 用户无法打开它。Firebase 邀请使通过电子邮件或短信分享内容或应用推荐变得非常简单。它与 Firebase 动态链接一起工作,为用户提供最佳的平台体验。您可以将动态链接与要分享的内容相关联,Firebase SDK 将为您处理,为您的应用用户提供最佳的用户体验。

应用索引

对于任何应用程序,让应用程序安装以及保留这些用户并进行一些参与同样重要。重新吸引已安装您的应用程序的用户,应用索引是一种方法。通过 Google 搜索集成,您的应用链接将在用户搜索您的应用提供的内容时显示。此外,应用索引还可以帮助您改善 Google 搜索排名,以便在顶部搜索结果和自动完成中显示应用链接。

AdMob

应用开发者的最终目标大多是将其货币化。AdMob 通过应用内广告帮助您实现应用的货币化。您可以有不同类型的广告,比如横幅广告、视频广告,甚至原生广告。它允许您展示来自 AdMob 调解平台或谷歌广告商的广告。AdMob 调解平台具有广告优化策略,旨在最大化您的收入。您还可以查看 AdMob 生成的货币化报告,以制定产品策略。

AdWords

在当今世界,最好的营销策略之一是在线广告。Google AdWords 帮助您通过广告活动吸引潜在客户或应用用户。您可以将您的 Google AdWords 帐户链接到您的 Firebase 项目,以定义特定的目标受众来运行您的广告活动。

现在我们已经了解了 Firebase 平台的所有产品,我们可以混合匹配这些产品来解决常见的开发问题,并在市场上推出最佳产品。

开始使用 Firebase

在我们实际在示例应用程序中使用 Firebase 之前,我们必须通过 Firebase 控制台在console.firebase.google.com/上创建我们的 Firebase 项目。打开此链接将重定向您到 Google 登录页面,您将需要登录到您现有的 Google 帐户或创建一个新的帐户。

一旦您成功登录到 Firebase 控制台,您将看到以下截图所示的仪表板:

我们将通过单击“添加项目”按钮来创建我们的第一个项目。一旦您单击“添加项目”按钮,它将显示一个弹出窗口,询问您的项目名称和组织所在国家。我将其称为DemoProject,将国家设置为美国,然后单击“创建项目”按钮:

项目创建后,您就可以开始了。您将被重定向到项目仪表板,您可以在其中配置您想要在项目中使用的产品/服务:

接下来,我们将看看如何将这个 Firebase 项目集成到 Web 应用程序中。您的 Web 应用程序可以是任何 JavaScript 或 NodeJS 项目。

首先,我们将使用纯 JavaScript 创建一个示例,然后我们将进一步包含 React。

现在,您需要在系统中创建一个名为DemoProject的目录,并在其中创建几个名为imagescssjs(JavaScript)的文件夹,以使您的应用程序易于管理。完成文件夹结构后,它将如下所示:

要将我们的 Firebase 项目集成到 JavaScript 应用程序中,我们需要一个代码片段,必须添加到我们的 JavaScript 代码中。要获取它,请单击“将 Firebase 添加到您的 Web 应用程序”,并注意它生成的初始化代码,它应该看起来像以下代码:

当我们开始使用 ReactJS 或纯 JavaScript 制作应用程序时,我们需要进行一些设置,这仅涉及 HTML 页面并包括一些文件。首先,我们创建一个名为chapter1的目录(文件夹)。在任何代码编辑器中打开它。直接在其中创建一个名为index.html的新文件,并添加以下 HTML5 Boilerplate 代码:

  • 例如,我创建了一个名为DemoProject的文件夹

  • 在文件夹中创建一个名为index.html的文件

  • 在你的 HTML 中,添加我们从 Firebase 控制台复制的代码片段:

我更喜欢并建议您在任何类型的 JavaScript 应用程序开发中使用 Visual Studio 代码编辑器,而不是列出的文本编辑器,因为它具有广泛的功能。

现在,我们需要将 Firebase 代码片段复制到 HTML 中:

<!doctype html>
<html class="no-js" lang="">
<head>
 <meta charset="utf-8">
 <title>Chapter 1</title>
</head>
<body>
 <!--[if lt IE 8]>
<p class="browserupgrade">You are using an
<strong>outdated</strong> browser.
Please <a href="http://browsehappy.com/">
upgrade your browser</a> to improve your
experience.</p>
<![endif]-->
 <!-- Add your site or application content here -->
 <p>Hello world! This is HTML5 Boilerplate.</p>
 <script src="https://www.gstatic.com/firebasejs/4.6.1/firebase.js"></script>
 <script>
 // Initialize Firebase
 var config = {
 apiKey: "<PROJECT API KEY>",
 authDomain: "<PROJECT AUTH DOMAIN>",
 databaseURL: "<PROJECT DATABASE AUTH URL>",
 projectId: "<PROJECT ID>",
 storageBucket: "",
 messagingSenderId: "<MESSANGING ID>"
 };
 firebase.initializeApp(config);
 </script>
</body>
</html>

以下显示了我们数据库中的数据,我们将使用 JavaScript 获取并在 UI 上显示:

//HTML Code to show the message
<p id="message">Hello world! This is HTML5 Boilerplate.</p>
<script>
//Firebase script to get the value from database and replace the "message".
var messageLabel = document.getElementById('message');
 var db = firebase.database();
 db.ref().on("value", function(snapshot) {
 console.log(snapshot.val());
 var object = snapshot.val();
 messageLabel.innerHTML = object.chapter1.example;
 });
</script>

在上述代码中,我们使用on()方法来检索数据。它以value作为事件类型,然后检索数据的快照。当我们向快照添加val()方法时,我们将获得要显示在messageField中的数据。

让我简要介绍一下 Firebase 中可用的事件,我们可以用它来读取数据。

就目前而言,在数据库规则中,我们允许任何人读取和写入数据库中的数据;否则,它会显示权限被拒绝的错误。将其视为一个例子:

{

   "rules": {

      ".read": true,

     ".write": true

    }

}

Firebase 事件

如果您可以看到前面的代码,我们已经使用了接收 DataSnapshot 的回调函数,该 DataSnapshot 保存了快照的数据。快照是数据库引用位置在某个特定时间点的数据的图片,如果在引用位置不存在数据,则快照的值返回 null。

value

最近,我们已经使用了这个宝贵的事件来读取实时数据库中的数据。每当数据发生变化时,都会触发此事件类型,并且回调函数将检索所有数据,包括子数据。

child_added

每当我们需要检索项目对象列表时,此事件类型将被触发一次,并且每当新对象被添加到我们的数据给定路径时都会触发。与value不同,它返回该位置的整个对象,此事件回调作为包含两个参数的快照传递,其中包括新子项和先前子项数据。

例如,如果您想在博客应用程序中的每次添加新评论时检索数据,可以使用child_added

child_changed

当任何子对象更改时,将触发child_changed事件。

child_removed

当立即子项被移除时,将触发child_removed事件。它通常与child_addedchild_changed结合使用。此事件回调包含已移除子项的数据。

child_moved

当您使用有序数据(如列表项的拖放)时,将触发child_moved事件。

现在,让我们快速查看一下我们的完整代码:

<!doctype html> <html  class="no-js"  lang=""> <head> <meta  charset="utf-8"> <title>Chapter 1</title><script  src="</span>https://www.gstatic.com/firebasejs/4.6.1/firebase.js"></script> </head> <body><!--[if lt IE 8]> <p class="browserupgrade">You are using an<strong>outdated</strong> browser.Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve yourexperience.
</p> <![endif]--> <!-- Add your site or application content here -->
<p  id="message">Hello world! This is HTML5 Boilerplate.</p> <script> // Initialize Firebase var  config  =  {
 apiKey: "<PROJECT API KEY>",
 authDomain: "<PROJECT AUTH DOMAIN>",
 databaseURL: "<PROJECT DATABASE AUTH URL>",
 projectId: "<PROJECT ID>",
 storageBucket: "",
 messagingSenderId: "<MESSANGING ID>"  }; firebase.initializeApp(config); var  messageLabel  =  document.getElementById('message'); var  db  =  firebase.database(); db.ref().on("value",  function(snapshot)  {
 console.log(snapshot.val());
 var object  =  snapshot.val();
 messageLabel.innerHTML  =  object.chapter1.example; });</script> </body> </html>

现在,在浏览器中打开index.html,让我们看一下结果:

在上面的屏幕摘录中,我们可以看到MessageLabel上的数据库值和浏览器控制台中的 JavaScript 数据表示。

让我们通过从用户那里获取输入值并将这些值保存在数据库中来进一步扩展此示例。然后,使用事件,我们将在实时中在浏览器中显示这些消息:

如图所示,我在数据库中添加了一个子节点messages。现在,我们将在我们的 HTML 中添加表单输入和保存按钮,并在底部在实时中显示用户提交的消息列表。

这是 HTML 代码:

<input type="text" id="messageInput" />
 <button type="button" onclick="addData()">Send message</button>
<h2>Messages</h2>
 <p id="list">sdfdf</p>

现在,我们将创建addData()函数来获取并保存数据到 Firebase:

 // Save data to firebase
 function addData() {
 var message = messageInput.value;
   db.ref().child('users').push({
    field: message
  });
  messageInput.value = '';
 }

在下一个屏幕截图中,我已经向输入文本添加了一些消息:

现在,我们需要将这些消息显示在 HTML 的消息标题底部:

// Update list of messages when data is added
db.ref().on('child_added', function(snapshot) {
var data = snapshot.val();
console.log("New Message Added", data);
  snapshot.forEach(function(childSnap) {
    console.log(childSnap.val());
    var message = childSnap.val();
    messages.innerHTML = '\n' + message.field;
  });
});

我们已经使用了child_added事件,这意味着每当在节点上添加任何子项时,我们都需要获取该值并更新消息列表。

现在,打开你的浏览器并注意输出:

看起来很棒。我们现在能够看到用户提交的消息,并且我们的数据也在实时中得到更新。

现在,让我们快速看一下我们的代码是什么样子的:

<!doctype html>
<html class="no-js" lang="">
<head>
 <meta charset="utf-8">
 <title>Chapter 1</title>
 <script src="https://www.gstatic.com/firebasejs/4.6.1/firebase.js"></script>
</head>
<body>
 <!-- Add your site or application content here -->
 <p id="message">Hello world! This is HTML5 Boilerplate.</p>
 <input type="text" id="messageInput" />
 <button type="button" onclick="addData()">Send message</button> 
 <h2>Messages</h2>
 <p id="list"></p>
<script>
 // Initialize Firebase
 var config = {
   apiKey: "<PROJECT API KEY>",
   authDomain: "<PROJECT AUTH DOMAIN>",
   databaseURL: "<PROJECT DATABASE AUTH URL>",
   projectId: "<PROJECT ID>",
   storageBucket: "",
   messagingSenderId: "<MESSANGING ID>"
 };
 firebase.initializeApp(config);

 var messageLabel = document.getElementById('message');
 var messageInput = document.getElementById('messageInput');
 var messages = document.getElementById('list'); 
 var db = firebase.database();
 db.ref().on("value", function(snapshot) {
     var object = snapshot.val();
     messageLabel.innerHTML = object.chapter1.example;
    //console.log(object);
 });
// Save data to firebase
 function addData() {
   var message = messageInput.value;
   db.ref().child('messages').push({
   field: message
 });
   messageInput.value = '';
 }
// Update results when data is added
 db.ref().on('child_added', function(snapshot) {
   var data = snapshot.val();
   console.log("New Message Added", data);
   snapshot.forEach(function(childSnap) {
   console.log(childSnap.val());
   var message = childSnap.val();
   messages.innerHTML = '\n' + message.field;
  });
 });
 </script>
</body>
</html>

总结

我们简单的 Hello World 应用程序和示例看起来很棒,并且正如他们应该的那样工作;所以,让我们回顾一下我们在本章学到的内容。

首先,我们介绍了 React 和 Firebase,以及设置 Firebase 帐户和配置有多么容易。我们还了解了实时数据库和 Firestore 之间的区别。除此之外,我们还学习了如何使用 JavaScript 初始化实时 Firebase 数据库,并开始构建我们的第一个 Hello World 应用程序。我们创建的 Hello World 应用程序演示了 Firebase 的一些基本功能,例如:

  • 关于实时数据库和 Firestore

  • 实时数据库和 Firestore 之间的区别

  • 使用 JavaScript 应用程序创建 Firebase 帐户和配置

  • Firebase 事件(值和child_data

  • 将值保存到数据库中

  • 从数据库中读取值

在第二章中,将 React 应用程序与 Firebase 集成,让我们使用 Firebase 构建一个 React 应用程序。我们将探索更多 React 和 Firebase 的基础知识,并介绍我们将在本书中构建的项目。

第二章:将 React 应用程序与 Firebase 集成

在第一章中,使用 Firebase 和 React 入门,我们看到了如何将 Firebase 与 JavaScript 集成,并创建了我们的第一个示例应用程序,这给了我们一个关于 Firebase 如何工作的简要概念。现在您已经完成了使用 JavaScript 和 Firebase 创建您的第一个 Web 应用程序,我们将使用 React 和 Firebase 构建帮助台应用程序。

我们将首先设置 React 环境,然后快速查看 JSX 和 React 组件方法。我们还将看到如何在 React 中使用 JSX 创建表单组件,并将这些表单值提交到 Firebase 实时数据库中。

以下是本章我们将关注的要点列表:

  • React 环境设置

  • JSX 和 React Bootstrap 的介绍

  • 使用 JSX 创建表单

  • 与 React 集成的 Firebase

  • 保存和读取实时数据库中的数据

设置环境

首先,我们需要创建一个类似于我们在第一章中制作的 Hello World 应用程序的文件夹结构。以下屏幕截图描述了文件夹结构:

当我们开始使用 ReactJS 制作应用程序时,我们需要进行一些设置,这仅涉及 HTML 页面和reactjs库。一旦我们完成了文件夹结构的创建,我们需要安装我们的两个框架:ReactJS 和 Firebase。只需在页面中包含 JavaScript 和 CSS 文件即可。我们可以通过内容交付网络CDN)(例如 Google 或 Microsoft)来实现这一点,但我们将在我们的应用程序中手动获取文件,这样我们就不必依赖于互联网,可以脱机工作。

安装 React

首先,我们必须转到reactjs.org/,查看我们将在应用程序中使用的最新可用版本:

在撰写本书时,最新可用版本是 v16.0.0。我们将在本章中使用 CDN React 包来构建我们的应用程序:

<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

前述版本仅用于开发,不适合生产。要使用经过缩小和优化的生产版本,我们需要使用这些生产包:

<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>

如果您想使用不同的版本,请将数字16替换为您在应用程序中要使用的版本。让我们在您的 HTML 中包含开发版本 CDN:

<!doctype html>
<html class="no-js" lang="">
<head>
    <meta charset="utf-8">
    <title>ReactJs and Firebase - Chapter 2</title>
    <script crossorigin  
     src="https://unpkg.com/react@16/umd/react.development.js">
    </script>
    <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-
     dom.development.js"></script>
</head>
<body>
    <!-- Add your site or application content here -->
    <p>Hello world! This is Our First React App with Firebase.</p>
</body>
</html>

使用 React

现在我们已经从 ReactJS 中初始化了我们的应用程序,让我们开始编写我们的第一个 Hello World 应用程序,使用ReactDOM.render()ReactDOM.render方法的第一个参数是我们要渲染的组件,第二个参数是它应该挂载(附加)到的 DOM 节点。请观察以下代码:

ReactDOM.render( ReactElement element, DOMElement container,[function callback] )

我们需要将它转换为原始 JavaScript,因为并非所有浏览器都支持 JSX 和 ES6 功能。为此,我们需要使用转译器 Babel,它将在 React 代码运行之前将 JSX 编译为原始 JavaScript。在 head 部分与 React 库一起添加以下库:

<script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>

现在,添加带有 React 代码的脚本标签:

<script type="text/babel">
ReactDOM.render(
<h1>Hello, world!</h1>,
document.getElementById('hello')
);
</script>

<script type="text/babel">标签实际上是在浏览器中执行转换的标签。

JavaScript 的 XML 语法称为JSX。我们将更详细地探讨这一点。让我们在浏览器中打开 HTML 页面。如果你在浏览器中看到 Hello, world!,那么我们就在正确的轨道上。请观察以下截图:

在上面的截图中,你可以看到它在你的浏览器中显示了 Hello, world!。看起来不错。我们已经成功完成了我们的设置,并用 ReactJS 构建了我们的第一个 Hello World 应用程序。

React 组件

React 基于模块化构建,具有封装的组件,这些组件管理自己的状态,因此当数据发生变化时,它将高效地更新和渲染您的组件。在 React 中,组件的逻辑是用 JavaScript 编写的,而不是模板,因此您可以轻松地通过应用程序传递丰富的数据并在 DOM 之外管理状态。使用render()方法,我们在 React 中渲染一个组件,该组件接受输入数据并返回您想要显示的内容。它可以接受 HTML 标签(字符串)或 React 组件(类)。

让我们快速看一下这两种例子:

var myReactElement = <div className="hello" />;
ReactDOM.render(myReactElement, document.getElementById('example'));

在这个例子中,我们将 HTML 作为字符串传递给render方法,之前我们创建了<Navbar>

var ReactComponent = React.createClass({/*...*/});
var myReactElement = <ReactComponent someProperty={true} />;
ReactDOM.render(myReactElement, document.getElementById('example'));

在上面的例子中,我们渲染组件只是为了创建一个以大写约定开头的局部变量。在 JSX 中使用大写约定,以避免区分本地组件类和 HTML 标签,因为 JSX 是 JavaScript 的扩展。在 React 中,我们可以以两种方式创建我们的 React 元素或组件:要么使用React.createElement的纯 JavaScript,要么使用 React 的 JSX。因此,让我们用 JSX 创建我们的第一个表单组件。

在 React 中 JSX 是什么?

JSX 是 JavaScript 语法的扩展,如果你观察 JSX 的语法或结构,你会发现它类似于 XML 编码。使用 JSX,你可以执行预处理步骤,将 XML 语法添加到 JavaScript 中。虽然你当然可以在不使用 JSX 的情况下使用 React,但 JSX 使 React 变得非常干净和可管理。与 XML 类似,JSX 标签具有标签名称、属性和子元素,如果属性值被引号括起来,那个值就成为一个字符串。XML 使用平衡的开放和关闭标签。JSX 类似地工作,它还有助于阅读和理解大量的结构,比 JavaScript 函数和对象更容易。

在 React 中使用 JSX 的优势

以下是一些优势的列表:

  • 与 JavaScript 函数相比,JSX 非常简单易懂

  • JSX 代码语法对非程序员更加熟悉

  • 通过使用 JSX,你的标记变得更有语义、有组织和有意义

如何使你的代码整洁清晰

正如我之前所说,这种结构/语法非常容易可视化/注意到,旨在使 JSX 格式的代码更加清晰和易懂,与 JavaScript 语法相比。

以下是一些代码片段的示例,它们将让你清楚地了解 React JavaScript 语法和 JSX:

render: function () {
return React.DOM.div({className:"title"},
"Page Title",
React.DOM.hr()
);
}

现在,观察以下的 JSX 语法:

render: function () {
return <div className="title">
Page Title<hr />
</div>;
}

所以现在我们清楚了,对于通常不习惯处理编码的程序员来说,JSX 真的很容易理解,他们可以学习、执行和编写它,就像 HTML 语言一样。

使用 JSX 的 React 表单

在开始使用 JSX 创建表单之前,我们必须了解 JSX 表单库。通常,HTML 表单元素输入将它们的值作为显示文本/值,但在 React JSX 中,它们取相应元素的属性值并显示它们。由于我们已经直观地感知到我们不能直接改变 props 的值,所以输入值不会有转变后的值作为展示值。

让我们详细讨论一下。要改变表单输入的值,你将使用 value 属性,然后你会看到没有变化。这并不意味着我们不能改变表单输入的值,但为此,我们需要监听输入事件,然后你会看到值的变化。

以下的例外是不言自明的,但非常重要:

在 React 中,标签内容将被视为值属性。由于 for 是 JavaScript 的保留关键字;HTML for 属性应该像 prop 一样被绑定。当您查看下一个示例时,您会更好地理解。现在,是时候学习了,为了在输出中有表单元素,我们需要使用以下脚本,并且还需要用先前编写的代码替换它。

现在,让我们开始为我们的应用程序构建一个 Add Ticket form。在根目录中创建一个 reactForm.html 文件和一个 js 文件夹中的 react-form.js 文件。以下代码片段只是一个包含 Bootstrap CSS 和 React 的基本 HTML 页面。

以下是我们的 HTML 页面的标记:

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Add ticket form with JSX</title>
    <link rel="stylesheet" href="css/bootstrap.min.css">
</head>
<body>
    <script crossorigin 
    src="https://unpkg.com/react@16/umd/react.development.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-
    dom.development.js"></script>
    <script src="https://unpkg.com/babel-
    standalone@6.15.0/babel.min.js"></script>
</body>
</html>

在页面底部加载所有脚本是一个很好的做法,在 <body> 标签关闭之前,这样可以成功在 DOM 中加载组件,因为当脚本在 <head> 部分执行时,文档元素不可用,因为脚本本身在 <head> 部分。解决这个问题的最佳方法是在页面底部保留脚本,在 <body> 标签关闭之前执行,这样在加载所有 DOM 元素后执行,不会抛出任何 JavaScript 错误。

由于 JSX 类似于 JavaScript,我们不能在 JSX 中使用 class 属性,因为它是 JavaScript 中的保留关键字。我们应该在 ReactDOM 组件中使用 classNamehtmlFor 作为属性名称。

现在,让我们在这个文件中使用 bootstrap 创建一些 HTML 布局

 <div class="container">
   <div class="row">
     <nav class="navbar navbar-inverse navbar-static-top" role="navigation">
   <div class="container">
    <div class="navbar-header">
     <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
     <span class="sr-only">Toggle navigation</span>
     <span class="icon-bar"></span>
     <span class="icon-bar"></span>
     <span class="icon-bar"></span>
 </button>
 <a class="navbar-brand" href="#">HelpDesk</a>
 </div>
 <div class="navbar-collapse collapse">
 <ul class="nav navbar-nav">
    <li class="active"><a href="#">Add Ticket</a></li>
 </ul>
 </div>
 </div>
 </nav>
 <div class="col-lg-12">
 <h2>Add Ticket</h2>
 <hr/> 
 <div id="form">
    <!-- Here we'll load load our AddTicketForm component with help of "form" id -->
 </div>
 </div>
 </div>
 </div>

在上面的代码中,我们创建了导航并将其包装到 bootstrap 网格类中,以实现组件的响应行为。

这是我们在浏览器中的 HTML 外观。

对于我们的 Add Ticket form 组件,我们需要以下表单字段以及标签:

  • 邮箱:<input>

  • 问题类型:<select>

  • 分配部门:<select>

  • 注释:<textarea>

  • 按钮:<button>

此外,以下是支持的事件列表:

  • onChange, onInput, 和 onSubmit

  • onClick, onContextMenu, onDoubleClick, onDrag, 和 onDragEnd

  • onDragEnteronDragExit

  • onDragLeave, onDragOver, onDragStart, onDrop, 和 onMouseDown

  • onMouseEnteronMouseLeave

  • onMouseMove, onMouseOut, onMouseOver, 和 onMouseUp

让我们快速查看一下我们表单组件的代码在 react-form.js 中:

class AddTicketForm extends React.Component {
    constructor() {
        super();
        this.handleSubmitEvent = this.handleSubmitEvent.bind(this);
    }
    handleSubmitEvent(event) {
        event.preventDefault();
    }
    render() {
        var style = {color: "#ffaaaa"};
        return ( <form onSubmit = {this.handleSubmitEvent}>
   <div className = "form-group">
      <label htmlFor = "email"> Email <span style = {style}> * </span></label>
      <input type = "text" id = "email" className = "form-control" placeholder = "Enter your email address" required />
   </div>
   <div className = "form-group">
      <label htmlFor = "issueType"> Issue Type <span style = {style}> * </span></label>
      <select className = "form-control" id = "issueType" required>
         <option value = ""> -- -- - Select-- -- < /option> 
         <option value = "Access Related Issue"> Access Related Issue </option>
         <option value = "Email Related Issues"> Email Related Issues </option>
         <option value = "Hardware Request"> Hardware Request</option>
         <option value = "Health & Safety"> Health & Safety </option>
         <option value = "Network"> Network </option> 
         <option value = "Intranet"> Intranet </option> 
         <option value = "Other"> Other </option> 
      </select>
   </div>
   <div className = "form-group">
      <label htmlFor = "department"> Assign Department 
      <span style = {style} > * </span>
      </label>
      <select className="form-control" id="department" required>
         <option value = ""> -- -- - Select-- -- </option> 
         <option value = "Admin" > Admin </option>
         <option value = "HR"> HR </option>
         <option value = "IT"> IT </option> 
         <option value = "Development"> Development </option>
      </select>
   </div>
   <div className = "form-group">
      <label htmlFor = "comments"> Comments 
      <span style = {style}> * </span>
      </label>
      ( <span id = "maxlength"> 200 </span> characters max)
      <textarea className = "form-control" rows = "3" id = "comments" required> </textarea> 
   </div>
   <div className = "btn-group">
      <button type = "submit" className = "btn btn-primary"> Submit </button> 
      <button type = "reset" className = "btn btn-default"> cancel </button> 
   </div>
</form>
            );
        }
    });
ReactDOM.render( <AddTicketForm /> ,
    document.getElementById('form')
);

要应用样式或调用onSubmit()函数的属性值,而不是使用引号(""),我们必须在 JavaScript 表达式中使用一对花括号({})。这意味着你可以通过用花括号包裹任何 JavaScript 表达式在 JSX 中嵌入它,甚至是一个函数。

在 react 库之后,在 HTML 页面底部添加这个脚本标签

<script src="js/react-form.js" type="text/babel"></script>

现在,打开你的浏览器,让我们看看我们的 JSX 代码的输出:

看起来很棒。我们可以看到我们的表单如预期的那样。

在 React 中创建组件时,第一个字符应该始终大写。例如,我们的Add Ticket form组件是<AddTicketForm></AddTicketForm>

对于大型应用程序,这种方法并不推荐;我们不能每次创建表单元素时都把整个 JSX 代码放在一个地方。为了使我们的代码清晰和易于管理,我们应该创建一个可重用的组件,只需在需要使用它的地方给出该组件的引用。

那么让我们看看如何在我们现有的代码中实现这一点,我们将创建一个可重用的文本输入组件:

const TextInput = ({
    type,
    name,
    label,
    onChange,
    placeholder,
    value,
    required
}) => {
    return ( <div className = "form-group">
        <label htmlFor = {name} > {label} </label> 
        <div className = "field">
        <input type = {type}  name = {name} className ="form-control" placeholder = {         placeholder} value = {value} onChange = {onChange} required = {required}/> 
</div> 
</div>
    )
}

在上面的代码片段中,我们创建了一个对象,它接受与输入属性相关的一些参数,并将这些参数的值分配给属性的值:

<TextInput
 type="email"
 name="email"
 label="Email"
 placeholder="Enter your email address"
 required={true}/>

现在我们只需要在我们的render方法中像这样添加前面的TextInput组件,正如你在前面的代码中所看到的,而不是在我们的应用程序中每次都添加标签和输入;这展示了 ReactJS 的强大之处。

使用 React-Bootstrap

React-Bootstrap 是一个为 React 重建的开源 JavaScript 框架。它类似于 Bootstrap,我们有现成的组件可以与 React 集成。这是 Bootstrap 框架组件在 React 中的纯重新实现。React-Bootstrap 不依赖于任何其他框架,因为 Bootstrap JS 依赖于 jQuery。通过使用 React-Bootstrap,我们可以确保不会有外部 JavaScript 调用来渲染组件,这可能与ReactDOM.render不兼容或需要额外的工作。然而,我们仍然可以实现相同的功能和外观

Twitter Bootstrap,但代码更清晰,更少。

让我们看看如何使用 React-Bootstrap 创建我们的Add Ticket Form组件。

首先,按照这里提到的步骤在你的项目中配置 React-Bootstrap:

  1. 通过运行以下命令安装 React bootstrap npm 包
  • npm install --save react-bootstrap
  1. 如果您正在使用 create-react-app CLI,我们不需要担心 bootstrap CSS;它已经存在,我们不需要包含。

  2. 现在,通过使用 import 关键字,我们需要在 React 应用程序中添加对 react-bootstrap 组件的引用。

例如:

  • import Button from 'react-bootstrap/lib/Button';

// 或者

import { Button } from 'react-bootstrap';

使用 React-Bootstrap 添加工单表单

现在,您可能会想知道,既然我们已经安装了 React-Bootstrap,并且已经通过使用import语句在我们的项目中添加了 React-Bootstrap 的引用,它们不会互相冲突吗?不,它们不会。React-Bootstrap 与现有的 Bootstrap 样式兼容,因此我们不需要担心任何冲突。

查看Add Ticket组件渲染方法的代码:

<form>
    <FieldGroup id="formControlsEmail" type="email" label="Email 
    address" placeholder="Enter email" />
    <FormGroup controlId="formControlsSelect">
        <ControlLabel>Issue Type</ControlLabel>
        <FormControl componentClass="select" placeholder="select">
            <option value="select">select</option>
            <option value="other">...</option>
        </FormControl>
    </FormGroup>
    <FormGroup controlId="formControlsSelect">
        <ControlLabel>Assign Department</ControlLabel>
        <FormControl componentClass="select" placeholder="select">
            <option value="select">select</option>
            <option value="other">...</option>
        </FormControl>
    </FormGroup>
    <FormGroup controlId="formControlsTextarea">
        <ControlLabel>Textarea</ControlLabel>
        <FormControl componentClass="textarea" placeholder="textarea" 
        />
    </FormGroup>
</form>

如您在上述代码中所见,它看起来比 Twitter Bootstrap 组件更清晰,因为我们可以从 React-Bootstrap 中导入单个组件,而不是包含整个库,例如import { Button } from 'react-bootstrap';

以下是支持的表单控件列表:

  • <FieldGroup>用于自定义组件

  • <FormControl>用于<input><textarea><select>

  • <Checkbox>用于复选框

  • <Radio>用于单选按钮

  • FormControl.Static(用于静态文本)

  • HelpBlock

现在由您决定是使用 React-Bootstrap 还是带有 Bootstrap 样式的普通 JSX 组件。

更多细节,请查看react-bootstrap.github.io/components/forms/

使用 React 的 Firebase

我们已经创建了一个 React 表单,您可以在其中提出 Helpdesk 的工单并保存到 Firebase。为此,现在我们需要在现有应用程序中集成和初始化 Firebase。

它的样子是这样的:

在我们的 HTML 底部添加了脚本标签:

<!--Firebase Config -->
<script src="js/firebase-config.js"></script>
<!--ReactJS Form -->
<script type="text/babel" src="js/react-form.js"></script>

将现有的 Firebase 配置代码从上一章复制到firebase-config.js中:

 // Initialize Firebase
 var config = {
 apiKey: "<PROJECT API KEY>",
 authDomain: "<PROJECT AUTH DOMAIN>",
 databaseURL: "<PROJECT DATABASE AUTH URL>",
 projectId: "<PROJECT ID>",
 storageBucket: "",
 messagingSenderId: "<MESSANGING ID>"
 };
 firebase.initializeApp(config);
 var firebaseDb = firebase.database();

还要将Reactjs Form添加到react-form.js中,以使我们的代码看起来干净和可管理:

class AddTicketForm extends React.Component {
    constructor() {
        super();
        this.handleSubmitEvent = this.handleSubmitEvent.bind(this);
    }
    handleSubmitEvent(event) {
            event.preventDefault();
            console.log("Email--" + this.refs.email.value.trim());
            console.log("Issue Type--" + 
            this.refs.issueType.value.trim());
            console.log("Department--" + 
            this.refs.department.value.trim());
            console.log("Comments--" + this.refs.comment.value.trim());
        },
        render() {
            return ();
        }
};

属性和状态

在我们进行实际操作之前,我们应该知道在 React 中状态和属性是什么。在 ReactJs 中,组件使用 JSX 将您的原始数据转换为丰富的 HTML,属性和状态一起构建这些原始数据,以保持您的 UI 一致。好的,让我们确定它到底是什么:

  • 属性和状态都是普通的 JS 对象。

  • 它们由渲染更新触发。

  • React 通过调用 setState(数据,回调)来管理组件状态。这种方法将数据合并到此状态中,并重新渲染组件,以保持我们的 UI 最新。例如,下拉菜单的状态(可见或隐藏)。

  • React 组件属性(属性)随时间不会改变,例如下拉菜单项。有时组件只使用此属性方法获取一些数据并呈现它,这使得您的组件无状态。

  • 使用属性和状态一起可以帮助您创建一个交互式应用程序。

将表单数据读取和写入 Firebase 实时数据库。

正如我们所知,ReactJS 组件有自己的属性和类似状态的表单,支持

一些受用户交互影响的属性:

<input><textarea>

组件 支持的属性
<input><textarea> Value, defaultValue
<input> 复选框或单选框类型 checked, defaultChecked
<select> selected, defaultValue

在 HTML <textarea> 组件中,值是通过 children 设置的,但在 React 中也可以通过 value 设置。onChange 属性被所有原生组件支持,例如其他 DOM 事件,并且可以监听所有冒泡变化事件。

正如我们所见,状态和属性将使您能够改变组件的值并处理该组件的状态。

现在,让我们在我们的“添加工单表单”中添加一些高级功能,这些功能可以帮助您获取用户输入的值,并借助 Firebase,我们将这些值保存在数据库中。

Ref 属性

React 提供了 ref 非 DOM 属性来访问组件。ref 属性可以是回调函数,并且它将在组件挂载后立即执行。因此,我们将在我们的表单元素中附加 ref 属性以获取这些值。

在添加 ref 属性后,让我们快速查看一下我们的组件:

<div>
   <form ref = "form" onSubmit = {this.handleSubmitEvent}>
      <div className = "form-group">
         <label htmlFor= "email"> Email <span style = {style} > * </span></label>
         <input type = "text" id = "email" className = "form-control" placeholder = "Enter your email address" required ref = "email" />
      </div>
      <div className = "form-group">
         <label htmlFor = "issueType"> Issue Type <span style = {style}> * </span></label>
         <select className = "form-control" id = "issueType" required ref = "issueType">
            <option value = "" > -- -- - Select-- -- </option>
            <option value = "Access Related Issue"> Access Related 
               Issue 
            </option>
            <option value = "Email Related Issues"> Email Related 
               Issues 
            </option>
            <option value = "Hardware Request"> Hardware Request </option>
            <option value = "Health & Safety"> Health & Safety </option>
            <option value = "Network" > Network < /option> 
            <option value = "Intranet"> Intranet </option>
            <option value = "Other"> Other </option>
         </select>
      </div>
      <div className = "form-group">
         <label htmlFor = "department"> Assign Department <span style = {style} > * </span></label>
         <select className = "form-control" id = "department" required ref = "department">
            <option value = ""> -- -- - Select-- -- </option>
            <option value = "Admin"> Admin </option> 
            <option value = "HR"> HR </option>
            <option value = "IT"> IT </option>
            <option value = "Development"> Development </option>
         </select>
      </div>
      <div className = "form-group">
         <label htmlFor = "comments"> Comments <span style = {style
            } > * </span></label>
         ( <span id = "maxlength"> 200 </span> characters max) <textarea className = "form-control" rows = "3" id = "comments" required ref = "comment"> </textarea> 
      </div>
      <div className = "btn-group"><button type = "submit" className = "btn btn-primary"> Submit </button> <button type = "reset" className = "btn btn-default"> cancel </button> </div>
   </form>
</div>

现在,让我们打开浏览器,看看我们的组件是什么样子的:

Firebase 在我们的应用程序中完美运行,因为您可以看到标题底部显示的消息“Hello world! This is My First JavaScript Firebase App”; 这是来自 Firebase 实时数据库

此外,在控制台中,您可以在提交表单时看到这些值。

现在我们需要将这些值保存到数据库中:

//React form data object
var data = {
   date: Date(),
   email:this.refs.email.value.trim(),
   issueType:this.refs.issueType.value.trim(),
   department:this.refs.department.value.trim(),
   comments:this.refs.comment.value.trim()
 }

我们这样做是为了将“表单”数据对象写入 Firebase 实时数据库;firebase.database.Reference是一个异步监听器,用于从 Firebase 检索数据。一旦触发此监听器,它将在初始状态和数据发生更改时触发。

如果我们有权限,我们可以从 Firebase 数据库中读取和写入数据,因为默认情况下,数据库是受限制的,没有人可以在没有设置身份验证的情况下访问它。

firebaseDb.ref().child('helpdesk').child('tickets').push(data);

在上述代码中,我们使用push()方法将数据保存到 Firebase 数据库中。每当向指定的 Firebase 引用添加新子项时,它都会生成一个唯一键。我们还可以使用set()方法将数据保存到指定引用的数据;它将替换该节点路径上的现有数据:

firebaseDb.ref().child('helpdesk').child('tickets').set(data);

要在添加数据时检索更新结果,我们需要使用on()方法附加监听器,或者在任何情况下,如果我们想要在特定节点上分离监听器,那么我们可以通过调用off()方法来实现:

 firebaseDb.ref().on('child_added', function(snapshot) {
 var data = snapshot.val();
  snapshot.forEach(function(childSnap) {
    console.log(childSnap.val());
     this.refs.form.reset();
    console.log("Ticket submitted successfully");
  });
 });

但是,如果我们想要一次读取它们而不监听更改,我们可以使用once()方法:

 firebaseDb.ref().once('value').then(function(snapshot){
 });

这在我们不期望数据发生任何变化或任何主动监听时非常有用。例如,在我们的应用程序中成功验证用户配置文件数据时,加载用户配置文件数据时。

要更新数据,我们有update()方法,要删除数据,我们只需要调用该数据位置的delete()方法。

update()set()方法都返回一个 Promise,因此我们可以使用它来知道写入是否提交到数据库。

现在,让我们提交表单并在浏览器控制台中查看输出:

看起来很棒;现在,让我们来看看我们的 Firebase 数据库:

我们能够看到我们从 ReactJS 表单提交的数据。

现在我们将以表格格式显示这些数据;为此,我们需要创建另一个 React 组件并设置组件的初始状态:

constructor(){
    super();
    this.state = {
      tickets:[]
    }
  }

现在,使用componentDidMount()方法,我们将通过ref()调用数据库,迭代对象,并使用this.setState()设置组件的状态:

componentDidMount()  {
  var  itemsRef  =  firebaseDb.ref('/helpdesk/tickets');
  console.log(itemsRef);
  itemsRef.on('value',  (snapshot)  =>  {
  let  tickets  =  snapshot.val();
  console.log(tickets);
  let  newState  = [];
  for (let  ticket  in  tickets) {
  newState.push({
 id:tickets[ticket],
 email:tickets[ticket].email,
 issueType:tickets[ticket].issueType,
 department:tickets[ticket].department,
 comments:tickets[ticket].comments,
 date:tickets[ticket].date
  });
  }
  this.setState({
 tickets:  newState
  });
  }); },

现在我们将在渲染方法内部迭代票务状态并在表格中显示:

render() {
  return (<table className="table">
<thead>
<tr> 
    <th>Email</th>
    <th>Issue Type</th> 
    <th>Department</th> 
    <th>Comments</th> 
    <th>Date</th> 
</tr>
</thead>
<tbody>
 {
   this.state.tickets.map((ticket) => 
    { return ( 
    <tr key={ticket.id}> 
        <td>{ticket.email}</td> 
        <td>{ticket.issueType}</td> 
        <td>{ticket.department}</td> 
        <td>{ticket.comments}</td> 
        <td>{ticket.date}</td> 
</tr> )})
 } 
</tbody>
</table>
)}

现在,用户可以在实时上查看票据列表,每当数据库中添加新的票据时:

这是我们 HTML 页面的标记:viewTickets.html

 <div class="col-lg-10">
 <h2>View Tickets</h2>
 <hr>
    <div id="table" class="table-responsive">
      <!-- React Component will render here -->
    </div>
 </div>
 </div>
 </div>

这是在 Firebase 实时数据库中添加的票据列表:

总结

在本章中,我们看到了 JSX 在 React 中制作自定义组件以及使它们非常简单可视化、理解和编写方面起着重要作用。我们还看到了 props 和 state 在使组件交互以及在 DOM 交互中获取表单字段的值方面起着重要作用。借助refs,我们可以调用任何公共方法并向特定的子实例发送消息。

此外,我们通过创建一个Add Ticket form来探索了 React-Bootstrap 组件,该表单在所有预期的设备上以及桌面浏览器上都能很好地工作。

此外,我们还看到了在 ReactJS 应用程序中使用 Firebase 实时数据库有多么容易。只需几行代码,我们就可以将数据保存到实时数据库,并实时从数据库中检索票据列表,使我们的应用程序实时化。

在下一章中,我们将在 node.js 环境中进行 React 和 Firebase 设置,以及如何使用 Firebase OAuth 提供程序在我们的应用程序中添加身份验证。我们还将探索用于导航的 React 路由

第三章:使用 Firebase 进行认证

在上一章中,我们学习了如何将 Firebase 与 ReactJS 集成,以及如何在 JSX 中创建组件。我们还看到了如何与 DOM 元素交互以获取onSubmit表单值,并将其发送到 Firebase 数据库中以在云中存储和同步表单数据。React 使用快速、内部的合成 DOM 来执行差异,并为您计算最有效的 DOM 变化,其中您的组件活动地存在。

在本章中,我们将使用 React 和 JSX 创建一个login组件,以使用 Firebase 认证功能来保护我们的帮助台应用程序,该功能只允许授权用户查看和添加新的工单。

以下是本章我们将重点关注的内容列表:

  • 使用 Node.js 进行 React 和 Firebase 设置

  • 使用 React 和 JSX 创建复合组件

  • Firebase 认证配置

  • 自定义认证

  • 使用 Facebook 和 Google 进行第三方认证

使用 Node.js 进行 React 和 Firebase 设置

之前,我们使用纯 JavaScript 创建了一个 React 应用程序;现在我们需要使用 React 和 Firebase 设置来使用 node 做同样的事情。为此,我们必须在系统中安装 Node.js 和npm;如果没有,请先从nodejs.org/en/download/下载 Node.js。安装完成后,运行以下命令以确保 node 和npm已正确安装:

对于 node,使用以下命令:

node -v

对于npm,使用以下命令:

npm -v

命令的输出应该如下所示:

现在我们需要安装create-react-app模块,它提供了初始和默认设置,并让我们快速启动 React 应用程序。在 CMD 中运行以下命令,它将全局安装create-react-app模块(即在命令后加上-g--global):

npm install -g create-react-app 
or 
npm i -g create-react-app

安装完成后,在需要创建项目的本地目录中运行下一个命令;这将为 React 生成无需构建配置的快速启动项目:

create-react-app <project-name> 
or
create-react-app login-authentication

安装完成后,我们的文件夹结构如下所示:

在这里,我们已经完成了 React 的设置;现在,我们安装firebase npm包并集成我们现有的应用程序。

运行以下命令安装firebase npm包:

npm install firebase --save

安装 firebase 后,在src文件夹内创建一个名为 firebase 的文件夹。

src文件夹中,创建一个名为firebase-config.js的文件,其中将托管我们项目的配置详细信息:

import  firebase  from  'firebase'; const  config  = {  apiKey:  "AIzaSyDO1VEnd5VmWd2OWQ9NQuh-ehNXcoPTy-w",
  authDomain:  "demoproject-7cc0d.firebaseapp.com",
  databaseURL:  "https://demoproject-7cc0d.firebaseio.com",
  projectId:  "demoproject-7cc0d",
  storageBucket:  "demoproject-7cc0d.appspot.com",
  messagingSenderId:  "41428255556" }; firebase.initializeApp(config); export  default  firebase;

同样,我们需要在节点中集成我们现有的组件视图票和addTicket,使用导入和导出关键字,并使用npm命令,我们需要安装 React 和 firebase 模块及其依赖项。

这是您的package.json应该看起来的样子:

//package.json
{
 "name": "login-authentication",
 "version": "0.1.0",
 "private": true,
 "dependencies": {
 "firebase": "⁴.8.0",
 "react": "¹⁶.2.0",
 "react-dom": "¹⁶.2.0",
 "react-router-dom": "⁴.2.2",
 "react-scripts": "1.0.17",
 "react-toastr-basic": "¹.1.14"
 },
 "scripts": {
 "start": "react-scripts start",
 "build": "react-scripts build",
 "test": "react-scripts test --env=jsdom",
 "eject": "react-scripts eject"
 }
}

此外,在集成现有应用程序后,应用程序文件夹结构如下所示:

用于身份验证的 Firebase 配置

Firebase 身份验证是一个非常令人印象深刻的功能,可以通过安全规则授予用户读/写访问权限。我们还没有在我们的帮助台应用程序中涵盖或添加安全规则。Firebase 让我们能够使用其自己的电子邮件/密码和 OAuth 2 集成来进行 Google、Facebook、Twitter 和 GitHub 的认证。我们还将把我们自己的身份验证系统与 Firebase 集成,以便让用户访问帮助台应用程序,并允许用户在我们的系统上创建帐户。

让我们来看看用于身份验证的 Firebase 提供程序列表,并执行以下步骤来为我们的应用程序启用 Firebase 身份验证:

  1. 打开firebase.google.com并使用您的凭据登录

  2. 点击左侧的 DEVELOP 选项卡内的身份验证选项:

在上述截图中,如果您能看到,我们在身份验证部分有四个可用的选项卡,并且我们已经启用了提供商的身份验证,其中包括自定义的电子邮件/密码选项,我们可以添加到用户选项卡和 Google 身份验证。

  • 用户:在这里,我们可以管理并添加多个用户的电子邮件 ID 和密码,以便使用各种提供程序进行身份验证,而无需编写任何服务器端代码。

  • 登录方式:在此部分,我们可以看到 Firebase 中可用的提供程序列表。我们还可以管理授权域,防止用户使用相同的电子邮件地址和登录配额。

  • 模板:此功能允许我们自定义 Firebase 发送的电子邮件模板,当用户使用电子邮件和密码注册时。我们还可以自定义密码重置、电子邮件地址更改和短信验证的模板。

在本章中,我们将涵盖以下三种身份验证方式:

  • 脸书

  • 谷歌

  • 电子邮件/密码

使用 Facebook 进行身份验证

要向我们的帮助台应用程序添加 Facebook 身份验证,如果您还没有 Facebook 帐户,您需要在 Facebook 上创建一个帐户。否则,我们需要登录到 Facebook 开发者论坛developers.facebook.com/apps。一旦我们登录,它会显示应用程序列表和一个“添加新应用程序”按钮,用于创建身份验证的新应用程序 ID。参考以下内容:

点击“添加新应用程序”按钮;它会显示弹出窗口以添加应用程序的名称。然后,点击“创建应用程序 ID”,它会将您重定向到我们应用程序的仪表板:

这是 Facebook 开发者应用程序仪表板的屏幕截图。图像的目的只是显示 Facebook 提供的 API 或产品列表,以与任何 Web 应用程序集成。

现在,我们需要选择 Facebook 登录进行设置:

如果您能看到上述的屏幕截图,我们需要为客户端 OAuth 设置。为此,我们首先需要启用嵌入式浏览器 OAuth 登录功能以控制 OAuth 登录的重定向,然后复制有效的 OAuth 重定向 URL,当我们在 Firebase 中启用 Facebook 提供程序时,我们可以获得它。

要在 Firebase 中启用 Facebook 身份验证,我们需要从 Facebook 应用程序仪表板复制应用程序 ID应用程序密钥

然后,将这些复制的值放入 firebase 输入字段中,复制重定向 URI,并将其粘贴到客户端 OAuth 设置中。还要启用 Facebook 身份验证,然后点击“保存”按钮,如下所示:

这是我们在 Facebook 开发者论坛和 Firebase 中进行 Facebook 身份验证的最后一件事情。

点击保存,并注意提供程序的状态现在已启用:

现在,点击部分左侧的数据库,转到规则面板;它应该看起来像这样:

图像的目的是显示实时数据库部分和规则选项卡下的选项卡列表。在这里,我们可以添加数据库的安全规则来保护我们的数据,并借助模拟器来验证它是否按预期工作。

在我们的应用程序中,以前每个人都有权访问我们的应用程序和数据库以读取和写入数据。现在,我们将更改前面的规则配置,以便只有经过授权的用户才能访问应用程序并向我们的数据库写入数据。查看给定的代码并发布更改:

{
 "rules": {
 ".read": "auth != null",
 ".write": "auth != null"
 }
}

使用 React 创建登录表单进行身份验证

就像我们为 Firebase 和 Facebook 的身份验证配置以及启用其他提供程序的功能一样,现在我们将在 react 中创建一个登录表单,以确保应用程序始终验证用户是否已登录;它将重定向用户到登录页面。因此,让我们创建一个登录页面,并配置 React 路由以根据路径 URL 重定向用户。

打开 firebase 文件夹中的firebase-config.js并导出以下不同提供程序的对象,以便我们可以在整个应用程序中访问这些对象:

export  const  firebaseApp  =  firebase.initializeApp(config); export  const  googleProvider  =  new  firebase.auth.GoogleAuthProvider(); export  const  facebookProvider  =  new  firebase.auth.FacebookAuthProvider();

在上述代码中,new firebase.auth.GoogleAuthProvider()将为我们提供通过 Google API 对用户进行身份验证的方法。

同样,new firebase.auth.FacebookAuthProvider()将为我们提供通过 Facebook API 对用户进行身份验证的方法。

打开app.js并将以下代码添加到构造函数中以初始化应用程序的状态:

constructor() { super();   this.state  = {  authenticated :  false,
  data:''
 } }

在这里,我们将 authenticated 的默认值设置为 false,因为这是应用程序的初始状态,用户尚未通过 Firebase 进行身份验证;数据的默认值在组件的初始状态下为空。当用户登录时,我们将更改这些状态。

首先,让我们在login.js中创建Login组件,并在constructor()中设置该组件的初始状态:

 constructor() {
 super();
   this.state = {
     redirect: false
   }
 }

我们在初始状态下将重定向的默认值设置为false,但每当用户登录和退出时,它都会更改:

if(this.state.redirect === true){
 return <Redirect to = "/" />
 }
 return (
 <div className="wrapper">
 <form className="form-signin" onSubmit={(event)=>{this.authWithEmailPassword(event)}} ref={(form)=>{this.loginForm = form}}> 
 <h2 className="form-signin-heading">Login</h2>
 <input type="email" className="form-control" name="username" placeholder="Email Address" ref={(input)=>{this.emailField = input}} required />
 <input type="password" className="form-control" name="password" placeholder="Password" ref={(input)=>{this.passwordField = input}} required /> 
 <label className="checkbox">
 <input type="checkbox" value="remember-me" id="rememberMe" name="rememberMe"/> Remember me
 </label>
 <button className="btn btn-lg btn-primary btn-block btn-normal" type="submit">Login</button> 
 <br/> 
<!-- Here we will add the buttons for google and facebook authentication
 </form>
 </div>
 );

render方法中,我们将检查状态并将用户重定向到不同的路由<Redirect>。它将覆盖历史堆栈中的当前路由,就像服务器端重定向(HTTP 3xx)一样。

以下是我们可以与Redirect组件一起使用的属性列表:

  • to:String:我们还使用的重定向 URL。

  • to:Object:带有参数和其他配置(例如状态)的位置 URL。考虑以下示例:

<Redirect to={{
 pathname: '/login',
 search: '?utm=your+selection',
 state: { referrer: currentLocation }
}}/>
  • : bool:当为 true 时,重定向将在历史记录中推送一个新条目,而不是替换当前条目。

  • from: string:要重定向的旧 URL。这只能用于匹配<Switch>内的位置。考虑这个例子:

<Switch>
 <Redirect from='/old-url' to='/new-url'/>
 <Route path='/new-url' component={componentName}/>
</Switch>

所有上述的<Redirect>功能只在 React Router V4 中可用。

我们已经为我们的登录表单添加了 JSX,并绑定了方法和 ref 属性以访问表单值。我们还添加了 Facebook 和 Google 身份验证的按钮。只需看看以下代码:

 <!-- facebook button that we have bind with authWithFacebook()-->
<button className="btn btn-lg btn-primary btn-facebook btn-block" type="button" onClick={()=>{this.authWithFacebook()}}>Login with Facebook</button> 
<!-- Google button which we have bind with authWithGoogle()-->
 <button className="btn btn-lg btn-primary btn-google btn-block" type="button" onClick={()=>{this.authWithGoogle()}}>Login with Google</button>

app.js中,我们已经配置了一个路由器,就像这样:

<Router>
<div className="container"> {
this.state.authenticated
?
(
<React.Fragment>
<Header authenticated = {this.state.authenticated}/>
<Route path="/" render={() => (<Home userInfo = {this.state.data} />)} />
<Route path="/view-ticket" component={ViewTicketTable}/>
<Route path="/add-ticket" component={AddTicketForm}/>
</React.Fragment>
)
:
(
<React.Fragment>
<Header authenticated = {this.state.authenticated}/>
<Route exact path="/login" component={Login}/>
</React.Fragment>
)
}
</div>
</Router>

在上面的代码中,我们使用的是 React Router 版本 4,这是一个完全重写的用于 react 包的路由器。在以前的 React 路由器版本中,他们使用了非常困难的配置,这将很难理解,而且我们还需要创建一个单独的组件来管理布局。在路由器 V4 中,一切都作为一个组件工作。

在 React 路由器 V4 中,我们需要从 react-router-dom 中导入,而不是从 V3 中的 react-router。如果路由路径匹配,<Router>组件和所有其他子组件都会被渲染。

使用<React.Fragment>标签,我们可以包装任何 JSX 组件,而不会在 DOM 中添加另一个节点。

在 V4 react 路由器中,不再有<IndexRoute>;使用<Route exact>将会做同样的事情。

现在我们将更改包含导航的标题组件,并添加登录和注销链接:

class Header extends Component {
render() {
 return (
 <div className="navbar navbar-inverse firebase-nav" role="navigation">
 {
 this.props.authenticated
 ?
 (
 <React.Fragment>
 <ul className="nav navbar-nav">
 <li className="active"><Link to="/">Home</Link></li>
 <li><Link to="/view-ticket">Tickets</Link></li>
 <li><Link to="/add-ticket">Add new ticket</Link></li>
 </ul>
 <ul className="nav navbar-nav navbar-right">
 <li><Link to="/logout">Logout</Link></li>
 </ul>
 </React.Fragment>
 ):(
 <React.Fragment>
 <ul className="nav navbar-nav navbar-right">
 <li><Link to="/login">Register/Login</Link></li>
 </ul>
 </React.Fragment>
 )
 }
 </div>
 );
 }
}

如果我们使用 React 路由器,这是必要的。让我们在导航中添加<link>而不是<a>标签,并用to替换href属性。在 V4 中,我们还可以使用<NavLink>;它的工作方式与<Link>相同,但可以添加额外的样式。看看这段代码:

<li><NavLink to="/view-ticket/" activeClassName="active" activeStyle={{fontWeight: 'bold', color: red'}} exact strict>Tickets</NavLink></li>

根据身份验证,我们将更新导航以显示登录和注销链接。

通过在命令提示符中运行以下命令再次启动服务器:

npm start

一旦服务器启动,打开浏览器快速查看:

如果你只看一下上面的屏幕摘录并注意地址栏,我尝试打开另一个 URL 来查看票据,但除了标题登录链接外,什么都没有显示;所以现在,如果我们点击登录,它将呈现登录表单。请参考以下截图;它应该是这样的:

令人惊讶的是,我们的登录表单看起来很棒,正如预期的那样。

有关 react 路由器的更多信息,您可以查看reacttraining.com/react-router/web/api

使用 Facebook 进行身份验证

每个按钮的onClick将指向三个函数,这些函数将对用户进行身份验证。Facebook 身份验证方法将处理我们与 Firebase 的身份验证,如下所示:

 authWithFacebook(){
 console.log("facebook");
 firebaseApp.auth().signInWithPopup(facebookProvider).then((result,error)=>{
 if(error){
   console.log("unable to sign in with facebook");
 }
 else{
   this.setState({redirect:true})
 }}).catch((error)=>{
        ToastDanger(error.message);
    })
 }

在这里,我们从 firebase auth模块调用signInWithPopup()方法,并传递 facebook 提供程序。

为了在 UI 上显示错误消息,我们使用 React Toaster 模块,并将这些消息传递给它(在使用之前不要忘记安装和导入 React Toaster 模块)。我们还需要将authWithFacebook()方法绑定到构造函数中。npm install --save react-toastr-basic

//在 app.js 中导入容器

从' react-toastr-basic '导入 ToastrContainer;

//在 render 方法内部

<ToastrContainer />

constructor() {
 super();
 this.authWithFacebook = this.authWithFacebook.bind(this);
 this.state = {
  redirect: false,
  data:null
 }}

现在,当我们点击“使用 Facebook 登录”按钮时,它将打开一个弹出窗口,让我们选择使用 Facebook 帐户登录,如下所示:

signInWithPopup()具有一个 promise API,允许我们在其上调用.then()并传递回调。此回调将提供一个包含用户的所有信息的名为user的对象,其中包括他们刚刚成功登录的姓名、电子邮件和用户照片 URL。我们将使用setState()将此对象存储在状态中,并在 UI 上显示用户的姓名、电子邮件和照片:

使用 Google 进行身份验证

同样,我们可以在我们的应用程序中配置 Google 身份验证;只需将以下代码添加到authWithGoogle()方法中,它将打开用于使用 Google 登录的弹出窗口:

 authWithGoogle(){
 console.log("Google");      
 googleProvider.addScope('profile');
 googleProvider.addScope('email');
 firebaseApp.auth().signInWithPopup(googleProvider).then((result,error)=>{
   if(error){
     console.log("unable to sign in with google");
    }
   else{
     this.setState({redirect:true,data:result.user})
   }}).catch((error)=>{
        ToastDanger(error.message);
     })
}

如您所见,我已添加了我们想要从身份验证提供程序请求的额外 OAuth 2.0 范围。要添加范围,请调用添加范围。我们还可以使用firebase.auth().languageCode = 'pt'来定义语言代码。如果我们想要在请求中发送特定的自定义参数,可以调用setCustomParamter()方法。考虑这个例子:

provider.setCustomParameters({
 'login_hint': 'admin'
});

因此,一旦您点击“使用 Google 登录”按钮,它将触发弹出窗口以与 Google 进行身份验证:

因此,如果您已经登录并尝试使用不同提供者的相同电子邮件 ID 登录,它会抛出错误,如下所示:

好的,现在让我们看看如何处理这些类型的错误。

处理帐户存在的错误

考虑到我们已经在 firebase 设置中启用了“每个电子邮件地址一个帐户”的选项。如前面的截图所示,当我们尝试使用提供者(Google)登录已经存在于 firebase 中的具有不同提供者(如 Facebook)的电子邮件时,它会抛出上述错误——auth/account-exists-with-different-credential——我们可以在前面的截图中看到。为了处理这个错误并完成对所选提供者的登录,用户必须首先登录到现有的提供者(Facebook),然后链接到前面的 AuthCredential(带有 Google ID 令牌)。重写authWithFacebook()方法后,我们的代码如下:

if (error.code === 'auth/account-exists-with-different-credential') {
 // Step 2.
 var pendingCred = error.credential;
 // The provider account's email address.
 var email = error.email;
 // Get registered providers for this email.
 firebaseApp.auth().fetchProvidersForEmail(email).then(function(providers) {
 // Step 3.
 // If the user has several providers,
 // the first provider in the list will be the "recommended" provider to use.
 if (providers[0] === 'password') {
 // Asks the user his password.
 // In real scenario, you should handle this asynchronously.
 var password = promptUserForPassword(); // TODO: implement promptUserForPassword to open the dialog to get the user entered password.
 firebaseApp.auth().signInWithEmailAndPassword(email, password).then(function(user) {
 // Step 4.
 return user.link(pendingCred);
 }).then(function() {
 // Google account successfully linked to the existing Firebase user.
 });
 }
 })}

要了解更多错误代码列表,请访问firebase.google.com/docs/reference/js/firebase.auth.Auth#signInWithPopup

管理刷新时的登录

目前,每次刷新页面时,我们的应用都会忘记用户已经登录。但是,Firebase 有一个事件监听器——onAuthStateChange()——可以在应用加载时检查身份验证状态是否已更改,以及用户上次访问应用时是否已经登录。如果是,那么您可以自动将其重新登录。

我们将把这个方法写在app.jscomponentDidMount()中。只需查看以下代码:

 componentWillMount() {
   this.removeAuthListener = firebase.auth().onAuthStateChanged((user) 
   =>{
    if(user){
     console.log("App user data",user);
     this.setState({
       authenticated:true,
       data:user.providerData
     })
  }
 else{
   this.setState({
     authenticated:false,
     data:''
 })}
 })}

此外,在componentWillUnmount()中,我们将删除该监听器以避免内存泄漏:

 componentWillUnmount(){
   this.removeAuthListener();
 }

现在,如果您刷新浏览器,它不会影响应用程序的状态;如果您已经登录,它将保持不变。

使用 Facebook API 或其他方式登录后,我们需要在 UI 中显示用户信息。为此,如果再次查看路由器组件,我们将使用userInfo属性将此用户信息发送到Home组件中:

<Route path="/" render={() => (<Home userInfo = {this.state.data} />)} />

Home组件的渲染方法中,我们将迭代包含成功登录到系统的用户数据的userInfo属性:

render() {
 var userPhoto = {width:"80px",height:"80px",margintop:"10px"}; 
 return (
 <div>
 {
 this.props.userInfo.map((profile)=> {
 return (
 <React.Fragment key={profile.uid}>
 <h2>{ profile.displayName } - Welcome to Helpdesk Application</h2>
 <div style={userPhoto}>
 <img src = { profile.photoURL } alt="user"/>
 <br/>
 <span><b>Eamil:</b></span> {profile.email }
 </div>
 </React.Fragment>
 )})
 }
 </div>
 )}

Logout()方法中,我们将简单地调用 firebase auth 中的signOut()方法;通过使用 Promise API,我们从应用程序状态中删除用户数据。现在this.state.data等于 null,用户将看到登录链接而不是注销按钮。它应该是这样的:

constructor() {
 super();
  this.state = {
    redirect: false,
    data:''
  }
 }
 componentWillMount(){
   firebaseApp.auth().signOut().then((user)=>{
     this.setState({
      redirect:true,
      data: null
   })
 })}
 render() {
 if(this.state.redirect === true){
 return <Redirect to = "/" />
 }
 return (
 <div style={{textAlign:"center",position:"absolute",top:"25%",left:"50%"}}>
 <h4>Logging out...</h4>
 </div>);
 }

使用电子邮件和密码进行身份验证

在 Firebase 中,我们还可以将您自己的身份验证系统与 Firebase 身份验证集成,以便用户可以访问数据,而无需强制他们使用现有系统的第三方 API 来创建帐户。Firebase 还允许匿名身份验证会话,通常用于在等待客户端使用永久的auth方法进行身份验证时保存少量数据。我们可以配置这个匿名会话,直到用户使用永久的login方法登录或清除他们的浏览器缓存的最后几天、几周、几个月,甚至几年。例如,一个购物车应用程序可以为每个将商品添加到购物车的用户创建一个匿名身份验证会话。购物车应用程序将提示用户创建一个帐户以进行结账;在那时,购物车将被持久化到新用户的帐户,并且匿名会话将被销毁。

支持的身份验证状态持久性类型

我们可以根据应用程序或用户的要求,在指定的 Firebase 身份验证instance(.auth())上使用三种持久性中的一种:

Auth 实例 描述
firebase.auth.Auth.Persistence.LOCAL 'local' 它表示即使关闭浏览器窗口或在 React Native 中销毁活动,状态也将被持久化。为此,需要显式注销以清除该状态。
firebase.auth.Auth.Persistence.SESSION 'session' 在这种情况下,状态将仅持续到当前会话或选项卡,并且在用户进行身份验证的选项卡或窗口关闭时将被清除。
firebase.auth.Auth.Persistence.NONE 'none' 当我们指定这个时,意味着状态只会存储在内存中,并且在窗口或应用程序刷新时将被清除。

考虑这个例子:

firebaseApp.auth().setPersistence('session')
 .then(function() {
 // Auth state is now persisted in the current
 // session only. If user directly close the browser window without doing signout then it clear the existing state
 // ...
 // New sign-in will be persisted with session.
 return firebase.auth().signInWithEmailAndPassword(email, password);
 })
 .catch(function(error) {
 // Handle Errors here.
 });

让我们创建一个名为authWithEmailPassword()的函数,并将以下代码添加到其中:

const email = this.emailField.value
const password = this.passwordField.value;
firebaseApp.auth().fetchProvidersForEmail(email).then((provider)=>{
 if(provider.length === 0){
 //Creating a new user
 return firebaseApp.auth().createUserWithEmailAndPassword(email,password);
 } else if(provider.indexOf("password") === -1){
 this.loginForm.reset();
 ToastDanger('Wrong Password. Please try again!!')
 } else {
 //signin user
 return firebaseApp.auth().signInWithEmailAndPassword(email,password);
 }}).then((user) => {
 if(user && user.email){
 this.loginForm.reset();
 this.setState({redirect: true});
 }})
 .catch((error)=>{
 console.log(error);
 ToastDanger(error.message);
 })

在上述代码中,首先,我们从表单中获取值。当用户点击提交按钮时,借助fetchProvidersForEmail(email),我们验证电子邮件是否存在于我们当前的 firebase 系统中;如果不存在,它将使用createUserWithEmailAndPassword()方法创建一个新用户。如果返回 true,我们将验证密码;如果用户输入了错误的密码,它将提示用户输入错误的密码,否则使用相同的方法—signInWithEmailAndPassword()—登录他们,并通过重定向 true 来更新组件的状态。

当我们在createUserWithEmailAndPassword()方法中创建新用户时,它会返回以下错误代码:

  • auth/email-already-in-use

  • auth/invalid-email

  • auth/operation-not-allowed(如果在 Firebase 控制台中未启用电子邮件/密码帐户。)

  • auth/weak-password(如果密码不够强大。)

当我们使用fetchProvidersForEmail(email)基于电子邮件获取提供程序时,它会返回以下错误代码:

  • auth/invalid-email(如果用户输入了无效的电子邮件)

阅读更多身份验证方法和错误代码的列表,请参考firebase.google.com/docs/reference/js/firebase.auth.Auth

我们还可以在我们的应用程序中使用以下 firebase 方法来操作用户:

var currentUser = firebase.auth().currentUser;
currentUser.updateProfile({
 displayName: “Harmeet Singh”,
 photoURL: “http://www.liferayui.com/g/200/300"
});
currentUser.sendPasswordResetEmail(“harmeetsingh090@gmail.com”); // Sends a temporary password
// Re-authentication is necessary for email, password and delete functions
var credential = firebase.auth.EmailAuthProvider.credential(email, password);
currentUser.reauthenticate(credential);
currentUser.updateEmail(“harmeetsingh090@gmail.com”);
currentUser.updatePassword(“D@#Log123”);
currentUser.delete();

成功登录后,我们将被重定向到应用程序仪表板页面,并且我们将能够看到完整的导航,可以添加和查看票务:

现在,如果您点击注销按钮,将不会发生任何事情,因为我们还没有创建任何logout组件。因此,在注销按钮中,我们需要做的就是简单地调用 firebase 的signOut()方法:

class Logout extends Component {
 constructor(props) {
 super();
  this.state = {
    redirect: props.authenticated,
    data:''
 }}
 componentWillMount(){
  firebaseApp.auth().signOut().then((user)=>{
    this.setState({
       redirect:true,
       data: null
   })
 })}
 render() {
 if(this.state.redirect === true){
    return <Redirect to = "/" />
 }
 return (
 <div style={{textAlign:"center",position:"absolute",top:"25%",left:"50%"}}>
   <h4>Logging out...</h4>
 </div>
 );
 }}

在上述代码中,我们创建了一个组件,并根据组件 props 中传递的值(authenticated)设置了状态;然后,在组件生命周期挂钩方法componentWillMount()中,我们调用了firebaseApp.auth().signout()方法,该方法登出用户并将其重定向到登录页面,并从状态中删除数据。

摘要

在本章中,我们看到了如何借助 Firebase 的身份验证系统使我们的应用程序免受未知用户的侵害。我们还了解了如何在 node 环境中配置 React-Firebase 应用程序,以及如何在 React 中创建登录表单并集成 Firebase 身份验证的登录方法,如 Google、Facebook 和电子邮件/密码。同样,我们也可以在应用程序中集成其他身份验证登录方法。

我们还介绍了根据 Firebase 身份验证错误代码处理身份验证错误的方法,这有助于我们在应用程序中执行操作。为了“持久化”身份验证状态,我们可以使用firebaseApp.auth().setPersistence('session')这个方法,它允许我们维护 Firebase 身份验证状态。

在下一章中,我们将探索 Redux 的强大功能,并使用 React、Redux 和 Firebase 创建一个实时的订票应用程序。

第四章:将 React 连接到 Redux 和 Firebase

在第三章中,使用 Firebase 进行身份验证,我们看到了 React 组件如何构建以及它们如何管理自己的状态。在本章中,我们将看看如何高效地管理应用程序状态。我们将详细探讨 Redux,并了解何时以及何时需要在我们的 React 应用程序中使用 Redux。我们还将看到如何将 React,Redux 和 Firebase 集成到一个示例座位预订应用程序中。这将是一个通用的座位预订应用程序,并且可以用作任何座位预订,例如公交车座位预订,体育场座位预订或剧院座位预订,在数据结构中进行一些小的更改。

以下是本章将涵盖的主题列表:

  • 使用 React Starter Kit 设置 React

  • Firebase 实时数据库和 React 的集成

  • Redux

  • React,Redux 和 Firebase 实时数据库的集成

  • 涵盖上述所有概念的座位预订应用程序

让我们设置开发环境。

要设置 React 开发环境,您需要使用 6.0 或更高版本的 node。

React 设置

为了设置我们的开发环境,第一步将是 React 设置。有不同的选项可用于安装 React。如果您已经有现有的应用程序并希望添加 React,可以使用包管理器(如npm)使用以下命令进行安装:

npm init
npm install --save react react-dom

但是,如果您要启动一个新项目,最简单的方法是使用 React Starter Kit 开始。只需转到命令提示符并执行以下命令以安装 React Starter kit:

npm install -g create-react-app

此命令将通过下载所有必需的依赖项来安装和设置本地开发环境。使用 node 拥有开发环境的许多好处,例如优化的生产构建,使用简单的npmyarn命令安装库等。

安装后,您可以使用给定的命令创建您的第一个应用程序:

create-react-app seat-booking

它将创建一个前端应用程序,并不包括任何后端逻辑或集成。它只是前端,因此您可以将其与任何后端技术集成或集成到现有项目中。

上述命令将花费一些时间来下载所有依赖项并创建项目,所以请耐心等待。

项目创建后,只需进入该文件夹并运行服务器:

cd seat-booking
npm start

一旦服务器启动,您可以在http://localhost:3000访问应用程序。

起始套件是开始使用 React 的最佳方式。但是,如果您是高级用户,可以通过使用以下命令手动配置项目来添加 React 依赖项:

npm init
npm install --save react react-dom

对于这个示例座位预订应用程序,我们将使用create-react-app命令。

项目结构将如下所示,如果您在 Visual Code 编辑器中查看:

创建的应用程序结构已经足够好了,但是对于我们的座位预订应用程序,我们需要以更好的包结构组织我们的源代码。

因此,我们将为操作、组件、容器和减速器创建不同的文件夹,如下面的屏幕截图所示。现在,只关注components文件夹,因为在其中,我们将放置我们的 React 组件。其余的文件夹与 Redux 有关,我们将在 Redux 部分中看到:

在应用程序开发的开始阶段识别组件非常重要,这样您就可以拥有更好的代码结构。

为了开始,我们的座位预订应用程序中将有以下 React 组件:

  • SeatSeat对象和应用程序的基本构建块

  • SeatRow:它表示一排座位

  • SeatList:它表示所有座位的列表

  • Cart:它表示将包含所选座位信息的购物车

请注意,组件的设计取决于应用程序的复杂性和应用程序的数据结构。

让我们从我们的第一个组件开始,称为 Seat。这将在components文件夹下。

components/Seat.js

import React from 'react'
import PropTypes from 'prop-types'

const Seat = ({ number, price, status }) => (
  <li className={'seatobj ' + status} key={number.toString()}>
   <input type="checkbox" disabled={status === "booked" ? true : false} id={number.toString()} onClick={handleClick}/>
   <label htmlFor={number.toString()}>{number}</label>
  </li>
)
const handleClick = (event) => {
  console.log("seat selected " + event.target.checked);
}

Seat.propTypes = {
   number:PropTypes.number,
   price:PropTypes.number,
   status:PropTypes.string
}

export default Seat;

在这里,需要注意的一件重要的事情是,我们正在使用 JSX 语法,这是我们在第二章中已经看到的,将 React 应用程序与 Firebase 集成

在这里,我们已经定义了具有三个属性的Seat组件:

  • number:指的是给该座位的编号或 ID

  • price:指的是预订此座位需要收取的金额

  • status:指的是座位状态,即已预订或可用

在 React 中,PropTypes用于验证组件接收的输入;例如,价格应该是一个数字而不是一个字符串。如果为属性提供了无效值,JavaScript 控制台将显示警告。出于性能原因,PropTypes检查只会在开发模式下进行。

在我们的座位预订应用程序中,当用户选择座位时,我们需要将其添加到购物车中,以便用户可以结账/预订票。为此,我们需要处理座位的onClick()。目前,我们只是在点击处理程序函数中打印一个控制台语句,但是我们需要编写一个逻辑将所选座位推送到购物车中。当我们在应用程序中集成 Redux 时,我们将在后面的部分中进行研究。

如果任何座位已经预订,显然我们不会允许用户选择它,因此基于状态,如果座位已预订,我们将禁用座位。

座位是我们的基本构建块,它将从父组件SeatRow接收数据。

components/SeatRow.js

import React from 'react'
import PropTypes from 'prop-types'
import Seat from './Seat';
const SeatRow = ({ seats, rowNumber }) => (
   <div>
      <li className="row">
        <ol className="seatrow">
           {seats.map(seat =>
             <Seat key={seat.number} number={seat.number}
                   price={seat.price}
                   status={seat.status}
             />
           )}
        </ol>
      </li>
    </div>
)
SeatRow.propTypes = {
   seats: PropTypes.arrayOf(PropTypes.shape({
      number: PropTypes.number,
      price: PropTypes.number,
      status: PropTypes.string
   }))
}
export default SeatRow;

SeatRow代表一排座位。我们正在创建松散耦合的组件,可以轻松维护并在需要时重用。在这里,我们正在迭代座位 JSON 数据数组,以渲染相应的Seat对象。

您可以在前面的代码块中看到,我们正在使用PropTypes验证我们的值。PropTypes.arrayOf表示座位的数组,PropTypes.shape表示Seat对象的 props。

我们的下一个组件是SeatList组件。

components/SeatList.js

import React from 'react'
import PropTypes from 'prop-types'
const SeatList = ({ title, children }) => (

  <div>
    <h3>{title}</h3>
    <ol className="list">
         {children}
    </ol>
  </div>

)
SeatList.propTypes = {
children: PropTypes.node,
title: PropTypes.string.isRequired
}
export default SeatList;

在这里,我们定义了一个SeatList组件,具有两个属性:

  • title: 用于座位预订的标题

  • children:它代表座位列表

与 proptypes 相关的两个重要事项:

  • Proptypes.string.isRequiredisRequired可以链接以确保如果接收到的数据无效,则在控制台中看到警告。

  • Proptypes.node:Node 表示可以呈现任何内容:数字、字符串、元素或包含这些类型的数组(或片段)。

我们应用程序中的下一个和最终组件是Cart

components/Cart.js

const Cart = () => {
 return (
    <div>
       <h3>No. of Seats selected: </h3>
       <button>
         Checkout
       </button>
    </div>
 )
}
export default Cart;

我们的购物车组件将有一个名为Checkout的按钮来预订票。它还将显示所选座位的摘要和需要支付的总费用。目前,我们只是放置了一个按钮和一个标签。一旦我们在应用程序中集成 Firebase 和 Redux,我们将对其进行修改。

因此,我们的展示组件已准备就绪。现在,让我们将 Firebase 与我们的应用程序集成。

集成 Firebase 实时数据库

现在是时候在我们的应用程序中集成 Firebase 了。虽然我们已经在第二章中看到了 Firebase 实时数据库的详细描述和特性,连接 React 到 Redux 和 Firebase,我们将看到 JSON 数据架构的关键概念和最佳实践。Firebase 数据库将数据存储为 JSON 树。

考虑以下示例:

{
  "seats" : {
    "seat-1" : {
      "number" : 1,
      "price" : 400,
      "rowNo" : 1,
      "status" : "booked"
    },
    "seat-2" : {
      "number" : 2,
      "price" : 400,
      "rowNo" : 1,
      "status" : "booked"
    },
    "seat-3" : {
      "number" : 3,
      "price" : 400,
      "rowNo" : 1,
      "status" : "booked"
    },
   "seat-4" : {
      "number" : 4,
      "price" : 400,
      "rowNo" : 1,
      "status" : "available"
    },
    ...
  }
}

数据库使用 JSON 树,但存储在数据库中的数据可以表示为某些原生类型,以帮助您编写更易维护的代码。如前面的例子所示,我们创建了一个类似seats > seat-#的树形结构。我们正在定义我们自己的键,比如seat-1seat-2,等等,但如果您使用push方法,它将被自动生成。

值得注意的是,Firebase 实时数据库数据嵌套可以达到 32 级深。然而,建议您尽量避免嵌套,并使用扁平化的数据结构。如果您有一个扁平化的数据结构,它将为您提供两个主要的好处:

  • 加载/获取所需的数据:您将只获取所需的数据,而不是完整的树,因为在嵌套树的情况下,如果您加载一个节点,您将同时加载该节点的所有子节点。

  • 安全性:您可以限制对数据的访问,因为在嵌套树的情况下,如果您给予对父节点的访问权限,这实际上意味着您也授予对该节点下数据的访问权限。

这里的最佳实践如下:

  • 避免嵌套数据

  • 使用扁平化的数据结构

  • 创建可扩展的数据

让我们首先创建我们的实时 Firebase 数据库:

我们可以直接在 Firebase 控制台上创建这个结构,或者创建这个 JSON 并将其导入 Firebase。我们的数据结构如下:

  • 座位:座位是我们的主节点,包含一个座位列表

  • 座位:座位是一个表示具有唯一编号、价格和状态的座位的单独对象

我们可以为我们的示例应用程序设计一个三级深度嵌套的数据结构,比如seats > row > seat,但正如前面的最佳实践中所述,我们应该设计一个扁平化的数据结构。

现在我们设计好了我们的数据,让我们在应用程序中集成 Firebase。在这个应用程序中,我们将使用npm添加它的模块,而不是通过 URL 添加 Fireabase 依赖项:

npm install firebase

这个命令将在我们的应用程序中安装 Firebase 模块,我们可以使用以下语句导入它:

import firebase from 'firebase';

导入语句是 ES6 的特性,所以如果你不了解它,请参考 ES6 文档es6-features.org/

我们将把与数据库相关的文件放在一个名为 API 的文件夹中。

api/firebase.js

import firebase from 'firebase'
var config = { /* COPY THE ACTUAL CONFIG FROM FIREBASE CONSOLE */
apiKey:"AIzaSyBkdkAcHdNpOEP_W9NnOxpQy4m1deMbG5Vooo",
authDomain:"seat-booking.firebaseapp.com",
databaseURL:"https://seat-booking.firebaseio.com",
projectId:"seat-booking",
storageBucket:"seat-booking.appspot.com",
messagingSenderId:"248063178000"
};
var fire = firebase.initializeApp(config);
export default fire;

上述代码将初始化 Firebase 实例,该实例可用于连接 Firebase。为了更好地关注点分离,我们还将创建一个名为service.js的文件,该文件将与我们的数据库交互。

api/service.js


import fire from './firebase.js';

export function getSeats() {
    let seatArr = [];
    let rowArray = [];
    const noOfSeatsInARow = 5;

    return new Promise((resolve, reject) => {
        //iterate through seat array and create row wise groups/array
        const seatsRef = fire.database().ref('seats/').orderByChild("number");
        seatsRef.once('value', function (snapshot) {
            snapshot.forEach(function (childSnapshot) {
                var childData = childSnapshot.val();
                seatArr.push({
                    number: childData.number,
                    price: childData.price,
                    status: childData.status,
                    rowNo: childData.rowNo
                });
            });

            var groups = [], i;
            for (i = 0; i < seatArr.length; i += noOfSeatsInARow) {
                groups = seatArr.slice(i, i + noOfSeatsInARow);
                console.log(groups);
                rowArray.push({
                    id: i,
                    seats: groups
                })
            }
            console.log(rowArray);
            resolve(rowArray);
        }).catch(error => { reject(error) });
    })

}

export function bookSelSeats(seats) {
    console.log("book seats", seats);
    return new Promise((resolve, reject) => {
        //write logic for payment 
        seats.forEach(obj => {
            fire.database().ref('seats/').child("seat-" + obj.number)
                .update({ status: "booked" }).then(resolve(true)).catch(error => { reject(error) });
        })
    });

}

在这个文件中,我们主要定义了两个函数——getSeats()bookSelSeats()——用于从数据库中读取座位列表和在用户从购物车中选中座位时更新座位。

Firebase 提供了两种方法——on()once()——用于在路径上读取数据并监听更改。ononce方法之间有区别:

  1. on 方法:它将监听数据更改,并在事件发生时接收数据库中指定位置的数据。此外,它不会返回Promise对象。

  2. once 方法:它将仅被调用一次,并且不会监听更改。它将返回一个Promise对象。

由于我们使用了 once 方法,所以我们得到一个返回到我们组件对象的Promise对象,因为从我们组件到服务的调用将是异步的。您将在接下来的App.js文件中更好地理解它。

要读取给定路径上内容的静态快照,我们可以使用value事件。当侦听器附加时,此方法仅执行一次,并且每次数据更改(包括子级)时都会执行。事件回调传递一个包含该位置的所有数据的快照,包括子数据。如果没有数据,则返回的快照为 null。

重要的是要注意,value事件将在给定路径上的数据每次更改时触发,包括子级的数据更改。因此,建议我们仅在需要限制快照大小的最低级别处附加侦听器。

在这里,我们正在从 Firebase 实时数据库中获取数据并获取所有座位。一旦我们获取数据,我们根据需要的格式创建一个 JSON 对象并返回它。

App.js将是我们的容器组件,并且将如下所示:

App.js

import React, { Component } from 'react';
import './App.css';
import SeatList from './components/SeatList';
import Cart from './components/Cart';
import { getSeats } from './api/service.js';
import SeatRow from './components/SeatRow';

class App extends Component {
  constructor() {
    super();
    this.state = {
      seatrows: [],
    }
  }

  componentDidMount() {
    let _this = this;
    getSeats().then(function (list) {
      console.log(list);
      _this.setState({
        seatrows: list,
      });
    });

  }

  render() {
    return (
      <div className="layout">
        <SeatList title="Seats">
          {this.state.seatrows.map((row, index) =>
            <SeatRow
              seats={row.seats}
              key={index}
            />
          )}

        </SeatList>
        <hr />
        <Cart />
      </div>
    )

  }
}

export default App;

在这里,我们可以看到App组件维护着状态。然而,我们的目标是将状态管理与我们的展示组件分离,并使用 Redux 来管理状态。

所以,现在我们已经准备好了所有的功能模块,但如果没有适当的设计和 CSS,它会是什么样子呢?我们必须设计一个用户友好的座位布局,所以让我们应用 CSS。我们有一个名为App.css的整个应用程序文件。如果需要,我们可以将它们分开放在不同的文件中。

App.css

.layout {
  margin: 19px auto;
  max-width: 350px;
}
*, *:before, *:after {
  box-sizing: border-box;
}
.list {
  border-right: 4px solid grey;
  border-left: 4px solid grey;
}

html {
  font-size: 15px;
}

ol {
  list-style: none;
  padding: 0;
  margin: 0;
}

.seatrow {
  display: flex;
  flex-direction: row;
  flex-wrap: nowrap;
  justify-content: flex-start;
}
.seatobj {
  display: flex;
  flex: 0 0 17.28571%;
  padding: 5px;
  position: relative;
}

.seatobj label {
  display: block;
  position: relative;
  width: 100%;
  text-align: center;
  font-size: 13px;
  font-weight: bold;
  line-height: 1.4rem;
  padding: 4px 0;
  background:#bada60;
  border-radius: 4px;
  animation-duration: 350ms;
  animation-fill-mode: both;
}

.seatobj:nth-child(2) {
  margin-right: 14.28571%;
}
.seatobj input[type=checkbox] {
  position: absolute;
  opacity: 0;
}
.seatobj input[type=checkbox]:checked + label {
  background: #f42530;
}

.seatobj input[type=checkbox]:disabled + label:after {
  content: "X";
  text-indent: 0;
  position: absolute;
  top: 4px;
  left: 49%;
  transform: translate(-49%, 0%);
}
.seatobj input[type=checkbox]:disabled + label:hover {
  box-shadow: none;
  cursor: not-allowed;
}

.seatobj label:before {
  content: "";
  position: absolute;
  width: 74%;
  height: 74%;
  top: 1px;
  left: 49%;
  transform: translate(-49%, 0%);
  border-radius: 2px;
}
.seatobj label:hover {
  cursor: pointer;
  box-shadow: 0 0 0px 3px yellowgreen;
}
.seatobj input[type=checkbox]:disabled + label {
  background: #dde;
  text-indent: -9999px;
  overflow: hidden;
}

我们已经完成了我们的最小座位预订应用程序。耶!以下是应用程序的截图。

下一张截图显示了默认布局,所有座位都可以预订:

下面的截图显示了已预订的票被标记为 X,所以用户无法选择它们。它还显示了当用户选择一个座位时,它会变成红色,这样他们就知道他们选择了哪些座位:

最后,我们的座位预订应用程序已经准备就绪,我们正在从 Firebase 数据库加载数据,并使用 React 显示它们。然而,看完前面的截图后,你一定会想,虽然我们已经选择了两个座位,但购物车是空的,没有显示任何座位的数据。如果你记得的话,我们还没有在seat click handler函数中编写任何逻辑来将选定的座位添加到购物车中,因此我们的购物车仍然是空的。

所以,现在的问题是,由于SeatCart组件之间没有直接关联,Seat组件如何与Cart组件进行通信?让我们找到这个问题的答案。

当组件之间没有关联或者关联但在层次结构中太远时,我们可以使用外部事件系统来通知任何想要监听的人。

Redux 是处理 React 应用程序中的数据和事件的流行选择。它是 Flux 模式的简化版本。让我们详细探讨一下 Redux。

Redux 是什么?

在这个技术时代,由于 Web 应用程序的要求变得越来越复杂,应用程序的状态管理在代码层面上面临许多挑战。例如,在实时应用程序中,除了持久化在数据库中的数据外,还有很多数据存储在缓存中以加快检索速度。同样,在 UI 方面,由于复杂的用户界面,如多个选项卡、多个路由、分页、过滤器和面包屑等,应用程序状态管理变得非常困难。

在任何应用程序中,存在不同的组件,它们相互交互以产生特定的输出或状态。也会有可能这种交互是如此复杂,以至于您失去了对应用程序状态的控制。例如,一个组件或模型更新另一个组件或模型,进而导致另一个视图的更新。这种类型的代码很难管理。添加新功能或修复错误变得具有挑战性,因为您不知道一个小改变何时会影响另一个正在工作的功能。

为了减少这种问题,像 React 这样的库移除了直接的 DOM 操作和异步性。然而,这仅适用于视图或表示层。数据的状态管理取决于应用程序开发人员。这就是 Redux 出现的原因。

Redux 是一个管理 JavaScript 应用程序状态的框架。这是官方网站上说的:

Redux 是 JavaScript 应用程序的可预测状态容器。

Redux 试图通过对状态变化施加一定的限制来使状态变化可预测。我们将很快看到这些限制是什么,以及它们是如何工作的,但在此之前,让我们先了解 Redux 的核心概念。

Redux 非常简单。当我们谈论 Redux 时,我们需要记住三个核心术语:存储器、动作和 Reducer。以下是它们:

  1. 存储器:应用程序的状态将由存储器管理,它是一个维护应用程序状态树的对象。请记住,在 Redux 应用程序中应该只有一个存储器。此外,由于施加了限制,您不能直接操作或改变应用程序存储器。

  2. 动作:要更改存储器中的内容,您需要分派一个动作。动作只是一个描述发生了什么的普通 JavaScript 对象。动作使我们能够理解应用程序中正在发生的事情以及为什么,因此使状态可预测。

  3. Reducer:最后,为了将动作和状态联系在一起,我们编写一个 Reducer,它是一个简单的 JavaScript 函数,接受状态和动作作为参数,并返回应用程序的新状态。

既然我们现在对 Redux 有了基本的了解,让我们来看看 Redux 的三个基本原则,如下所列:

  1. 单一真相来源:存储器只是一个状态容器。如前所述,在您的 React 应用程序中,应该只有一个存储器,因此它被视为真相的来源。单一对象树也使得调试应用程序变得容易。

  2. 状态是只读的:要更改状态,应用程序必须发出描述发生了什么的操作。没有视图或其他函数可以直接写入状态。这种限制可以防止任何不需要的状态更改。每个操作都将在一个集中的对象上执行,并且按顺序执行,以便我们可以在任何时间点获得最新的状态。由于操作只是普通对象,我们可以对它们进行调试和序列化以进行测试。

  3. 纯函数进行更改:要描述通过分派操作来转换状态树,编写纯函数/减速器。纯函数是什么意思?如果一个函数在传递给它的一组参数时每次返回相同的值,那么它就是纯的。纯函数不会修改它们的输入参数。相反,它们使用输入来计算一个值,然后返回该计算出的值。我们的减速器是纯函数,它们将状态和操作作为输入,并返回新状态而不是变异状态。您可以拥有尽可能多的减速器,并且建议将大的减速器拆分为可以管理应用程序树特定部分的小的减速器。减速器是 JavaScript 函数,因此您可以向它们传递附加数据。它们可以像普通函数一样构建,可以在展示和容器组件的整个应用程序中使用。

展示和容器组件

在我们的应用程序中,座位列表负责获取数据并呈现它。这对于小型或示例应用程序来说是可以的,并且效果很好,但是这样做,我们失去了 React 的一些好处,其中之一是可重用性。SeatList除非在完全相同的情况下,否则无法轻松重用,那么解决方案是什么?

我们知道这种问题在不同的编程语言中很常见,并且我们在设计模式方面有解决方案。同样,我们问题的解决方案是一种称为容器组件模式的模式。

因此,我们的容器组件将负责获取数据并将其传递给相应的子组件,而不是我们的 React 组件。简单来说,容器获取数据,然后呈现其子组件。

React 与 Redux 的绑定也接受了展示组件和容器组件的分离的想法。展示组件关注的是如何呈现给用户的外观,而不是关注事物如何运作。同样,容器组件关注的是事物如何运作,而不是外观。

让我们来看一下这个表格中的比较:

展示组件 容器组件
关注用户视图或事物外观的人 关注数据和事物如何运作的人
从父组件中获取/读取数据作为 props 与 Redux 状态连接
有自己的 DOM 标记和样式 很少或没有自己的 DOM 标记和样式
很少有状态 经常有状态
手写 可以手写或由 React Redux 生成

现在我们知道了展示组件和容器组件之间的区别,我们应该知道这种关注点分离的好处:

  • 组件的可重用性

  • 可以减少重复的代码,使应用程序更易管理

  • 不同的团队,比如设计师和 JS/应用程序开发人员可以并行工作

在我们开始将 Redux 集成到我们的应用程序之前,让我们先了解 Redux 的基本构建模块和 API。

Redux 的基础知识

Redux 相当简单,所以不要被 Reducers、Actions 等花哨的术语吓到。我们将介绍 Redux 应用程序的基本构建模块,你也会感到同样简单。

操作

正如我们在本章开头所看到的,一个操作只不过是一个描述发生了什么的普通 JavaScript 对象。改变状态就是发出描述发生了什么的操作。此外,对于存储,操作只是真相或信息的来源。

这里有一个示例操作创建者。

每种操作类型都应该被定义为一个常量:

const fetchSeats = rows => ({
    type: GET_SEATS,
    rows
})

操作的“类型”描述了已发生的操作的种类。如果你的应用程序足够大,你可以将操作类型分离为字符串常量,放到一个单独的模块/文件中,并在操作中使用它。

现在,你可能会有一个问题——我的操作应该是什么结构?我们

在这里有类型,然后直接添加了行。对于你的问题的答案是,除了类型,你可以有任何结构的操作。然而,有一个标准来定义一个操作。

动作必须是 JavaScript 对象,并且必须具有类型属性。此外,动作可能具有错误或有效负载属性。有效负载属性可以是任何类型的值。在前面的示例中,rows代表有效负载。

操作创建者

操作创建者是创建动作的函数;例如,function fetchSeats(){return{type:FETCH_SEATS,rows}}

操作通常在调用时触发分派,例如,dispatch(fetchSeats(seats))

请注意,操作创建者也可以是异步的,因此我们需要在逻辑中处理异步流。这是一个高级主题,可以在 Redux 网站上查阅。

减速器

动作只指定发生了什么,但不指定该动作对应用程序状态的影响是什么。减速器函数指定了应用程序状态如何更改。它是一个纯函数,接受两个参数——先前的状态和一个动作——并返回下一个更新的状态。

(previousState, action) =>newState

您在减速器内部绝对不要做的事情:

  • 修改其参数

  • API 调用和路由

  • 调用其他非纯函数,例如,Date.now()

在 Redux 中,一个对象代表应用程序状态。因此,在编写任何代码之前,非常重要的是考虑和决定应用程序状态对象的结构。

建议我们尽可能使我们的状态对象规范化,并避免对象的嵌套。

商店

正如最初所述,商店是保存应用程序状态树的主要对象。重要的是要注意,在 Redux 应用程序中将只有一个商店。当需要拆分数据处理逻辑时,您将使用 Reducer Composition 模式,而不是创建许多商店。

商店有以下方法可用:

  • getState(): 给出应用程序的当前状态树

  • dispatch(action): 用于分派动作

  • subscribe(listener): 订阅商店更改

  • replaceReducer(nextReducer): 这是一个高级 API,用于替换商店当前使用的 Reducer

数据流

我们已经看到了 Redux 架构的核心组件。现在,让我们了解所有这些组件实际上是如何一起工作的。Redux 架构仅支持单向数据流,如下图所示。这意味着应用程序中的所有数据都通过单向定义的工作流程,使您的应用程序逻辑更加简单。

Redux 中的高级主题

一旦您掌握了基础知识,还有一些高级主题需要学习,例如React RouterAjax 和异步动作中间件。我们不会在这里涵盖它们,因为它们超出了本书的范围。但是,我们将简要讨论中间件这个重要主题。

默认情况下,Redux 仅支持同步数据流。要实现异步数据流,您需要使用中间件。中间件只是一个为您的调度方法提供包装器并允许传递函数和承诺而不仅仅是动作的框架或库。中间件主要用于支持异步动作。有许多中间件,例如用于异步动作的 redux-thunk。中间件还可用于日志记录或崩溃报告。我们还将在我们的应用程序中使用 redux-thunk。要增强createStore(),我们需要使用applyMiddleware(...middleware)函数。

使用 Redux 进行座位预订

通过集成 Redux 来增强我们的座位预订应用程序。

我们可以显式安装 React 绑定,使用以下命令,因为它们不是默认包含在 Redux 中的:

npm install --save react-redux

现在,我们将通过集成 Redux 来扩展我们的座位预订应用程序。这将会对我们所有的组件产生很多变化。在这里,我们将从我们的入口点开始。

src/index.js

import React from 'react'
import { render } from 'react-dom'
import { createStore, applyMiddleware } from 'redux'
import SeatBookingApp from './containers/SeatBookingApp'
import { Provider } from 'react-redux'
import { createLogger } from 'redux-logger'
import thunk from 'redux-thunk'
import reducer from './reducers'
import { getAllSeats } from './actions'

const middleware = [thunk];
//middleware will print the logs for state changes
if (process.env.NODE_ENV !== 'production') {
    middleware.push(createLogger());
}

const store = createStore(
    reducer,
    applyMiddleware(...middleware)
)

store.dispatch(getAllSeats())

render(
    <Provider store={store}>
        <SeatBookingApp />
    </Provider>,
    document.getElementById('root')
)

让我们了解我们的代码:

  • <Provide store>:使 Redux Store 可用于组件层次结构。请注意,您不能在没有包装父组件的情况下使用connect()

  • <SeatBookingApp>:这是我们的父组件,并将在容器组件包中定义。它将具有类似于我们之前在App.js中看到的代码。

  • 中间件:它类似于其他语言中的拦截器,提供了在派发动作和达到减速器之间的第三方扩展点,例如日志记录或记录器。如果不应用中间件,您将需要在所有动作和减速器中手动添加记录器。

  • applyMiddleware:它告诉 Redux 存储如何处理和设置中间件。请注意 rest 参数(...middleware)的使用;它表示applyMiddleware函数接受多个参数(任意数量)并可以将它们作为数组获取。中间件的一个关键特性是可以将多个中间件组合在一起。

根据 Redux 状态管理,我们还需要稍微更改我们的展示组件。

让我们从购物车组件开始。

components/Cart.js:

import React from 'react'
import PropTypes from 'prop-types'
const Cart = ({seats,total, onCheckoutClicked}) => {

  const hasSeats = seats.length > 0
  const nodes = hasSeats ? (

    seats.map(seat =>
      <div>
        Seat Number: {seat.number} - Price: {seat.price}
      </div>
    )

  ) : (
    <em>No seats selected</em>
  )

  return (
    <div>
    <b>Selected Seats</b> <br/>
      {nodes} <br/>
    <b>Total</b> <br/>
      {total}
      <br/>
      <button onClick={onCheckoutClicked}>
        Checkout
      </button>
    </div>
  )
}

Cart.propTypes = {
  seats: PropTypes.array,
  total: PropTypes.string,
  onCheckoutClicked: PropTypes.func
}
export default Cart;

我们的购物车组件从父组件接收一个结账函数,该函数将分发结账操作以及添加到购物车中的座位。

components/Seat.js:

import React from 'react'
import PropTypes from 'prop-types'

const Seat = ({ number, price, status, rowNo, handleClick }) => {
  return (

    <li className={'seatobj ' + status} key={number.toString()}>
      <input type="checkbox" disabled={status === "booked" ? true : false} id={number.toString()} onClick={handleClick} />
      <label htmlFor={number.toString()}>{number}</label>
    </li>

  )
}

Seat.propTypes = {
  number: PropTypes.number,
  price: PropTypes.number,
  status: PropTypes.string,
  rowNo: PropTypes.number,
  handleClick: PropTypes.func
}

export default Seat;

我们的座位组件从父组件接收其状态,以及分发ADD_TO_CART操作的handleClick函数。

components/SeatList.js:

import React from 'react'
import PropTypes from 'prop-types'

const SeatList = ({ title, children }) => (

  <div>
    <h3>{title}</h3>
    <ol className="list">
      {children}
    </ol>
  </div>
)

SeatList.propTypes = {
  children: PropTypes.node,
  title: PropTypes.string.isRequired
}

export default SeatList;

SeatList从容器组件接收座位数据。

components/SeatRow.js:

import React from 'react'
import PropTypes from 'prop-types'
import Seat from './Seat';

const SeatRow = ({ seats, rowNumber, onAddToCartClicked }) => {
  return (
  <div>
    <li className="row row--1" key="1">
      <ol className="seatrow">
        {seats.map(seat =>
          <Seat key={seat.number} number={seat.number}
            price={seat.price}
            status={seat.status}
            rowNo={seat.rowNo} 
            handleClick={() => onAddToCartClicked(seat)}
          />
        )}

      </ol>
    </li>
  </div>
)
}
SeatRow.propTypes = {
  seats: PropTypes.arrayOf(PropTypes.shape({
    number: PropTypes.number,
    price: PropTypes.number,
    status: PropTypes.string,
    rowNo: PropTypes.number
  })),
  rowNumber: PropTypes.number,
  onAddToCartClicked: PropTypes.func.isRequired
}

export default SeatRow;

SeatRow接收该特定行的所有座位。

让我们检查我们的容器组件。

containers/SeatBookingApp.js:

import React from 'react'
import SeatContainer from './SeatContainer'
import CartContainer from './SeatCartContainer'
import '../App.css';

const SeatBookingApp = () => (
    <div className="layout">
        <h2>Ticket Booking App</h2>
        <hr />
        <SeatContainer />
        <hr />
        <CartContainer />
    </div>
)
export default SeatBookingApp; 

它是我们的父组件,并包括其他子容器组件:SeatContainerCartContainer

container/SeatCartContainer.js:

import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { bookSeats } from '../actions'
import { getTotal, getCartSeats } from '../reducers'
import Cart from '../components/Cart'

const CartContainer = ({ seats, total, bookSeats }) => {

    return (

    <Cart
        seats={seats}
        total={total}
        onCheckoutClicked={() => bookSeats(seats)}
    />
)
}
CartContainer.propTypes = {
    seats: PropTypes.arrayOf(PropTypes.shape({
        number: PropTypes.number.isRequired,
        rowNo: PropTypes.number.isRequired,
        price: PropTypes.number.isRequired,
        status: PropTypes.string.isRequired
    })).isRequired,
    total: PropTypes.string,
    bookSeats: PropTypes.func.isRequired
}
const mapStateToProps = (state) => ({
    seats: getCartSeats(state),
    total: getTotal(state)
})

export default connect(mapStateToProps, {bookSeats})(CartContainer)

这个容器组件将与存储交互,并将数据传递给子组件 - 购物车。

让我们理解代码:

  1. mapStateToProps: 这是一个函数,每次存储更新时都会调用它,这意味着组件订阅了存储更新。

  2. {bookSeats}: 它可以是 Redux 提供的一个函数或对象,以便容器可以轻松地将该函数传递给其子组件的 props。我们传递bookSeats函数,以便购物车组件中的“结账”按钮可以调用它。

  3. connect(): 将 React 组件连接到 Redux 存储。

让我们看看下一个容器 - SeatContainer

containers/SeatContainer.js:

import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { addSeatToCart } from '../actions'
import SeatRow from '../components/SeatRow'
import SeatList from '../components/SeatList'
import { getAllSeats } from '../reducers/seats';

const SeatContainer = ({ seatrows, addSeatToCart }) => {
    return (

    <SeatList title="Seats">
        {seatrows.map((row, index) =>
            <SeatRow key={index}
                seats={row.seats}
                rowNumber={index}
                onAddToCartClicked={addSeatToCart} />

        )}

    </SeatList>

)
}
SeatContainer.propTypes = {
    seatrows: PropTypes.arrayOf(PropTypes.shape({
        number: PropTypes.number,
        price: PropTypes.number,
        status: PropTypes.string,
        rowNo: PropTypes.number
    })).isRequired,
    addSeatToCart: PropTypes.func.isRequired
}

const mapStateToProps = state => ({
    seatrows: getAllSeats(state.seats)
})

export default connect(mapStateToProps,  { addSeatToCart })(SeatContainer)

如前所述,对于CartContainer,我们将为SeatContainer有类似的代码结构。

现在,我们将创建一个constants文件,定义我们的Actions的常量。虽然您可以直接在操作文件中定义常量,但将常量定义在单独的文件中是一个很好的做法,因为这样更容易维护清晰的代码。

constants/ActionTypeConstants.js:

//get the list of seats
export const GET_SEATS = 'GET_SEATS'
//add seats to cart on selection
export const ADD_TO_CART = 'ADD_TO_CART'
//book seats
export const CHECKOUT = 'CHECKOUT'

我们将有三个操作:

  • 获取座位:从 Firebase 获取座位数据并在 UI 上填充

  • ADD_TO_CART: 将选定的座位添加到用户购物车中

  • CHECKOUT: 预订座位

让我们在一个名为index.js的文件中定义操作。

actions/index.js:

import { getSeats,bookSelSeats } from '../api/service';
import { GET_SEATS, ADD_TO_CART, CHECKOUT } from '../constants/ActionTypeConstants';

//action creator for getting seats
const fetchSeats = rows => ({
    type: GET_SEATS,
    rows
})

//action getAllSeats
export const getAllSeats = () => dispatch => {
    getSeats().then(function (rows) {
        dispatch(fetchSeats(rows));
    });
}

//action creator for add seat to cart
const addToCart = seat => ({
    type: ADD_TO_CART,
    seat
})

export const addSeatToCart = seat => (dispatch, getState) => {
    dispatch(addToCart(seat))

}

export const bookSeats = seats => (dispatch, getState) => {
    const { cart } = getState()
    bookSelSeats(seats).then(function() {
        dispatch({
            type: CHECKOUT,
            cart
        })
    });

}

操作使用dispatcher()方法将数据从应用程序发送到存储。在这里,我们有两个函数:

  • fetchSeats(): 它是一个操作创建者,用于创建GET_SEATS操作

  • getAllSeats(): 这是一个实际的操作,通过调用我们的服务的getSeats()方法获取数据并将其分发到存储中

同样,我们可以为另外两个操作定义我们的操作:ADD_TO_CARTCHECKOUT

现在,让我们来看看 reducers。我们将从座位 reducer 开始。

reducers/seats.js

import { GET_SEATS } from "../constants/ActionTypeConstants";
import { combineReducers } from 'redux'

const seatRow = (state = {}, action) => {
    switch (action.type) {
        case GET_SEATS:
            return {
                ...state,
                ...action.rows.reduce((obj, row) => {
                    obj[row.id] = row
                    return obj
                }, {})
            }
        default:
            return state
    }
}

const rowIds = (state = [], action) => {
    switch (action.type) {
        case GET_SEATS:
            return action.rows.map(row => row.id)
        default:
            return state
    }
}

export default combineReducers({
    seatRow,
    rowIds
})

export const getRow = (state, number) =>
    state.seatRow[number]

export const getAllSeats = state =>
    state.rowIds.map(number => getRow(state, number))

让我们理解这段代码:

  • combineReducers:我们将 Reducer 拆分为不同的函数——rowIdsseatRow——并将根 reducer 定义为调用管理状态不同部分的 reducer 的函数,并将它们合并为一个单一对象

同样,我们将有购物车 reducer。

reducers/cart.js

import {
    ADD_TO_CART,
    CHECKOUT
} from '../constants/ActionTypeConstants'

const initialState = {
    addedSeats: []
}

const addedSeats = (state = initialState.addedSeats, action) => {
    switch (action.type) {
        case ADD_TO_CART:
        //if it is already there, remove it from cart
        if (state.indexOf(action.seat) !== -1) {
            return state.filter(seatobj=>seatobj!=action.seat);
          }
        return [...state, action.seat]
        default:
            return state
    }
}

export const getAddedSeats = state => state.addedSeats

const cart = (state = initialState, action) => {
    switch (action.type) {
        case CHECKOUT:
            return initialState
        default:
            return {
                addedSeats: addedSeats(state.addedSeats, action)
            }
    }
}

export default cart

它公开了与购物车操作相关的 reducer 函数。

现在,我们将有一个最终的 combine reducer。

reducers/index.js

import { combineReducers } from 'redux'
import seats from './seats'
import cart, * as cartFunc from './cart'

export default combineReducers({
    cart,
    seats
})

const getAddedSeats = state => cartFunc.getAddedSeats(state.cart)

export const getTotal = state =>
    getAddedSeats(state)
        .reduce((total, seat) =>
            total + seat.price,
        0
        )
        .toFixed(2)

export const getCartSeats = state =>
    getAddedSeats(state).map(seat => ({
        ...seat
    }))

它是座位和购物车的combineReducers。它还公开了一些常用函数来计算购物车中的总数,以及获取购物车中添加的座位。就是这样。我们最终引入了 Redux 来管理我们应用程序的状态,并且使用 React、Redux 和 Firebase 准备好了我们的座位预订应用程序。

总结

在本章中,我们深入探讨了 React 和 Firebase。我们谈到了 Firebase 数据库中的数据结构,并且看到我们应该尽可能避免数据嵌套。我们还看到了关于数据读取的ononce方法以及在数据库中数据发生变化时触发的“value”事件。我们还学习了 Redux 的核心概念,并看到了使用 Redux 来管理应用程序状态有多么容易。我们还讨论了展示组件和容器组件之间的区别以及它们应该如何设计。然后,我们讨论了 Redux 的基础知识,也简要讨论了 Redux 的高级主题。

此外,我们使用了 React、Redux 和 Firebase 创建了一个座位预订应用程序,并看到了它们顺利集成的实际实例。

在下一章中,我们将探索 Firebase Admin SDK,并看看如何实现用户和访问管理。

第五章:用户个人资料和访问管理

在上一章中,我们看到了如何在 react-redux 应用程序中使用 Firebase。我们还详细探讨了 Redux,并了解了在我们的 React 应用程序中何时以及为何需要使用 Redux,以及 Firebase 实时数据库将在我们的应用程序中提供实时的座位预订状态。在本章中,我们将介绍 Firebase Admin SDK,它提供了一个用户管理 API,以完全的管理员权限读取和写入实时数据库数据。因此,我们将为我们的应用程序创建一个管理员页面,在这个页面上我们有能力执行以下操作:

  • 创建新用户

  • 用户搜索引擎,我们可以按不同的标准搜索用户

  • 所有用户的列表

  • 访问用户元数据,其中包括特定用户的帐户创建日期和最后登录日期

  • 删除用户

  • 更新用户信息,而无需以用户身份登录

  • 验证电子邮件

  • 更改用户的电子邮件地址,而不发送电子邮件通知以撤销这些更改

  • 创建一个带有电话号码的新用户,并更改用户的电话号码而不发送短信验证

首先,我们需要在 Node.js 环境中设置 Firebase Admin SDK 以作为管理员执行上述操作。

设置 Firebase Admin SDK

要使用 Firebase Admin SDK,我们需要一个 Firebase 项目,其中有服务账户与 Firebase 服务通信,并包含服务账户凭据的配置文件。

要配置 Firebase Admin SDK,请按照以下步骤进行:

  1. 登录到Firebase 控制台,选择<project_name>项目,并点击项目概述中的设置图标:

概述选项卡

  1. 转到项目设置中的服务帐户选项卡。

  2. 点击 Firebase 管理部分底部的 GENERATE PRIVATE KEY 按钮;它将生成包含服务账户凭据的 JSON 文件:

这个 JSON 文件包含了关于您的服务账户和私人加密密钥的非常敏感的信息。因此,永远不要分享和存储在公共存储库中;保持它的机密性。如果因为任何原因我们丢失了这个文件,那么我们可以再次生成它,并且我们将不再使用旧文件访问 Firebase Admin SDK。

Firebase CLI

Firebase 提供了一个命令行界面,提供了各种工具来创建、管理、查看和部署 Firebase 项目。使用 Firebase CLI,我们可以轻松地在生产级静态托管上部署和托管我们的应用程序,并且它会自动通过全球 CDN 提供 HTTPS 服务。

安装

在安装之前,请确保我们在计算机上安装了 Node.js 4.0+。如果尚未安装,请从nodejs.org下载 Node.js 8 的最新版本“LTS” 安装完成后,我们可以从npm(node 包管理器)下载 Firebase CLI。

运行以下命令在您的系统上全局安装 Firebase CLI:

npm install -g firebase-tools

要验证安装,请运行以下命令;如果在您的系统上正确安装了 Firebase CLI,它将打印 Firebase CLI 版本:

firebase --version

Firebase Admin 集成

现在我们已经成功安装了 Firebase CLI,让我们将现有应用程序代码从第三章,使用 Firebase 进行身份验证复制到第五章,用户配置文件和访问管理的新目录中。在这里,我们将初始化 Firebase 应用程序,并运行以下命令在初始化应用程序之前登录到 Firebase 控制台:

firebase login

一旦您成功登录到 Firebase 控制台,请运行以下命令初始化项目:

firebase init

运行此命令后,它将提示您选择 Firebase 功能、项目和目录文件夹(相对于您的项目目录),该文件夹将包含要与firebase deploy命令一起上传的hosting资产(默认情况下为 public)。

我们也可以在项目中随后添加功能,并且也可以将多个项目与同一目录关联。

一旦 Firebase 初始化完成,请运行以下命令安装项目依赖项,然后构建项目:

//run this command to install the project dependencies
npm install

//run this command to build the project
npm run build

为了在部署到生产环境之前在本地验证我们的应用程序,请运行以下命令:

firebase serve

它将从构建目录或您在firebase.json文件中定义的名称启动本地服务器:

这是我们的文件夹结构在使用 Firebase CLI 初始化后的样子。

使用 Firebase Admin Auth API 与 React

Firebase Admin SDK 将使我们能够使用 Firebase Auth API 集成自己的服务器。使用 Firebase Admin SDK,我们可以管理我们的应用程序用户,如查看创建更新删除,而无需用户的凭据或管理身份验证令牌而无需转到 Firebase Admin 控制台。

为了实现这一点,我们将在现有的 React 应用程序中创建管理面板。

以下是我们将使用 Firebase Admin SDK 集成到我们的管理面板中的功能列表:

  • 创建和验证自定义令牌

  • 具有自定义用户声明的用户级访问角色

  • 查看应用用户列表

  • 获取用户个人资料

  • 创建删除更新用户信息

  • 解决工单状态

初始化 Admin SDK

正如我们所看到的,Firebase admin SDK 仅在 Node.Js 中受支持,因此我们将使用 npm init 创建一个新项目,并从npm包中安装 firebase admin。

运行以下命令安装 firebase admin 并将其保存在您的package.json中:

npm install firebase-admin --save

将以下代码片段复制到您的 JS 文件中并初始化 SDK;我们已经添加了从 Firebase Admin 服务帐户下载的 JSON 文件的引用:

const admin = require('firebase-admin');
const serviceAccount = require('./firebase/serviceAccountKey.json');

admin.initializeApp({
    credential: admin.credential.cert(serviceAccount),
    databaseURL: "https://demoproject-7cc0d.firebaseio.com"
});

现在我们将只需创建 Restful API 与客户端应用程序交互以访问 Admin SDK 功能。

运行此命令启动节点管理员服务器:

node <fileName>

它将在不同端口上启动本地服务器,例如http://localhost:3001

创建和验证自定义令牌

Firebase Admin SDK 为我们提供了使用外部机制(如 LDAP 服务器或第三方 OAuth 提供程序)对用户进行身份验证的能力,Firebase 不支持这些机制,如 Instagram 或 LinkedIn。我们可以使用 Firebase 内置的 Admin SDK 中的自定义令牌方法来执行所有这些操作,或者我们可以使用任何第三方 JWT 库。

让我们看看如何使用 Admin SDK 创建和验证令牌。

要创建自定义令牌,我们必须有一个有效的uid,我们需要在createCustomToken()方法中传递它:

function createCustomToken(req,res){
 const userId = req.body.uid "guest_user"
 admin.auth().createCustomToken(userId)
 .then(function(customToken) {
 res.send(customToken.toJSON());
 })
 .catch(function(error) {
 console.log("Error creating custom token:", error);
 });
}

在前面的函数中,当用户使用用户名和密码登录时,我们从客户端获取uid,如果凭据有效,我们将从服务器返回自定义 JWT(JSON Web Token),客户端设备可以使用它来与 Firebase 进行身份验证:

app.get('/login', function (req, res) {
 if(validCredentials(req.body.username,req.body.password)){
    createCustomToken(req,res);
 }
})

一旦经过身份验证,此身份将用于访问 Firebase 服务,如 Firebase 实时数据库和云存储。

如果需要,我们还可以添加一些附加字段以包含在自定义令牌中。考虑这段代码:

function createCustomToken(req,res){
 const userId = req.body.uid
 const subscription = {
   paid:true
 }
 admin.auth().createCustomToken(userId)
 .then(function(customToken) {
   res.send(customToken.toJSON());
 })
 .catch(function(error) {
 console.log("Error creating custom token:", error);
 });
}

这些附加字段将在安全规则中的auth/request.auth对象中可用。

一旦令牌生成并被 React 方法接收,我们将通过将自定义令牌传递给 Firebase 的signInWithCustomToken()方法来对用户进行应用程序身份验证:

const uid = this.state.userId
fetch('http://localhost:3000/login', {
   method: 'POST', // or 'PUT'
   body: JSON.stringify({idToken:idToken}), 
   headers: new Headers({
     'Content-Type': 'application/json'
  })
 }).then(res => res.json())
 .catch(error => console.error('Error:', error))
 .then(res => {
  console.log(res,"after token valid");
  firebase.auth().signInWithCustomToken(res.customToken).catch(function(error) {
    var errorCode = error.code;
    var errorMessage = error.message;
 });
})

成功验证后,用户使用我们在创建自定义令牌方法中包含的uid指定的帐户登录到我们的应用程序。

同样,其他 Firebase 身份验证方法的工作方式类似于signInWithEmailAndPassword()signInWithCredential(),并且auth/request.auth对象将在 Firebase 实时数据库安全规则中与用户uid一起可用。在前面的示例中,我们指定了为什么生成自定义令牌。

//Firebase Realtime Database Rules
{
 "rules": {
 "admin": {
 ".read": "auth.uid === 'guest_user'"
 }
 }
}
//Google Cloud Storage Rules
service firebase.storage {
 match /b/<firebase-storage-bucket-name>/o {
 match /admin/{filename} {
 allow read, write: if request.auth.uid == "guest_user";
 }
 }
}

同样,我们还可以访问其他传递的附加对象,这些对象在auth.tokenrequest.auth.token中可用:

//Firebase Realtime Database Rules
{
 "rules": {
 "subscribeServices": {
 ".read": "auth.token.paid === true"
 }
 }
}
service firebase.storage {
 match /b/<firebase-storage-bucket-name>/o {
 match /subscribeServices/{filename} {
 allow read, write: if request.auth.token.paid === true;
 }
 }
}

Firebase 还可以为我们提供一种获取uid的方法,一旦用户登录到应用程序中,它会创建一个唯一标识他们的相应 ID 令牌,我们可以将此令牌发送到服务器进行验证,并允许他们访问应用程序的多个资源。例如,当我们创建一个自定义后端服务器与应用程序通信时,我们可能需要使用 HTTPS 安全地识别当前登录的用户。

要从 Firebase 中检索 ID 令牌,请确保用户已登录到应用程序,并且我们可以使用以下方法在您的 React 应用程序中检索 ID 令牌:

firebase.auth().currentUser.getIdToken(/* forceRefresh */ true).then(function(idToken) {
 // Send this token to custom backend server via HTTPS
}).catch(function(error) {
 // Handle error
});

一旦我们有了这个 ID 令牌,我们可以将这个 JWT(JSON Web Token)发送到后端服务器的 Firebase Admin SDK 或任何第三方库进行验证。

为了验证和解码 ID 令牌,Firebase Admin SDK 具有内置的verifyIdToken(idToken)方法;如果提供的令牌未过期、有效且经过正确签名,该方法将返回解码的 ID 令牌:

function validateToken(req,res){
  const idToken= req.body.idToken;
  admin.auth().verifyIdToken(idToken)
   .then(function(decodedToken) {
   var uid = decodedToken.uid;
  //...
  }).catch(function(error) {
 // Handle error
 });
}

现在,让我们扩展我们现有的应用程序,用户只能看到他们提交的那些票证,并且我们还将给用户更新现有个人资料的能力。我们还将在 React 中创建一个管理员面板,并根据角色向用户显示管理员 UI。

用于管理员访问和安全规则的自定义声明

正如我们之前看到的,Firebase Admin SDK 支持在令牌中定义自定义属性。这些自定义属性使我们能够定义不同级别的访问权限,包括基于角色的应用程序安全规则中强制执行的访问控制。

我们需要在以下常见情况下定义用户角色:

  • 为用户分配管理员角色以访问资源

  • 为用户分配不同的组

  • 为用户提供多级访问权限,例如付费用户、普通用户、经理、支持团队等

我们还可以根据数据库定义规则,限制访问,例如我们有数据库节点helpdesk/tickets/all,所有数据票务数据都可以被访问。但是,我们只希望管理员用户能够查看所有票务。为了更有效地实现这一目标,验证电子邮件 ID 并添加名为 admin 的自定义用户声明到以下实时数据库规则中:

{
 "rules": {
  "helpdesk":{
   "tickets":{
       "all": {
         ".read": "auth.token.admin === true",
         ".write": "auth.token.admin === true",
         }
        }
      }
   }
}

不要将自定义声明与自定义身份验证和 Firebase 身份验证混淆。它适用于已使用受支持提供程序(电子邮件/密码、Github、Google、Facebook、电话等)登录的用户,但当我们使用 Firebase 不支持的不同身份验证时,将使用自定义身份验证。例如,使用 Firebase Auth 的电子邮件/密码提供程序登录的用户可以使用自定义声明定义访问控制。

使用 Admin SDK 添加自定义声明

在 Firebase Admin SDK 中,我们可以使用setCustomUserClaims()方法应用自定义声明,该方法内置于 Firebase 中:

admin.auth().setCustomUserClaims(uid, {admin: true}).then(() => {
});

使用 Admin SDK 发送应用程序验证自定义声明

Firebase Admin SDK 还为我们提供了使用verifyIdToken()方法验证令牌的方法:

 admin.auth().verifyIdToken(idToken).then((claims) => {
  if (claims.admin === true) {
    // Allow access to admin resource.
   }
 });

我们还可以检查用户对象中是否有自定义声明可用:

admin.auth().getUser(uid).then((userRecord) => {
   console.log(userRecord.customClaims.admin);
});

现在,让我们看看如何在我们现有的应用程序中实现这一点。

首先,在 Node Admin SDK 后端服务器中创建一个 restful API:

app.post('/setCustomClaims', (req, res) => {
 // Get the ID token passed by the client app.
 const idToken = req.body.idToken;
 console.log("accepted",idToken,req.body);
 // Verify the ID token
 admin.auth().verifyIdToken(idToken).then((claims) => {
 // Verify user is eligible for admin access or not
 if (typeof claims.email !== 'undefined' &&
 claims.email.indexOf('@adminhelpdesk.com') != -1) {
 // Add custom claims for admin access.
 admin.auth().setCustomUserClaims(claims.sub, {
 admin: true,
 }).then(function() {
 // send back to the app to refresh token and shows the admin UI.
 res.send(JSON.stringify({
 status: 'success',
 role:'admin'
 }));
 });
 } else if (typeof claims.email !== 'undefined'){
 // Add custom claims for admin access.
 admin.auth().setCustomUserClaims(claims.sub, {
 admin: false,
 }).then(function() {
 // Tell client to refresh token on user.
 res.send(JSON.stringify({
 status: 'success',
 role:'employee'
 }));
 });
 }
 else{
 // return nothing
 res.send(JSON.stringify({status: 'ineligible'}));
 }
 })
 });

我已经使用管理 SDK 在 Firebase 控制台中手动创建了一个名为harmeet@adminhelpdesk.com的管理员用户;我们需要验证并为管理员添加自定义声明。

现在,打开App.JSX并添加以下代码片段;根据角色设置应用程序的初始状态:

  constructor() {
  super();
  this.state  = {  authenticated :  false,
  data:'',
  userUid:'',
  role:{
  admin:false,
 type:''
 } } }

现在,在componentWillMount()组件生命周期方法中调用上述 API,我们需要从firebase.auth().onAuthStateChanged((user))中获取用户对象的idToken并将其发送到服务器进行验证:

this.getIdToken(user).then((idToken)=>{
 console.log(idToken);
 fetch('http://localhost:3000/setCustomClaims', {
   method: 'POST', // or 'PUT'
   body: JSON.stringify({idToken:idToken}), 
   headers: new Headers({
     'Content-Type': 'application/json'
   })
 }).then(res => res.json())
  .catch(error => console.error('Error:', error))
  .then(res => {
   console.log(res,"after token valid");
   if(res.status === 'success' && res.role === 'admin'){
      firebase.auth().currentUser.getIdToken(true);
       this.setState({
         authenticated:true,
         data:user.providerData,
         userUid:user.uid,
             role:{
                 admin:true,
                 type:'admin'
             }
     })
 }
 else if (res.status === 'success' && res.role === 'employee'){
 this.setState({
     authenticated:true,
     data:user.providerData,
     userUid:user.uid,
     role:{
         admin:false,
         type:'employee'
         }
     })
 }
 else{
     ToastDanger('Invalid Token !!')
 }

在上述代码中,我们使用fetch API 发送 HTTP 请求。它类似于 XMLHttpRequest,但具有新功能并且更强大。根据响应,我们设置组件的状态并将组件注册到路由器中。

这是我们的路由组件的样子:

{
 this.state.authenticated && !this.state.role.admin
 ?
 (
 <React.Fragment>
 <Route path="/view-ticket" render={() => (
 <ViewTicketTable userId = {this.state.userUid} />
 )}/>
 <Route path="/add-ticket" render={() => (
 <AddTicketForm userId = {this.state.userUid} userInfo = {this.state.data} />
 )}/>
 <Route path="/user-profile" render={() => (
 <ProfileUpdateForm userId = {this.state.userUid} userInfo = {this.state.data} />
 )}/>
 </React.Fragment>
 )
 :
 (
 <React.Fragment>
   <Route path="/get-alluser" component = { AppUsers }/>
   <Route path="/tickets" component = { GetAllTickets }/>
   <Route path="/add-new-user" component = { NewUserForm }/>
 </React.Fragment>
 )
 }

以下是我们正在注册和渲染管理员组件的组件列表,如果用户是管理员:

  • AppUser:获取应用程序用户列表,还负责删除用户和按不同标准搜索用户。

  • Tickets:查看所有票证列表并更改票证状态

  • 新用户表单:将新用户添加到应用程序

我们正在使用 Node.js Firebase Admin SDK 服务器执行上述操作。

创建一个名为admin的文件夹,并在其中创建一个名为getAllUser.jsx的文件。在其中,我们将创建一个 React 组件,负责获取并显示用户列表到 UI;我们还将添加按不同标准搜索用户的功能,例如电子邮件 ID,电话号码等。

getAllUser.jsx文件中,我们的渲染方法如下:

<form className="form-inline">
//Search Input
     <div className="form-group" style={marginRight}>
         <input type="text" id="search" className="form-control"
         placeholder="Search user" value={this.state.search} required  
         />
     </div>
//Search by options
     <select className="form-control" style={marginRight}>
         <option value="email">Search by Email</option>
         <option value="phone">Search by Phone Number</option>
     </select>
     <button className="btn btn-primary btn-sm">Search</button>
 </form>

我们还在render方法中添加了表格来显示用户列表:

 <tbody>
 {
 this.state.users.length > 0 ?
 this.state.users.map((list,index) => {
 return (
 <tr key={list.uid}>
 <td>{list.email}</td>
 <td>{list.displayName}</td> 
 <td>{list.metadata.lastSignInTime}</td> 
 <td>{list.metadata.creationTime}</td> 
 <td>
     <button className="btn btn-sm btn-primary" type="button" style={marginRight} onClick={()=>            {this.deleteUser(list.uid)}}>Delete User</button>
       <button className="btn btn-sm btn-primary" type="button" onClick={()=>                        {this.viewProfile(list.uid)}}>View Profile</button>
 </td> 
 </tr>
 )
 }) :
 <tr>
     <td colSpan="5" className="text-center">No users found.</td>
 </tr>
 }
 </tbody>

这是显示用户列表的表格主体,并且现在我们需要在componentDidMount()方法中调用用户 API:

fetch('http://localhost:3000/users', {
 method: 'GET', // or 'PUT'
 headers: new Headers({
 'Content-Type': 'application/json'
 })
 }).then(res => res.json())
 .catch(error => console.error('Error:', error))
 .then(response => {
 console.log(response,"after token valid");
 this.setState({
   users:response
 })
 console.log(this.state.users,'All Users');
 })

同样,我们需要调用其他 API 来删除、查看用户资料和搜索:

deleteUser(uid){
 fetch('http://localhost:3000/deleteUser', {
     method: 'POST', // or 'PUT'
     body:JSON.stringify({uid:uid}),
     headers: new Headers({
         'Content-Type': 'application/json'
     })
 }).then(res => res.json())
     .catch(error => console.error('Error:', error))
 }
//Fetch User Profile
 viewProfile(uid){
 fetch('http://localhost:3000/getUserProfile', {
     method: 'POST', // or 'PUT'
     body:JSON.stringify({uid:uid}),
     headers: new Headers({
         'Content-Type': 'application/json'
     })
 }).then(res => res.json())
     .catch(error => console.error('Error:', error))
     .then(response => {
         console.log(response.data,"User Profile");
     })
 }

对于搜索,Firebase Admin SDK 具有内置方法:getUserByEmail()getUserByPhoneNumber()。我们可以以与我们在 Firebase Admin API 中创建delete()fetch()相同的方式实现这些方法:

//Search User by Email
searchByEmail(emailId){
 fetch('http://localhost:3000/searchByEmail', {
 method: 'POST', // or 'PUT'
 body:JSON.stringify({email:emailId}),
 headers: new Headers({
 'Content-Type': 'application/json'
 })
 }).then(res => res.json())
 .catch(error => console.error('Error:', error))
 .then(response => {
 console.log(response.data,"User Profile");
 this.setState({
    users:response
 })
 })
 }

查看以下node.js API 代码片段:

function listAllUsers(req,res) {
 var nextPageToken;
 // List batch of users, 1000 at a time.
 admin.auth().listUsers(1000,nextPageToken)
 .then(function(data) {
 data = data.users.map((el) => {
 return el.toJSON();
 })
 res.send(data);
 })
 .catch(function(error) {
 console.log("Error fetching the users from firebase:", error);
 });
}
function deleteUser(req, res){
  const userId = req.body.uid;
  admin.auth().deleteUser(userId)
  .then(function() {
    console.log("Successfully deleted user"+userId);
    res.send({status:"success", msg:"Successfully deleted user"})
  })
  .catch(function(error) {
    console.log("Error deleting user:", error);
  res.send({status:"error", msg:"Error deleting user:"})
  });
}
function searchByEmail(req, res){
  const searchType = req.body.email;
  admin.auth().getUserByEmail(userId)
  .then(function(userInfo) {
    console.log("Successfully fetched user information associated with this email"+userId);
    res.send({status:"success", data:userInfo})
  })
  .catch(function(error) {
    console.log("Error fetching user info:", error);
  res.send({status:"error", msg:"Error fetching user informaition"})
  });
}

现在,我们将创建一个 API 来根据用户的请求调用上述功能:

app.get('/users', function (req, res) {
 listAllUsers(req,res);
})
app.get('/deleteUser', function (req, res) {
 deleteUser(req,res);
})
app.post('/searchByEmail', function (req, res){
 searchByEmail(req, res)
})

现在,让我们快速查看一下我们在浏览器中的应用程序,看看它的外观,并尝试使用管理员用户登录:

使用管理员凭据登录时我们应用程序的屏幕截图;目的是展示我们作为管理员登录时的 UI 和控制台

看起来很棒!只需看一下上述屏幕截图;它显示了管理员的不同导航,如果您可以在控制台中看到,它显示了带有自定义声明对象的令牌,我们将其添加到此用户以获得管理员访问权限:

看起来很棒!我们可以看到应用程序的用户列表和搜索 UI。

现在,考虑到我们从列表中删除了用户,并且与此同时用户会话仍处于活动状态并正在使用应用程序。在这种情况下,我们需要管理用户的会话,并提示其重新进行身份验证,因为每次用户登录时,用户凭据都会被发送到 Firebase 身份验证后端,并交换为 Firebase ID 令牌(JWT)和刷新令牌。

以下是我们需要管理用户会话的常见情况:

  • 用户被删除

  • 用户已禁用

  • 电子邮件地址和密码已更改

Firebase Admin SDK 还提供了使用revokeRefreshToken()方法吊销特定用户会话的能力。它吊销给定用户的活动刷新令牌。如果我们重置密码,Firebase 身份验证后端会自动吊销用户令牌。

请参考以下 Firebase Cloud Function 代码片段,根据特定的uid来吊销用户:

const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
// Revoke all refresh tokens for a specified user for whatever reason.
function revokeUserTokens(uid){
return admin.auth().revokeRefreshTokens(uid)
.then(() => {
    // Get user's tokensValidAfterTime.
    return admin.auth().getUser(uid);
})
.then((userRecord) => {
    // Convert to seconds as the auth_time in the token claims is in seconds too.
    const utcRevocationTimeSecs = new Date(userRecord.tokensValidAfterTime).getTime() / 1000;
    // Save the refresh token revocation timestamp. This is needed to track ID token
    // revocation via Firebase rules.
    const metadataRef = admin.database().ref("metadata/" + userRecord.uid);
    return metadataRef.set({revokeTime: utcRevocationTimeSecs});
  });
}

正如我们所知,Firebase ID 令牌是无状态的 JWT,只能通过向 Firebase 身份验证后端服务器发送请求来验证令牌的状态是否被吊销。因此,在服务器上执行此检查非常昂贵,并增加了额外的工作量,需要额外的网络请求负载。我们可以通过设置 Firebase 规则来检查吊销而不是发送请求到 Firebase Admin SDK 来避免这种网络请求。

这是声明规则的正常方式,没有客户端访问来写入存储每个用户的吊销时间:

{
"rules": {
    "metadata": {
        "$user_id": {
            ".read": "$user_id === auth.uid",
            ".write": "false",
            }
        }
    }
}

然而,如果我们只想允许未被吊销和经过身份验证的用户访问受保护的数据,我们必须配置以下规则:

{
 "rules": {
     "users": {
         "$user_id": {
         ".read": "$user_id === auth.uid && auth.token.auth_time >     (root.child('metadata').child(auth.uid).child('revokeTime').val() || 0)",
         ".write": "$user_id === auth.uid && auth.token.auth_time > (root.child('metadata').child(auth.uid).child('revokeTime').val() || 0)"
             }
         }
     }
}

用户的浏览器刷新令牌被吊销时,tokensValidAfterTime UTC 时间戳将保存在数据库节点中。

当要验证用户的 ID 令牌时,必须传递附加检查布尔标志到verifyIdToken()方法。如果用户的令牌被吊销,用户应该从应用程序中注销或要求使用 Firebase 身份验证客户端 SDK 提供的重新验证 API 重新进行身份验证。

例如,我们在上面创建了一个名为setCustomClaims的方法;只需在catch方法中添加以下代码:

 .catch(error => {
     // Invalid token or token was revoked:
     if (error.code == 'auth/id-token-revoked') {
     //Shows the alert to user to reauthenticate
     // Firebase Authentication API gives the API to reauthenticateWithCredential /reauthenticateWithPopup /reauthenticateWithRedirect
 }
 });

此外,如果令牌被吊销,发送通知给客户端应用程序重新进行身份验证。

考虑此示例用于电子邮件/密码 Firebase 身份验证提供程序:

let password = prompt('Please provide your password for reauthentication');
let credential = firebase.auth.EmailAuthProvider.credential(
firebase.auth().currentUser.email, password);
firebase.auth().currentUser.reauthenticateWithCredential(credential)
.then(result => {
// User successfully reauthenticated.
})
.catch(error => {
// An error occurred.
});

现在,让我们点击“所有工单”链接,查看所有用户提交的工单列表:

作为管理员用户,我们可以更改在 Firebase 实时数据库中更新的票的状态。现在,如果您单击“创建新用户”,它将显示表单以添加用户信息。

让我们创建一个新组件,并将以下代码添加到渲染方法中:

<form className="form" onSubmit={this.handleSubmitEvent}>
 <div className="form-group">
 <input type="text" id="name" className="form-control"
 placeholder="Enter Employee Name" value={this.state.name} required onChange={this.handleChange} />
 </div>
 <div className="form-group">
 <input type="text" id="email" className="form-control"
 placeholder="Employee Email ID" value={this.state.email} required onChange={this.handleChange} />
 </div>
 <div className="form-group">
 <input type="password" id="password" className="form-control"
 placeholder="Application Password" value={this.state.password} required onChange={this.handleChange} />
 </div>
 <div className="form-group">
 <input type="text" id="phoneNumber" className="form-control"
 placeholder="Employee Phone Number" value={this.state.phoneNumber} required onChange={this.handleChange} />
 </div>
 <div className="form-group">
 <input
 type="file"
 ref={input => {
 this.fileInput = input;
 }}
 />
 </div>
 <button className="btn btn-primary btn-sm">Submit</button>
 </form>

handleSubmitEvent(e)中,我们需要调用createNewUser() Firebase 管理员 SDK 方法,并将表单数据传递给它:

e.preventDefault();
 //React form data object
 var data = {
 email:this.state.email,
 emailVerified: false,
 password:this.state.password,
 displayName:this.state.name,
 phoneNumber:this.state.phoneNumber,
 profilePhoto:this.fileInput.files[0],
 disabled: false
 }
 fetch('http://localhost:3000/createNewUser', {
   method: 'POST', // or 'PUT'
   body:JSON.stringify({data:data}),
   headers: new Headers({
   'Content-Type': 'application/json'
 })
 }).then(res => res.json())
 .catch(error => { 
 ToastDanger(error)
 })
 .then(response => {
 ToastSuccess(response.msg)
 });

重新启动服务器并在浏览器中打开应用程序。让我们尝试使用管理员凭据在我们的应用程序中创建新用户:

创建新用户组件;图像的目的是在我们填写表单并提交到 Firebase 以创建新用户时显示警报消息

看起来很棒;我们已成功在我们的应用程序中创建了新用户,并返回了 Firebase 为新用户生成的自动生成的uid

现在,让我们继续并用普通用户登录:

如果您看一下前面的屏幕截图,一旦我们使用任何 Firebase Auth 提供程序登录到应用程序中,在仪表板上,它会显示用户的所有票,但它应该只显示与此电子邮件 ID 相关联的票。为此,我们需要更改数据结构和 Firebase 节点引用。

这是应用程序最重要的部分,我们需要计划如何保存和检索数据,以使过程尽可能简单。

JSON 树中的数据结构

在 Firebase 实时数据库中,所有数据都存储为 JSON 对象,这是一个托管在云中的 JSON 树。当我们向数据库添加数据时,它将成为现有 JSON 结构中的一个节点,并带有一个关联的键,该键由 Firebase 自动生成。我们还可以提供自定义键,例如用户 ID 或任何语义名称,或者可以使用push()方法提供它们。

例如,在我们的 Helpdesk 应用程序中,我们将票存储在路径上,例如/helpdesk/tickets;现在我们将其替换为/helpdesk/tickets/$uid/$ticketKey。看一下以下代码:

var newTicketKey = firebase.database().ref('/helpdesk').child('tickets').push().key;
 // Write the new ticket data simultaneously in the tickets list and the user's ticket list.
 var updates = {};
 updates['/helpdesk/tickets/' + userId + '/' + newTicketKey] = data;
 updates['/helpdesk/tickets/all/'+ newTicketKey] = data;

这是从数据库创建和检索票的数据结构:

在上图中,突出显示的节点是$uid,它属于提交了票的用户。

这是我们完整代码的样子:

var newTicketKey = firebase.database().ref('/helpdesk').child('tickets').push().key;
 // Write the new ticket data simultaneously in the tickets list and the user's ticket list.
 var updates = {};
 updates['/helpdesk/tickets/' + userId + '/' + newTicketKey] = data;
 updates['/helpdesk/tickets/all/'+ newTicketKey] = data;

 return firebase.database().ref().update(updates).then(()=>{
 ToastSuccess("Saved Successfully!!");
 this.setState({
    issueType:"",
    department:"",
    comment:""
 });
 }).catch((error)=>{
    ToastDanger(error.message);
 });

打开浏览器并重新提交票证;现在查看票证仪表板:

看起来不错!现在用户只能看到他们提交的票证。在下一章中,我们将看到如何在数据库中应用安全规则和常见安全威胁。

总结

本章解释了如何配置和初始化 Firebase Admin SDK 来在 NodeJS 中创建我们的应用后端。它还解释了如何使用 Firebase Admin 的用户管理 API 来管理我们的应用用户,而无需转到 Firebase 控制台,例如以下内容:

  • 创建

  • 删除

  • 更新

  • 删除

Firebase Admin SDK 赋予我们创建和验证自定义 JWT 令牌的能力,这允许用户使用任何提供者进行身份验证,即使它不在 Firebase 身份验证提供者列表中。它还赋予您在用户信息发生任何更改时管理用户会话的能力,例如用户被删除、禁用、电子邮件地址或密码发生更改等。

我们还学习了如何控制对自定义声明的访问。这有助于我们实现基于角色的访问控制,以在 Firebase 应用中为用户提供不同级别的访问权限(角色)。

在下一章中,我们将学习数据库安全风险以及预防此类威胁的检查表。我们还将看到 Firebase 实时数据库的安全部分和 Firebase 实时数据库规则语言。

第六章:Firebase 安全和规则

在上一章中,我们看到了如何在应用程序中整合访问管理以保护它免受未经授权的访问,这实质上是应用程序级别的安全性。然而,如果我们的数据库没有得到保护呢?嗯,在这种情况下,数据可能会被未经授权的用户或甚至经过授权的用户(如数据库管理员)滥用,这会导致业务损失,有时甚至会引发法律诉讼。

数据安全始终是一个主要关注点,特别是当它托管在云服务器上时。我们必须保护我们的数据免受完整性、可用性和机密性的妥协。无论您使用的是关系型数据库管理系统,如 MySQL 或 MSSQL,还是 NoSQL,如 MongoDB 或 Firebase 实时数据库;所有这些数据库都必须通过限制对数据的访问来进行保护。在本章中,我们将简要介绍常见的数据库安全风险以及预防此类威胁的清单。我们还将看到 Firebase 实时数据库的安全部分和 Firebase 实时数据库规则语言。

以下是本章将讨论的主题列表:

  • 常见数据库安全风险和预防措施概述

  • Firebase 安全概述

  • Firebase 实时数据库规则概述

  • Firebase 实时数据库规则的结构和定义

  • 数据索引简介

  • 数据库备份和恢复

让我们从威胁的安全风险和预防开始。

安全风险和预防

数据库是任何组织的核心,因为它们包含客户数据和机密业务数据,因此它们经常成为黑客的目标。在过去几年中已经确定了一些常见的威胁,包括以下内容:

  • 未经授权或意外活动

  • 恶意软件感染

  • 数据库服务器的物理损坏

  • 由于无效数据而导致数据损坏

  • 性能下降

为了防止这些风险,需要遵循许多协议或安全标准:

  1. 访问控制:包括身份验证和授权。所有数据库系统都提供访问控制机制,例如使用用户名和密码进行身份验证。同时,在某些数据库中,设置它并不是强制性的,因此有时人们不启用它,使数据库不安全。同样,在所有数据库中,提供了基于角色的安全授权机制,以限制用户对某些数据或数据库的访问。然而,有时人们会给予所有用户根或管理员访问权限,使数据对所有用户开放。

  2. 审计:审计涉及监控所有用户执行的数据库活动,以增强安全性并保护数据。许多数据库平台包括内置的审计功能,允许您跟踪数据创建、删除或修改活动以及数据库使用情况,以便在早期检测到任何可疑活动。

  3. 备份:备份旨在从较早的时间恢复数据,并在数据删除或数据损坏的情况下恢复数据。根据要求,备份过程可以是自动化的或手动的。理想情况下,应该是自动化的,以便可以定期进行备份。虽然至少应该有一些备份,但数据存储空间可能很大,这取决于数据/备份的大小。为了减小备份的大小,应在持久化之前对备份文件进行压缩。

  4. 数据完整性控制:数据完整性是指数据库中存储的数据的一致性和准确性。数据验证是数据完整性的先决条件。许多关系数据库(RDBMS)通过约束(如主键和外键约束)强制数据完整性。在 NoSQL 的情况下,需要在数据库级别和应用程序级别进行数据验证,以确保数据完整性。

  5. 应用程序级安全性:还需要应用程序级安全性,以防止任何不当的数据保存在数据库中。通常,开发人员在表单级别和业务级别进行验证,以确保他们在数据库中保存有效的数据。

  6. 加密:加密个人数据(如社会安全号码)或金融数据(如信用卡信息)非常重要,以防止其被滥用。通常,使用 SSL 加密来加密客户端和服务器之间的连接,这实质上是网络级别的安全,以防止任何恶意攻击者阅读这些信息。

现在,让我们来检查 Firebase 中我们的数据有多安全。

您的 Firebase 有多安全?

Firebase 位于云存储上,因此人们很自然地会考虑它是否足够安全。然而,不用担心,因为 Firebase 提供了一个安全的架构和一套工具来管理应用程序的安全性。Firebase 托管在 SSL(安全套接字层)上,通常加密客户端和服务器之间的连接,从而防止在网络层发生任何数据窃取或篡改。Firebase 配备了基于表达式的规则语言,允许您通过配置来管理数据安全性。

Firebase 安全性主要是关于配置而不是约定,这样您的应用程序的安全相关逻辑就与业务逻辑分离开来。这样一来,您的应用程序就变得松散耦合。

在本章中,我们将学习有关 Firebase 实时数据库安全性和规则的内容。

实时数据库规则概述

Firebase 数据库规则允许您管理对数据库的读取和写入访问权限。它们还帮助您定义数据的验证方式,例如它是否具有有效的数据类型和格式。只有在您的规则允许的情况下,读取和写入请求才会被完成。默认情况下,您的规则被设置为只允许经过身份验证的用户完全读取和写入数据库。

Firebase 数据库规则具有类似 JavaScript 的语法,并分为四种类型:

.read 它确定用户何时允许读取数据。
.write 它确定用户何时允许写入数据。
.validate 它验证值是否格式正确,是否具有子属性以及其数据类型。
.indexOn 它确定子级是否存在索引以支持更快的查询和排序。

您可以从 Firebase 控制台的 Database || Rulestab 中访问和设置您的规则:

Firebase 实时数据库安全性有三个步骤:

  1. 用户认证

  2. 用户授权-控制对数据的访问

  3. 用户输入验证

认证

用户身份验证是保护应用程序免受未经授权访问的第一步。在第一步中识别用户自动意味着对他们可以访问和操作的数据的限制。在我们使用 Java、Microsoft.Net 或任何其他平台的后端技术的应用程序中,我们编写身份验证逻辑来限制对我们应用程序的访问。然而,由于 Firebase 广泛用于仅客户端应用程序,我们将没有后端工具的奢侈。幸运的是,Firebase 平台提供了一种身份验证机制—Firebase 身份验证—它内置了对常见身份验证机制的支持,如基于表单的身份验证、使用用户名和密码的 Google 和 Facebook 登录等。在第三章中,使用 Firebase 进行身份验证,以及第五章中,用户配置文件和访问管理,我们已经看到了如何实现 Firebase 身份验证。以下规则指定要访问数据库,用户必须经过身份验证。它还指定一旦用户经过身份验证,就可以访问数据库中的所有可用数据:

{
  "rules": {
    ".read": "auth != null",
    ".write": "auth != null"
  }
}

授权

一旦用户经过身份验证,他们就可以访问数据库。但是,您需要对谁可以访问什么进行一些控制。不应该允许每个人读取/写入数据库中的所有数据。这就是授权出现的地方。Firebase 数据库规则允许您控制每个用户的访问权限。Firebase 安全规则是基于节点的,并由一个 JSON 对象管理,您可以在实时数据库控制台上编辑它,也可以使用 Firebase CLI:

{
  "rules": {
        "users": { 
           ".read": "true",
           ".write": "false"
        }
  }
}

前面的规则确定所有用户都能够读取用户数据,但没有人能够对其进行写入。另外,请注意,必须将rules作为安全 JSON 对象中的第一个节点。

以下是指定用户私有数据的规则示例:

{
  "rules": {
    "users": {
      "$uid": {
        ".read": "$uid === auth.uid",
        ".write": "$uid === auth.uid"
      }
    }
  }
}

现在,您可能会有一个问题,比如我们有嵌套的数据结构,规则将如何应用到该数据。为了回答这个问题,这里要记住的一点是,.read.write规则级联,即授予对父节点的读取或写入访问权限总是授予对所有子节点的读取/写入访问权限。

父节点上的规则具有更高的优先级,因此它们将覆盖其子级别定义的规则。

Firebase 规则还提供了一些内置变量和函数,允许您访问 Firebase 身份验证信息,引用其他路径等。我们将在本章的后续部分详细检查这一点。

数据验证

如在介绍部分中所示,我们需要在将数据保存到数据库之前验证数据,以保持数据的完整性和正确性。Firebase 规则提供.validate表达式,如.read.write来实现验证逻辑,例如字段的长度应该只有这么多个字符,或者它必须是字符串数据类型。

考虑这个例子:

{
  "rules": {
        "users": { 
             "email": {
                    ".validate":  "newData.isString() && newData.val().length < 50"
              }
        }
  }
}

上述电子邮件字段的验证规则确定了电子邮件字段的值必须是字符串,其长度应小于 30 个字符。

重要的是要注意验证规则不会级联,因此为了允许写入,所有相关的验证规则必须评估为 true。

现在,我们已经对 Firebase 规则有了基本的了解,让我们深入了解规则配置。

规则定义和结构

Firebase 规则提供了可以在规则定义中使用的预定义变量:

名称 定义/用法
auth 它表示经过身份验证的用户的信息。对于未经身份验证的用户,它将为 null。它是一个包含 uid、token 和 provider 字段及相应值的对象。
$ variables 它表示通配符路径,用于引用动态生成的键和表示 ID。
root 它表示在应用给定数据库操作之前 Firebase 数据库中根路径的数据快照。
data 它表示给定数据库操作之前的数据快照。例如,在更新或写入的情况下,根代表原始数据快照,不包括更新或写入中的更改。
newData 它表示给定数据库操作之前的数据快照。然而,它包括现有数据以及新数据,其中包括给定数据操作操纵的数据。
now 它表示当前时间(以毫秒为单位)-自 1970 年 1 月 1 日(协调世界时午夜)以来经过的秒数

在下一节中,我们将看看如何在我们的规则中使用这些预定义变量。

正如我们在授权部分看到的,我们需要看看规则如何适用于嵌套数据。一个经验法则是,我们需要根据数据库中数据的结构来构建规则。

我们将扩展我们在本书的第五章中开发的 HelpDesk 应用程序,用户配置文件和访问管理

我们有以下的数据结构:

"helpdesk" : {
    "tickets" : {
      "FlQefqueU2USLElL4vc5MoNUnu03" : {
        "-L4L1BLYiU-UQdE6lKA_" : {
          "comments" : "Need extra 4GB RAM in my system",
          "date" : "Fri Feb 02 2018 15:51:10 GMT+0530 (India Standard 
           Time)",
          "department" : "IT",
          "email" : "harmeet_15_1991@yahoo.com",
          "issueType" : "Hardware Request",
          "status" : "progress"
        }
      },
      "KEEyErkmP3YE1BagxSci0hF0g8H2" : {
        "-L4K01hUSDzPXTIXY9oU" : {
          "comments" : "Not able to access my email",
          "date" : "Fri Feb 02 2018 11:06:32 GMT+0530 (India Standard 
           Time)",
          "department" : "IT",
          "email" : "harmeetsingh090@gmail.com",
          "issueType" : "Email Related Issues",
          "status" : "progress"
        }
      },
      "all" : {
        "-L4K01hUSDzPXTIXY9oU" : {
          "comments" : "Not able to access my email",
          "date" : "Fri Feb 02 2018 11:06:32 GMT+0530 (India Standard 
           Time)",
          "department" : "IT",
          "email" : "harmeetsingh090@gmail.com",
          "issueType" : "Email Related Issues",
          "status" : "progress"
        },
        "-L4L1BLYiU-UQdE6lKA_" : {
          "comments" : "Need extra 4GB RAM in my system",
          "date" : "Fri Feb 02 2018 15:51:10 GMT+0530 (India Standard 
           Time)",
          "department" : "IT",
          "email" : "harmeet_15_1991@yahoo.com",
          "issueType" : "Hardware Request",
          "status" : "progress"
        }
      }
    }
  }

在这里,我们可以看到要使数据在用户级别上得到保护,只显示与已登录用户相关的工单,我们将它们存储在 userId 下,比如FlQefqueU2USLElL4vc5MoNUnu03KEEyErkmP3YE1BagxSci0hF0g8H2,并且要向管理员显示所有工单,我们将它们存储在all下。然而,这并不是理想的解决方案,因为它有两个问题:数据是冗余的,并且要更新任何数据,我们将不得不在两个地方进行更新。幸运的是,我们可以直接在数据库中使用规则处理这种安全性。

我们将改变我们的数据,从数据中删除all节点。我们还将在$userId下添加一个变量来标识用户是否是管理员。所以它将看起来像这样:

"helpdesk" : {
    "tickets" : {
      "FlQefqueU2USLElL4vc5MoNUnu03" : {
        "-L4L1BLYiU-UQdE6lKA_" : {
          "comments" : "Need extra 4GB RAM in my system",
          "date" : "Fri Feb 02 2018 15:51:10 GMT+0530 (India Standard 
           Time)",
          "department" : "IT",
          "email" : "harmeet_15_1991@yahoo.com",
          "issueType" : "Hardware Request",
          "status" : "progress"
        },
         "isAdmin": true
      },
      "KEEyErkmP3YE1BagxSci0hF0g8H2" : {
        "-L4K01hUSDzPXTIXY9oU" : {
          "comments" : "Not able to access my email",
          "date" : "Fri Feb 02 2018 11:06:32 GMT+0530 (India Standard 
           Time)",
          "department" : "IT",
          "email" : "harmeetsingh090@gmail.com",
          "issueType" : "Email Related Issues",
          "status" : "progress"
        },
        "isAdmin": false
      }
    }
  }
}

我们的规则将如下所示:

{
  "rules": {
     "helpdesk": {
      "tickets": {
        ".read": "data.child(auth.uid).child('isAdmin').val()==true",
        ".write": "data.child(auth.uid).child('isAdmin').val()==true",
        "$uid": {
          ".read": "auth.uid == $uid",
          ".write": "auth.uid == $uid"
        }
      }
     }

  }
}

这些规则实质上施加了限制,如果用户是管理员,也就是isAdmin为 true,那么他们可以读取和写入所有数据。然而,其他用户只能读/写自己的数据。

在这里,我们还使用了预定义的变量 data,它代表了write操作之前的DataSnapshot。同样,我们可以使用root变量来引用根路径,newData来引用写操作后将存在的数据快照。

现在,如果你已经注意到,我们使用了.child,这实质上是用来引用任何子路径/属性的。在我们的规则中,我们正在检查在$uid下,isAdmin的值是否为 true,因为我们希望给管理员访问所有数据的权限。同样,我们可以在我们的规则中使用任何数据作为条件。

另一个重要的事情要注意的是,一旦我们在父级tickets上定义了.read.write规则,我们就不再在$uid下检查isAdmin条件,因为规则会级联,所以一旦你授予管理员读/写权限,你就不需要在$uid级别重复这些条件。同时,重要的是要注意,在父位置定义规则是强制性的。如果我们不在父位置定义规则,即使子路径是可访问的,你的数据操作也会完全失败。

例如,在以下规则中,我们可以看到,尽管我们在票证级别拥有访问权限,但由于我们在$uid级别未定义规则,我们将无法访问数据:

{
  "rules": {
     "helpdesk": {
      "tickets": {
        "$ticketId": {
          ".read": true,
          ".write": true
        }
      }
     }

  }
}

基于查询的规则

如前面的示例所示,规则不能用作过滤器。但是,有时我们需要根据某些条件或查询参数仅访问数据的子集。例如,假设我们需要仅从查询结果集中返回 1000 条记录中的前 100 条记录。我们可以通过使用query.表达式根据查询参数为您的结果集提供读取和写入访问权限:

tickets: {
  ".read": "query.limitToFirst <= 100"
}

前面的代码将默认访问前 100 条按键排序的记录。如果要指定orderByChild,也可以这样做,如下所示:

tickets: {
  ".read": "query.orderByChild == 'department' && query.limitToFirst <= 100"
}

确保在读取数据时,指定orderByChild,否则读取将失败。

数据索引

Firebase 允许您使用子键编写查询。为了提高查询性能,您可以使用.indexOn规则在这些键上定义索引。我们假设您已经知道索引的工作原理,因为几乎所有数据库系统都支持索引。

让我们举个例子来更好地理解这一点。假设在我们的 HelpDesk 系统中,我们经常按部门键订购票,并且我们正在使用orderbyChild()

{  "rules":  {
 "helpdesk": { "tickets":  {  ".indexOn":  ["department"]  }
      }  }  }

类似地,如果我们使用orderByValue(),我们可以有以下规则:

".indexOn":  ".value"

备份

在本章的第一部分中,我们看到了管理数据备份的重要性。虽然您可以手动进行数据备份,但有可能会错过一些内容并丢失备份。幸运的是,Firebase 提供了自动备份服务,可以设置为每天自动备份数据和规则。请注意,此服务仅适用于 Blaze 计划用户,并且将按照标准费率收费。您可以查看firebase.google.com/pricing/上提供的各种订阅计划。

设置

您可以从 Firebase 部分的实时数据库的备份选项卡设置数据库备份。设置向导将指导您完成配置自动备份的步骤。您的数据库备份活动将在每天的特定时间进行,而不会影响负载,并确保所有备份客户的最高可用性。

此外,您还可以在需要获取数据和规则的时间点快照时随时进行手动备份。

您的备份将存储在 Google Cloud Storage 中,这是 Google Cloud Platform 提供的对象存储服务。基本上,Google Cloud Storage 提供了类似计算机文件系统上的目录的存储桶,您的备份将存储在其中。因此,一旦设置完成,将创建一个具有权限的存储桶,您的 Firebase 可以在其中写入数据。我们将在第八章Firebase 云存储中详细了解 Google Cloud Storage 和 Firebase 云存储。

备份服务会自动使用 Gzip 压缩备份文件,从而减小整体备份大小,最终降低成本,同时最小化数据传输时间。压缩文件大小根据数据库中的数据而变化,但通常情况下,它会将整体文件大小减小到原始解压文件大小的 1/3。您可以根据需求启用或禁用 Gzip 压缩。

为了进一步节省成本,您还可以在存储桶上启用 30 天的生命周期策略来删除旧的备份;例如,30 天前的备份会自动被删除。

您可以通过执行以下命令行命令来解压缩您的 Gzipped JSON 文件,该命令使用默认情况下在 OS-X 和大多数 Linux 发行版上都可用的gunzip二进制文件:

gunzip <DATABASE_NAME>.json.gz

文件名将根据以下命名约定生成。它将具有时间戳(ISO 8601 标准):

Database data: YYYY-MM-DDTHH:MM:SSZ_<DATABASE_NAME>_data.json
Database rules: YYYY-MM-DDTHH:MM:SSZ_<DATABASE_NAME>_rules.json

如果启用了 Gzip 压缩,文件名将附加一个.gz后缀。

考虑这个例子:

 Database data: YYYY-MM-DDTHH:MM:SSZ_<DATABASE_NAME>_data.json.gz
Database rules: YYYY-MM-DDTHH:MM:SSZ_<DATABASE_NAME>_rules.json.gz

一旦您进行了备份,您可能会希望在某个时间点进行恢复。让我们看看如何从备份中恢复数据。

从备份中恢复

要从备份中恢复数据,首先从 Google Cloud Storage 下载备份文件,并根据前面的命令进行解压缩。一旦您有了 JSON 文件,您可以通过以下两种方式之一导入数据:

  • 在 Firebase 控制台的数据库部分,您将找到一个导入 JSON 按钮,它将允许您上传文件。

  • 您可以使用 CURL 命令:curl 'https://<DATABASE_NAME>.firebaseio.com/.json?auth=<SECRET>&print=silent' -x PUT -d @<DATABASE_NAME>.json。请注意,您需要分别用自己的值替换DATABASE_NAMESECRET。您可以从数据库设置页面获取密钥。

总结

本章解释了数据面临的常见安全威胁,特别是当数据存储在云上时,以及我们如何保护我们的数据。它还解释了 Firebase 是安全的,只要我们通过在数据库中定义适当的规则并控制对数据的访问来正确管理安全性,我们就不必过多担心数据的安全性。

Firebase 托管在安全服务器层上,该层管理传输层的安全性。它还为您提供了一个强大而简单的规则引擎,可以配置以保护您的数据,并同时获得关注分离的好处——将安全逻辑与应用逻辑分离。

我们还详细学习了安全规则,以及如何使用类似于简单的 JavaScript 语法来定义它们。

在下一章中,我们将探讨 Firebase 云消息传递和云函数。

第七章:在 React 中使用 Firebase Cloud Messaging 和 Cloud Functions

在之前的章节中,我们探讨了一些 Firebase 产品,比如实时数据库、身份验证、Cloud Firestore 和 Cloud Storage。然而,我们还没有看到一些高级功能,比如实时消息传递和无服务器应用开发。现在我们准备好探索它们了,所以让我们讨论 Firebase 平台的另外两个产品:Firebase Cloud Messaging 和 Cloud Functions。Firebase Cloud Messaging 是一个消息平台,可以在不同平台(Android、iOS 和 Web)上免费发送消息。Cloud Functions 允许你拥有无服务器应用,这意味着你可以在没有服务器的情况下运行自定义应用逻辑。

以下是本章我们将重点关注的主题列表:

  • Firebase Cloud MessagingFCM)的主要特点

  • JavaScript Web 应用的 Firebase 设置

  • 客户端应用设置以接收通知

  • 服务器设置以发送通知

  • Cloud Functions 的主要特点

  • 为 Cloud Functions 设置 Firebase SDK

  • Cloud Function 的生命周期

  • 触发函数

  • 部署和执行函数

  • 函数的终止

让我们先从 FCM 开始,然后我们将介绍 Cloud Functions。

Firebase Cloud Messaging(FCM)

FCM 提供了一个平台,帮助你使用服务工作者实时向应用用户发送消息和通知。你可以免费跨不同平台发送数百亿条消息,包括 Android、iOS 和 Web(JavaScript)。你还可以安排消息的发送时间,立即或在将来。

FCM 实现中有两个主要组件:一个受信任的环境,包括一个应用服务器或 Cloud 函数来发送消息,以及一个 iOS、Android 或 Web(JavaScript)客户端应用来接收消息。

如果你了解Google Cloud MessagingGCM),你可能会问 FCM 与 GCM 有何不同。对这个问题的答案是,FCM 是 GCM 的最新和改进版本。它继承了 GCM 的所有基础设施,并对简化客户端开发进行了改进。只是要注意,GCM 并没有被弃用,Google 仍在支持它。然而,新的客户端功能只会出现在 FCM 上,因此根据 Google 的建议,你应该从 GCM 升级到 FCM。

尽管它支持不同的平台,包括 Android、iOS 和 Web,但在本章中我们主要讨论 Web(JavaScript)。现在让我们来看看 FCM 的主要特点。

FCM 的关键功能

GCM 的关键功能包括下行消息、上行消息和多功能消息。让我们在下一部分简要地看一下这些功能是什么。

发送下行消息

下行消息是代表客户端应用程序从服务器发送给用户的。FCM 消息可以分为两类:通知消息和数据消息。通知消息直接显示给用户。通知消息的一些示例是警报消息、聊天消息,或者通知客户端应用程序启动一些处理的消息备份。数据消息需要在客户端应用程序代码中处理。一些示例是聊天消息或任何特定于您的应用程序的消息。我们将在 FCM 消息的下一部分更多地讨论这些消息类型。

发送上行消息

上行消息通过 FCM 通道从设备发送回服务器。您可以通过可靠的 FCM 通道将确认、聊天消息和其他消息从设备发送回服务器。

多功能消息定位

FCM 非常灵活,允许您向单个设备、一组设备或所有订阅特定主题的订阅者发送消息。

FCM 消息

使用 FCM,您可以向客户端发送两种类型的消息:通知消息和数据消息。使用 Firebase SDK 时,这两种消息的最大有效负载大小为 4 KB。但是,当您从 Firebase 控制台发送消息时,它会强制执行 1024 个字符的限制。

通知消息由 FCM SDK 自动处理,因为它们只是显示消息。当您希望 FCM 代表您的客户端应用程序显示通知时,可以使用通知消息。通知消息包含一组预定义的键,还可以包含可选的数据有效负载。

通知消息对象如下所示:

{
  "message":{
    "token":"bk3RNwTe3H0:CI2k_HHwgIpoDKCIZvvDMExUdFQ3P1...",
    "notification":{
      "title":"This is an FCM notification message!",
      "body":"FCM message"
    }
  }
}

数据消息由客户端应用程序处理,并包含用户定义的键。它们如下所示:

{
  "message":{
    "token":"bk3RNwTe3H0:CI2k_HHwgIpoDKCIZvvDMExUdFQ3P1...",
    "data":{
      "Name" : "MT",
      "Education" : "Ph.D."
    }
  }
}

我们将在接下来的部分看到什么是令牌。

为 Javascript Web 应用程序设置 Firebase

FCM 允许您在不同浏览器中的 Web 应用程序中接收通知消息,并支持服务工作者。服务工作者是在后台运行的浏览器脚本,提供离线数据功能、后台数据同步、推送通知等功能。服务工作者支持在以下浏览器中使用:

  • Chrome:50+

  • Firefox:44+

  • Opera Mobile:37+

使用服务工作者,人们可以进行一些恶意活动,比如过滤响应或劫持连接。为了避免这种情况,服务工作者只能在通过 HTTPS 提供的页面上使用。因此,如果您想使用 FCM,您将需要在服务器上拥有有效的 SSL 证书。请注意,在本地环境中,您不需要 SSL;它可以在本地主机上正常工作。

安装 Firebase 和 Firebase CLI

如果您要开始一个新的 React 项目,最简单的方法是使用 React Starter Kit 开始。您可以使用以下命令创建一个 React 项目,然后安装firebasefirebase-tools。如果这是一个现有的 React 和 Firebase 项目,您可以跳过安装步骤:

npm install -g create-react-app

您可以使用以下命令安装 Firebase:

npm install firebase --save

您还需要安装 Firebase CLI 以在服务器上运行您的项目。可以使用以下命令进行安装:

npm install -g firebase-tools

现在,我们将使用 FCM 实现来扩展 Helpdesk 应用程序。

配置浏览器以接收消息

首先,您需要从developers.google.com/web/fundamentals/web-app-manifest/file中添加一个 Web 应用程序清单到我们的项目中,并将以下内容添加到其中:

{
  "gcm_sender_id": "103953800507"
}

它告诉浏览器允许 FCM 向此应用程序发送消息。103953800507的值是硬编码的,在任何您的应用程序中必须相同。Web 应用程序清单是一个简单的 JSON 文件,将包含与您的项目相关的配置元数据,例如您的应用程序的起始 URL 和应用程序图标详细信息。

我们在代码的根文件夹中创建了一个manifest.json文件,并将上述内容添加到其中。

客户端应用程序设置以接收通知

为了让您的应用程序在浏览器中接收通知,它将需要从用户那里获得权限。为此,我们将添加一段代码,显示一个同意对话框,让用户授予您的应用程序在浏览器中接收通知的权限。

我们将在主目录下的index.jsx文件中添加componentWillMount()方法,因为我们希望在用户成功登录到应用程序后显示对话框:

 componentWillMount() {
      firebase.messaging().requestPermission()
       .then(function() {
        console.log('Permission granted.');
        // you can write logic to get the registration token
          // _this.getToken();
       })
       .catch(function(err) {
        console.log('Unable to get permission to notify.', err);
      });
  }

请注意,您需要使用以下行导入firebase对象:

import firebase from '../firebase/firebase-config';

一旦您添加了上述代码,请重新启动服务器并登录到应用程序。它应该向您的应用程序用户显示以下对话框:

用户授予权限后,您的浏览器才能接收通知。

现在,让我们编写一个函数来获取注册令牌:

 getToken() {
        console.log("get token");
        firebase.messaging().getToken()
            .then(function (currentToken) {
                if (currentToken) {
                    console.log("current token", currentToken)
                   // sendTokenToServer(currentToken);
                   //updateUI(currentToken);
                } else {
                    // Show permission request.
                    console.log('No Instance ID token available. 
                    Request permission to generate one.');
                    // Show permission UI.
                  // updateUIForPushPermissionRequired();
                   // setTokenSentToServer(false);
                }
            })
            .catch(function (err) {
                console.log('An error occurred while retrieving token. 
                ', err);
              // showToken('Error retrieving Instance ID token. ', 
                 err);
               // setTokenSentToServer(false);
            });
   }

上述函数将检索当前访问令牌,需要将其发送到服务器以订阅通知。您可以在sendTokenToServer()方法中实现将此令牌发送到服务器的逻辑。

注册令牌可能在您的 Web 应用程序删除注册令牌或用户清除浏览器数据时更改。在后一种情况下,将需要调用getToken()来检索新令牌。由于注册令牌可能会更改,您还应该监视刷新令牌以获取新令牌。FCM 在生成令牌时触发回调,以便您可以获取新令牌。onTokenRefresh()回调在生成新令牌时触发,因此在其上下文中调用getToken()方法可以确保您拥有当前的注册令牌。您可以编写一个类似这样的函数:

 refreshToken() {
        firebase.messaging().onTokenRefresh(function () {
            firebase.messaging().getToken()
                .then(function (refreshedToken) {
                    console.log('Token refreshed.');
                    // Indicate that the new Instance ID token has not 
                       yet been sent to the
                    // app server.
                    //setTokenSentToServer(false);
                     Send Instance ID token to app server. Implement it 
                     as per your requirement
                    //sendTokenToServer(refreshedToken);
                    // ...
                })
                .catch(function (err) {
                    console.log('Unable to retrieve refreshed token ', 
                    err);
                    //showToken('Unable to retrieve refreshed token ', 
                      err);
                });
        });
    }

一旦您获得令牌,您可以通过实现类似sendTokenToServer(refreshedToken)的方法将其发送到您的应用服务器以存储它,如果您正在使用 React 和 Firebase 实时数据库,您可以直接将其存储在数据库中。

所有这些函数将被添加到index.jsx文件中。我们将从componentWillMount()方法中调用getToken()函数,而refreshToken()将从构造函数中调用。

现在,在完成所有这些设置之后,我们将在客户端应用程序中添加接收消息的实际功能。

根据页面状态,无论是在后台运行还是在前台(具有焦点)运行,还是关闭或隐藏在标签后面,消息的行为都会有所不同。

为了接收消息,页面必须处理onMessage()回调,并且要处理onMessage(),您的应用程序中必须定义一个 Firebase 消息服务工作者。

我们将在项目的根目录下创建一个名为firebase-messaging-sw.js的文件,并在其中编写以下代码:

importScripts('https://www.gstatic.com/firebasejs/4.1.1/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/4.1.1/firebase-messaging.js');

var config = {
    messagingSenderId: "41428255555"
};
firebase.initializeApp(config);
const messaging = firebase.messaging();

messaging.setBackgroundMessageHandler(function(payload) {
    console.log('[firebase-messaging-sw.js] Received background message ', payload);
    // Customize notification here
    const notificationTitle = 'Background Message Title';
    const notificationOptions = {
        body: 'Background Message body.',
        icon: '/firebase-logo.png'
    };

return self.registration.showNotification(notificationTitle,
    notificationOptions);
});

或者,您可以使用useServiceWorker指定现有的服务工作者。

请注意,您需要更新消息senderId值,您可以从 Firebase 控制台获取您的项目的值。

如果您希望在您的网页处于后台时显示通知消息,您需要设置setBackgroundMessageHandler来处理这些消息。您还可以自定义消息,例如设置自定义标题和图标。您可以在上面的代码片段中检查它。在应用程序处于后台时接收到的消息会触发浏览器中的显示通知。

现在您可以在您的网页上处理OnMessage()事件。我们将在我们的index.js文件中添加一个构造函数,以便在页面加载时注册回调函数:

constructor(props) {
        super(props);
        //this.refreshToken();
        firebase.messaging().onMessage(function (payload) {
            console.log("Message received. ", payload);
            // Update the UI to include the received message.
            console.log("msg", payload);
            // appendMessage(payload);
        });
    }

现在我们的客户端已准备好接收通知消息。让我们配置后端以发送通知。

服务器设置以发送通知

第一步是为您的项目启用 FCM API。您可以转到console.developers.google.com/apis/api/fcm.googleapis.com/overview?project=<project-id>并启用它。

要从受信任的环境发送通知,我们将需要 Oauth2 访问令牌和我们在客户端应用程序中获取的客户端注册令牌。

要获取 Oauth2 访问令牌,我们将需要来自您服务帐户的私钥。一旦生成私钥,请将包含私钥的 JSON 文件保存在某个安全的地方。我们将使用 Google API 客户端库在developers.google.com/api-client-library/检索访问令牌,因此请使用以下命令安装googleapisnpm模块:

npm install googleapis --save

以下函数需要添加到我们的main.js文件中以获取访问令牌:

app.get('/getAccessToken', function (req, res) {

  var { google } = require('googleapis');

  var key = require('./firebase/serviceAccountKey.json');
  var jwtClient = new google.auth.JWT(
    key.client_email,
    null,
    key.private_key,
    ['https://www.googleapis.com/auth/firebase.messaging'], // an array 
     of auth scopes
    null
  );
  jwtClient.authorize(function (err, tokens) {
    if (err) {
      console.log(err);
      res.send(JSON.stringify({
        "token": err
      }));
    }
    console.log("tokens", tokens);
    res.send(JSON.stringify({
      "token": tokens.access_token
    }));
  });

});

当您访问http://localhost:3000/getAccessToken URL 时,它将在浏览器中显示您的访问令牌。

在浏览器中,您将看到类似以下的内容:

显然,在实际应用中,出于安全原因,您不会在浏览器中显示此令牌或在浏览器控制台中打印它,并且您将在内部使用它。

此访问令牌将在请求的Authorization标头中传递,如下所示:

headers: {
  'Authorization': 'Bearer ' + accessToken
}

现在您有了访问令牌。此外,如果您还记得,当设置客户端应用程序时,我们谈到了sendTokenToServer(currentToken)方法,该方法将令牌发送到服务器。您现在必须已将其存储在数据库或缓存中,现在可以使用。

现在我们准备发送我们的第一条通知消息。要发送消息,我们将使用最新的 HTTP v1“发送”请求。

我们的请求将如下所示:

POST https://fcm.googleapis.com/v1/projects/demoproject-7cc0d/messages:send

Content-Type: application/json
Authorization: Bearer ya29.c.ElphBTDpfvg35hKz4nDu9XYn3p1jlTRgw9FD0ubT5h4prOtwC9G9IKslBv8TDaAPohQHY0-O3JmADYfsrk7WWdhZAeOoqWSH4wTsVyijjhE-PWRSL2YI1erT"

{
  "message":{
    "token" : "dWOV8ukFukY:APA91bFIAEkV-9vwAIQNRGt57XX2hl5trWf8YpocOHfkYAkgSZr5wfBsNozYZOEm_N0mbdZmKbvmtVCCWovrng4UYwj-zmpe36ySPcP31HxGGGb3noEkeBFyZRDUpv0TD7HAKxTfDuEx...",
    "notification" : {
      "body" : "This is an FCM notification message!",
      "title" : "FCM Message",
      }
   }
}

您将替换所有令牌并使用您的项目 ID 更新 URL,然后您应该能够发送您的第一条消息。

我使用了一个 rest 客户端来发送消息,并且由于我的浏览器在后台运行,它会在系统托盘中显示通知消息。您可以在下一个截图中看到:

Postman chrome 工具扩展;目的只是显示发送 FCM 通知消息的请求和响应

请求正文如下:

Postman chrome 工具扩展;图片的目的只是显示我们之前发送的请求的正文

以下是关于消息请求的重要注意事项:

URL:https://fcm.googleapis.com/v1/projects/<projectid>/messages:send

  • 标头:包含两个键值对:

  • Content-Typeapplication/json

  • AuthorizationBearer <访问令牌>

  • 请求正文:包含具有以下键值的消息对象:

  • token:<注册令牌,用于向客户端应用发送消息>

  • notification:它包含您的通知消息的配置

是的,现在我们已经在我们的应用程序中集成了 FCM,以便将通知消息发送到后台运行应用程序的单个设备。但是,您可能希望将通知发送到一组设备,或者可能希望将消息发送到客户端已订阅的主题。基本概念将保持不变,但配置将发生变化。您可以在 firebase 文档中参考这些主题firebase.google.com/docs/cloud-messaging

我们将在下一节中看到云函数。

云函数

一般来说,任何软件应用都有一些后端逻辑,这些逻辑部署在服务器上,通过互联网访问。在大型企业级应用程序(如银行或金融)的情况下,可能值得管理一个服务器或一组服务器。然而,在小型应用程序或您希望根据某些用户事件执行特定逻辑的应用程序中,例如数据库中的数据更改或来自移动应用程序或 Web 应用程序的 API 请求,管理服务器可能会增加工作量和成本。但是,当您使用 Firebase 平台时,您不需要担心这一点,因为它提供了云函数,让您根据特定 Firebase 产品发出的事件运行代码,而无需管理服务器。

云函数的关键特性

云函数具有许多功能,包括与其他 Firebase 产品和第三方 API 的轻松集成,以及强大的安全性和隐私。

云函数的关键特性在以下子主题中讨论。

与其他 Firebase 产品和第三方 API 的无缝集成

云函数可以与其他 Firebase 产品和第三方 API 无缝集成。

您的自定义函数可以在特定事件上执行,这些事件可以由列出的 Firebase 产品发出:

  • 云 Firestore 触发器

  • 实时数据库触发器

  • Firebase 身份验证触发器

  • Firebase 分析触发器

  • 云存储触发器

  • 云 Pub/Sub 触发器

  • HTTP 触发器

您可以使用 Firebase Admin SDK 来实现不同 Firebase 产品的无缝集成。在一些最常见的应用需求中非常有用。假设您想要在实时数据库中的某些内容发生变化时生成数据库索引或审计日志;您可以编写一个基于实时数据库触发器执行的云函数。反过来,您可以根据特定用户行为执行一些数据库操作。同样地,您可以将云函数与 Firebase 云消息传递(FCM)集成,以便在数据库中发生特定事件时通知用户。

云函数的集成不仅限于 Firebase 产品;您还可以通过编写 Webhook 将云函数与一些第三方 API 服务集成。假设您是开发团队的一部分,并且希望在有人提交代码到 Git 时更新 Slack 频道。您可以使用 Git Webhook API,它将触发您的云函数执行逻辑以将消息发送到 Slack 频道。同样,您可以使用第三方 Auth 提供程序 API,例如 LinkedIn,以允许用户登录。

无需维护服务器

云函数在无需购买或维护任何服务器的情况下运行您的代码。您可以编写 JavaScript 或 TypeScript 函数,并使用云上的单个命令部署它。您不需要担心服务器的维护或扩展。Firebase 平台将自动为您管理。服务器实例的扩展发生得非常精确,具体取决于工作负载。

私密和安全

应用程序的业务逻辑应该对客户端隐藏,并且必须足够安全,以防止对代码的任何操纵或逆向工程。云函数是完全安全的,因此始终保持私密,并始终按照您的意愿执行。

函数的生命周期

云函数的生命周期大致可以分为五个阶段,分别是:

  1. 编写新函数的代码,并定义函数应在何时执行的条件。函数定义或代码还包含事件提供程序的详细信息,例如实时数据库或 FCM。

  2. 使用 Firebase CLI 部署函数,并将其连接到代码中定义的事件提供程序。

  3. 当事件提供程序生成与函数中定义的条件匹配的事件时,它将被执行。

  4. Google 会根据工作负载自动扩展实例的数量。

  5. 每当您更新函数的代码或删除函数时,Google 都会自动更新或清理实例。

让我们现在使用实时数据库提供程序创建一个简单的云函数并部署它。

设置 Firebase SDK 以用于云函数

在继续初始化云函数之前,必须安装 Firebase CLI。如果尚未安装,可以按照下一节中的说明安装 Firebase CLI。

Firebase CLI

我们已经在[第五章](5697f854-7bc1-4ffb-86a2-8304d0fc73e7.xhtml)用户配置文件和访问管理中看到了如何安装它,但这里是命令,仅供参考:

npm install -g firebase-tools

一旦我们安装了 Firebase CLI,我们将使用以下命令登录到 firebase 控制台:

firebase login

这个命令将打开一个浏览器 URL,并要求你登录。成功登录后,你可以进行下一步——初始化 Firebase 项目。

初始化 Firebase 云项目

让我们创建一个名为 cloud-functions 的空项目目录。我们将从新创建的 cloud-functions 目录中运行以下命令来初始化云函数:

firebase init functions

这个命令将引导你通过一个包含不同步骤的向导,并将为你的项目创建必要的文件。它会询问你喜欢的语言:Javascript 还是 TypeScript。我们将选择 Typescript 作为示例。它还会问你是否想要关联任何现有的 firebase 项目或者想要创建一个新项目来关联。我们将选择一个现有的项目。它还会问你是否要安装所需的 node 依赖。我们会选择是,这样它就会安装所有必要的 node 包。如果你想自己管理依赖,可以选择否。以下截图显示了向导的外观:

最终的结构将如下所示:

让我们了解一些特定于云函数的文件:

  1. firebase.json:它包含了项目的属性。它包含一个名为"source"的属性,指向functions文件夹,用于存放云函数代码。如果你想指向其他文件夹,可以在这里更改。它还包括一个名为"predeploy"的属性,基本上包含了构建和运行代码的命令。

  2. .firebaserc:它包含了与该目录相关联的项目。它可以帮助你快速切换项目。

  3. functions/src/index.ts:这是主要的源文件,所有的云函数代码都将放在这里。默认情况下,这个文件中已经有一个名为helloworld的函数。但是,默认情况下它是被注释掉的。

  4. functions/package.json:包含了该项目的 NPM 依赖。

如果你是 Windows 用户,你可能需要将firebase.json文件中的"predeploy"属性的值从"npm --prefix $RESOURCE_DIR run build"改为"npm --prefix %RESOURCE_DIR% run build",因为有时在尝试部署函数时会出现错误。

设置完成后,我们就可以部署我们的第一个云函数了。对于这个示例,我们将编写一个简单的函数调用 greetUser,它接受 request 参数中的用户名称,并在响应中显示问候消息:

import * as functions from 'firebase-functions';

export const greetUser = functions.https.onRequest((request, response) => {
        const name = request.query.name;
        response.send("Welcome to Firebase Cloud Function, "+name+"!");
});

首先,我们需要导入 f*irebase-functions *来使我们的函数工作。还要注意,云函数是通过调用 functions.https 来实现的,这基本上意味着我们正在使用 HTTP 触发器。

greetUser() 云函数是一个 HTTP 端点。如果您了解 ExpressJS 编程,您一定注意到语法类似于 ExpressJS 端点,当用户点击端点时,它执行一个带有请求和响应对象的函数。实际上,HTTP 函数的事件处理程序监听 onRequest() 事件,支持由 Express web 框架管理的路由器和应用程序。响应对象用于向用户发送响应,在我们的情况下是文本消息,用户将在浏览器中看到。

部署和执行云函数

我们需要使用以下命令来部署我们的 helloworld 云函数:

firebase deploy --only functions

这个命令将部署我们的函数,您应该在命令提示符中看到以下响应:

如果部署成功完成,您将看到函数 URL,例如 https://us-central1-seat-booking.cloudfunctions.net/greetUser,现在可以用来触发云函数的执行。

函数 URL 包括以下内容:

  • us-central1:这是您的函数部署的区域

  • seat-booking:这是 Firebase 项目 ID

  • cloudfunction.net:这是默认域

  • greetUser:这是部署的函数名称

我们需要将名称属性作为请求参数附加以查看在问候消息中看到的名称。

当您从浏览器中点击该 URL 时,您应该看到以下输出:

所以我们成功创建了一个云函数,耶!

大多数开发人员希望在将函数部署到生产或测试环境之前对其进行单元测试。您可以使用以下命令在本地部署和测试您的函数:

firebase serve --only functions

它将启动一个本地服务器,并显示一个 URL,您可以点击以测试您的函数:

在这个例子中,我们看到了如何通过 functions.https 使用 HTTP 请求触发函数。现在让我们探索所有触发函数。

触发函数

Cloud Functions 可以响应其他 Firebase 产品生成的事件而执行。这些事件本质上是 Cloud Functions 的触发器。我们已经在关键特性部分看到了所有触发器的列表。我们将讨论与本书最相关的实时数据库触发器、身份验证触发器、云存储触发器和云 Firestore 触发器。其余三个可以在 Firebase 文档中进行探索:firebase.google.com/docs/functions/

实时数据库触发器

我们可以创建 Cloud Functions,以响应实时数据库更改来执行某些任务。我们可以使用 functions.database* 创建一个新的实时数据库事件函数。*为了指定函数何时执行,我们需要使用可用于处理不同数据库事件的事件处理程序之一。我们还需要指定函数将监听事件的数据库路径。

这里列出了 Cloud Functions 支持的事件:

  • onWrite():当实时数据库中的数据被创建、销毁或更改时触发

  • onCreate():当实时数据库中创建新数据时触发

onUpdate():当实时数据库中的数据更新时触发

onDelete():当实时数据库中的数据被删除时触发

我们可以看到示例实时数据库函数,它监听数据库中的用户路径,每当任何用户的数据发生变化时,将其名称转换为大写并将其设置为用户数据库的同级。在这里,我们使用了通配符 {userId},它实质上表示任何 userId

import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';

admin.initializeApp(functions.config().firebase);

export const makeUppercase = functions.database.ref('/users/{userId}')
    .onWrite(event => {
        // Grab the current value of what was written to the Realtime 
           Database.
        const original = event.data.val();
        console.log('Uppercasing', original);
        //status is a property
        const uppercase = original.name.toUpperCase();
        // You must return a Promise when performing asynchronous tasks 
           inside a Functions such as
        // writing to the Firebase Realtime Database.
        // Setting an "uppercase" sibling in the Realtime Database 
           returns a Promise.
        return event.data.ref.parent.child('uppercase').set(uppercase);
});

在这里,event.data 是一个 DeltaSnapshot。它有一个名为 'previous' 的属性,让您可以检查事件发生前保存在数据库中的内容。previous 属性返回一个新的 DeltaSnapshot,其中所有方法都指向先前的值。

身份验证触发器

使用身份验证触发器,我们可以在 Firebase 身份验证创建和删除用户时执行函数代码。

要创建一个在创建新用户时执行的 Cloud 函数,我们可以使用以下代码:

exports.userCreated = functions.auth.user().onCreate(event => { ... });

根据 Firebase 文档,Cloud Functions 的用户创建事件发生在以下情况下:

  • 开发人员使用 Firebase Admin SDK 创建帐户

  • 用户创建电子邮件帐户和密码

  • 用户首次使用联合身份提供者登录

  • 用户首次匿名身份验证登录

当用户首次使用自定义令牌登录时,不会触发 Cloud Functions 事件。如果您想要访问新创建用户的属性,可以使用event.data对象进行访问。

例如,您可以按以下方式获取用户的电子邮件和姓名:

const user = event.data; 

const email = user.email;
const name = user.displayName;

除了用户创建,如果您想要在用户删除时触发函数,可以使用onDelete()事件处理程序进行操作:

exports.deleteUser = functions.auth.user().onDelete(event => {
  // ...
});

Cloud Storage 触发器

使用 Cloud Storage 触发器,您可以对 Cloud Storage 中的文件和文件夹的创建、更新或删除操作执行 Firebase Cloud Function。我们可以使用functions.storage.为 Cloud Storage 事件创建一个新的函数。根据需求,您可以创建一个监听默认存储桶上所有更改的函数,或者可以通过指定存储桶名称来限制它:

functions.storage.object() - listen for object changes on the default storage bucket.
functions.storage.bucket('test').object() - listen for object changes on a specific bucket called 'test'

例如,我们可以编写一个将上传的文件压缩以减小大小的函数:

exports.compressFiles = functions.storage.object().onChange(event => {
  // ...
});

change事件在对象创建、修改或删除时触发。

Cloud Storage 函数公开了以下属性,可用于进一步处理文件:

  • event.data:表示存储对象。

  • event.data.bucket:文件存储的存储桶。

  • event.data.name:存储桶中的文件路径。

  • event.data.contentType:文件内容类型。

  • event.data.resourceState:两个可能的值:existsnot_exists。如果文件/文件夹已被删除,则设置为not_exists

  • event.data.metageneration:文件元数据生成的次数;对于新对象,初始值为1

Firebase Cloud Function 的最常见用例是进一步处理文件,例如压缩文件或生成图像文件的缩略图。

HTTP 触发器

我们已经看到了一个名为greetUser()的 HTTP 端点的示例,它涵盖了大部分 HTTP 端点的基本部分。只有一个重要的要点需要注意,我们应该始终正确终止我们的函数;否则,它们可能会继续运行,系统将强制终止它。我们可以使用send()redirect()end()来结束我们的函数。

考虑以下示例:

response.send("Welcome to the Cloud Function");

此外,如果您正在使用 Firebase 托管,并希望将您的 HTTP 端点与某个自定义域连接起来,您也可以这样做。

Cloud Firestore 触发器

使用 Cloud Firestore 触发器,您的云函数可以监听 Cloud Firestore 发出的事件,每当指定路径上的数据发生更改时,您的函数就会监听到。

在高层次上,它的工作方式类似于实时数据库触发器。您可以使用functions.firestore对象来监听特定事件。

它支持四个事件:创建、更新、删除和写入,如下所列:

  • onWrite(): 当任何文档被创建、更新或删除时触发

  • onCreate(): 当创建新文档时触发

  • onUpdate(): 当现有文档中的任何值更改时触发

  • onDelete(): 当文档被删除时触发

如果您想在特定文档更改时执行函数,可以编写如下函数:

exports.updateUser = functions.firestore
  .document('users/{userID}')
  .onWrite(event => {
    // An object with the current document value
    var document = event.data.data();

    // An object with the previous document value (for update or 
       delete)
    var oldDocument = event.data.previous.data();

    // perform desited database operations ...
});

现在我们将讨论云函数终止。

函数终止

云函数的优势在于您无需自行采购任何服务器,因此您只需要支付云函数运行的时间。这也给了您一个责任,即正确终止您的函数,不要让您的函数陷入无限循环,否则将在您的账单中产生额外费用。

为了正确终止函数并管理其生命周期,您可以遵循以下推荐方法:

  1. 通过返回 JavaScript promise 解决异步处理函数

  2. 使用res.redirect()res.send()res.end()结束 HTTP 函数

  3. 使用return;语句结束同步函数

总结

在本章中,我们讨论了 Firebase 的两个高级功能:Firebase Cloud Messaging 和 Firebase Cloud Functions。使用这两个功能,您可以开发一个高度交互式的无服务器应用程序。

FCM 是一个可靠的消息传递平台,用于下行和上行消息的可靠传递。我们还讨论了不同的消息类型,并看到何时使用其中一种。为了在 FCM 上有实际经验,我们增强了我们的 Helpdesk 应用程序以发送和接收通知。

我们还谈到了 Firebase 云函数,看到它如何帮助实现无服务器应用。我们介绍了如何开发云函数并将其部署到服务器上。我们还探讨了不同类型的触发器,如实时数据库触发器、HTTP 触发器、Cloud Firestore 触发器、Cloud Storage 触发器和 Auth 触发器。

在下一章中,我们将涵盖其他高级和有趣的功能,比如 Firebase 云存储和将 Firebase 应用与 Google Cloud 集成。

第八章:Firebase Cloud Storage

在本章中,我们将讨论 Firebase 的 Cloud Storage 以及它与 Google Cloud 平台的集成。我们还将探讨 Firebase 托管,它允许您在生产级环境上托管您的 Web 应用程序和静态内容(CDN)。

Cloud Storage 提供可扩展和安全的对象存储空间,因为今天大多数企业都需要可扩展的文件存储,考虑到他们通过移动应用程序、Web 应用程序或企业网站收集的大量数据。甚至部署在云上的应用程序也需要存储空间,无论是用于它们自己的资产,如图像、JavaScript、CSS、音频、视频文件,还是用户生成的内容,如文档、视频或音频。

Firebase Cloud Storage 的 SDK 使用 Google Cloud Storage 存储上传的文件。Google Cloud 平台需要一个计费账户来使用其产品,尽管他们提供了一些试用。Firebase Cloud Storage 的 SDK 使用 Google App Engine 免费层中的默认存储桶,因此您不需要计费账户。一旦您的应用程序开始增长,您还可以集成其他产品和服务,如托管计算、App Engine 或 Cloud Functions。

以下是本章将涵盖的主题列表:

  • Google Cloud Storage 概述

  • Google Cloud Storage 的关键特性

  • Google Cloud Storage 支持的存储类别

  • Google Cloud Storage 中安全性和访问控制列表(ACL)的概述

  • Firebase 的 Cloud Storage 的关键特性

  • Cloud Storage 的设置

  • 将 Firebase Cloud Storage 与 HelpDesk 应用程序集成,以上传和下载文件

  • Google App Engine 概述

  • Firebase 托管概述

  • 在 Firebase 托管上部署 HelpDesk 应用程序的前端

在深入讨论 Firebase 的 Cloud Storage 之前,让我们先讨论 Google Cloud Storage 及其特性。

Google Cloud Storage

Google Cloud 平台提供了一个安全、可扩展、具有成本效益和高性能的基础设施,包括各种服务,用于开发、管理和运行应用程序所需的一切。Google Cloud Storage 是 Google Cloud 平台的一部分,它是满足您所有对象存储需求的一站式解决方案,从存储到实时流媒体到分析到归档,应有尽有。对象存储是一种高度可扩展和具有成本效益的存储服务,可存储任何类型的数据以其原生格式。

对于您不同的存储需求,Google Cloud Storage 提供不同类别的存储,即多区域存储、区域存储、Nearline 存储和 Coldline 存储。

Google Cloud Storage 的关键特性

Google Cloud Storage 在以下关键领域提供优势:

  • 耐用性: Google Cloud Storage 旨在提供 99.999999999%的年度耐用性。数据被冗余存储。当您上传数据时,它会在后台进行复制,并使用自动校验和来确保数据完整性。

  • 可用性: Google Cloud Storage 提供高可用性,并在您需要时随时提供数据。根据 Google Cloud Storage 文档,多区域存储提供 99.95%的月度可用性,区域存储提供 99.9%的月度可用性。Nearline 和 Coldline 存储提供 99%的月度可用性。

  • 可扩展性: Google Cloud Storage 具有无限可扩展性,因此可以支持从小型到百亿字节规模的系统。

  • 一致性: Google Cloud Storage 确保读写一致性,这意味着如果写入成功,对于任何 GET 请求,全球范围内始终返回文档的最新副本。这适用于新建或覆盖对象的DELETEPUT

  • 安全性: Google Cloud Storage 具有高度安全性,并具有谷歌级别的安全性,以保护您最关键的文档、媒体和资产。它还提供不同的访问控制选项,以便您可以控制谁可以访问存储对象以及在什么级别。

  • 易于使用: Google Cloud Storage 提供简单易用的 API 和实用工具,用于处理对象存储。

我们需要了解一些 Google Cloud Storage 的基本概念,以便有效地使用它。所以,让我们在这里看看它们:

关键概念

Cloud Storage 中的所有数据都属于一个项目。一个项目包括 API、一组用户以及安全和监控设置。您可以创建任意多个项目。在项目内,我们有称为存储桶的数据容器,它们保存我们上传的数据作为对象。对象只是一个文件,还可以选择性地包含描述该文件的一些元数据。

存储桶

存储桶是容纳数据的容器。它们就像计算机文件系统中的目录,是您放置数据的基本容器。唯一的区别是,与目录不同,您不能嵌套存储桶。您在云存储中放置的所有内容都必须在存储桶内。存储桶允许您组织数据,并且还允许您控制对数据的访问权限。在设计应用程序时,由于一些强加的存储桶创建和删除速率限制,您应该计划更少的存储桶和大多数情况下更多的对象。每个项目大约每 2 秒可以进行 1 次操作。

创建存储桶时,您需要指定三件事:一个全局唯一的名称,一个默认存储类,以及存储桶及其内容存储的地理位置。如果您在存储对象时没有明确指定对象类别,则您选择的默认存储类将适用于该存储桶内的对象。

一旦创建了存储桶,除非删除并重新创建,否则无法更改存储桶的名称和位置。但是,您可以将其默认存储类更改为存储桶位置中提供的任何其他类。

存储桶名称应该是全局唯一的,并且可以与 CNAME 重定向一起使用。

您的存储桶名称必须满足以下要求:

  • 它只能包含小写字母,数字和特殊字符:破折号(-),下划线(_)和点(.)。包含点的名称需要验证。

  • 它必须以数字或字母开头和结尾。

  • 它必须是 3 到 63 个字符长。包含点的名称可以长达 222 个字符,但是每个以点分隔的组件的长度不能超过 63 个字符。

  • 它不能表示 IP 地址,例如192.168.1.1

  • 它不能以“goog”前缀开头,也不能包含 google 或 google 的拼写错误。

除了名称,您还可以将称为存储桶标签的键值元数据对与您的存储桶关联起来。存储桶标签允许您将存储桶与其他 Google Cloud Platform 服务(例如虚拟机实例和持久磁盘)分组。每个存储桶最多可以有 64 个存储桶标签。

对象

对象是您存储在云存储中的基本实体。您可以在一个存储桶中存储无限数量的对象,因此基本上没有限制。

对象由对象数据和对象元数据组成。对象数据通常是一个文件,并且对于云存储来说是不透明的(一块数据)。对象元数据是一组描述对象的键值对。

一个对象名称在存储桶中应该是唯一的;然而,不同的存储桶可以有相同名称的对象。对象名称是 Cloud Storage 中的对象元数据。对象名称可以包含任何组合的 Unicode 字符(UTF-8 编码),并且长度必须小于 1024 字节。

您的对象名称必须满足以下要求:

  • 对象名称不得包含回车或换行字符。

  • 对象名称不得以 well-known/acme-challenge 开头

您可以在对象名称中包含常见字符斜杠(/),如果您希望使其看起来好像它们存储在分层结构中,例如/team。

对象名称中常见的字符包括斜杠(/)。通过使用斜杠,您可以使对象看起来好像它们存储在分层结构中。例如,您可以将一个对象命名为/team/alpha/report1.jpg,另一个命名为object/team/alpha/report2.jpg。当您列出这些对象时,它们看起来好像是基于团队的分层目录结构;然而,对于 Cloud Storage 来说,对象是独立的数据片段,而不是分层结构。

除了名称之外,每个对象都有一个关联的数字,称为生成编号。每当您的对象被覆盖时,它的生成编号就会改变。Cloud Storage 还支持一个名为对象版本控制的功能,允许您引用被覆盖或删除的对象。一旦您为一个存储桶启用了对象版本控制,它就会创建一个存档版本的对象,该对象被覆盖或删除,并关联一个唯一的生成编号来唯一标识一个对象。

资源

Google Cloud Platform 中的任何实体都是一个资源。无论是项目、存储桶还是对象,在 Google Cloud Platform 中,它都是一个资源。

每个资源都有一个关联的唯一名称来标识它。每个存储桶都有一个资源名称,格式为projects/_/buckets/[BUCKET_NAME],其中[BUCKET_NAME]是存储桶的 ID。每个对象都有一个资源名称,格式为projects/_/buckets/[BUCKET_NAME]/objects/[OBJECT_NAME],其中[OBJECT_NAME]是对象的 ID。

还可以在资源名称的末尾附加一个#[NUMBER],表示对象的特定生成版本;#0是一个特殊标识符,表示对象的最新版本。当对象的名称以本应被解释为生成编号的字符串结尾时,#0会很有用。

对象的不可变性

在云存储中,当一个对象被上传后,在其生命周期内无法更改。成功上传对象和成功删除对象之间的时间就是对象的生命周期。这基本上意味着你无法通过追加一些数据或截断一些数据来修改现有对象。但是,你可以覆盖云存储中的对象。请注意,旧版本的文档将在成功上传新版本的文档之前对用户可用。

单个特定对象每秒只能更新或覆盖一次。

现在我们已经了解了云存储的基础知识,让我们来探索云存储中可用的存储类。

存储类

Google 云存储支持一系列基于不同用例的存储类。这些包括多区域和区域存储用于频繁访问的数据,近线存储用于较少访问的数据,如您每月不超过一次使用的数据,以及冷线存储用于极少访问的数据,如您每年只使用一次的数据。

让我们逐一了解它们。

多区域存储

多区域存储是地理冗余存储;它将您的数据存储在全球各地的多个地理位置或数据中心。它至少在存储桶的多区域位置内以至少 100 英里的距离分隔的两个地理位置存储您的数据。它非常适合低延迟高可用性应用程序,其中您的应用程序为全球用户提供内容,如视频、音频或游戏内容的实时流。由于数据冗余,它提供了高可用性。与其他存储类相比,它的成本略高。

它确保 99.95%的可用性 SLA。由于您的数据保存在多个地方,即使在自然灾害或其他干扰的情况下,它也提供高可用性。

作为多区域存储的数据只能放置在多区域位置,如美国、欧盟或亚洲,而不能放置在特定的区域位置,如 us-central1 或 asia-east1。

区域存储

区域存储将数据存储在特定的区域位置,而不是在不同地理位置分布的冗余数据。与多区域存储相比,它更便宜,并确保 99.9%的可用性 SLA。

区域存储更适合存储与使用数据的服务器实例位于同一区域位置的数据。它可以提供更好的性能,并且可以减少网络费用。

近线存储

有可能在某个时间点,应用程序或企业只频繁使用所有收集的数据中的一部分。在这种情况下,多区域或区域存储将不是理想的选择,也将是一种昂贵的选择。云存储提供了另一种存储类别,称为近线存储,可以解决之前的问题。这是一种用于存储访问频率较低的数据的低成本存储服务。在需要稍低可用性的情况下,近线存储是比多区域存储或区域存储更好的选择。例如,您每月对整个月份收集的数据进行一次分析。它确保99.0%的可用性 SLA

近线存储也更适合数据备份、灾难恢复和归档存储。然而,需要注意的是,对于一年内访问频率较低的数据,Coldline 存储是最具成本效益的选择,因为它提供了最低的存储成本。

Coldline 存储

Coldline 存储是一种用于数据归档和灾难恢复的成本非常低、高度耐用的存储服务。虽然它类似于“冷存储”,但它可以低延迟访问您的数据。这是您需要一年一两次的数据的最佳选择。您还可以将每日备份和归档文件存储到 Coldline 中,因为您不需要它们,并且只在灾难恢复时需要它们。它确保99.0%的可用性 SLA

标准存储

当用户在创建存储桶时没有指定默认存储类时,它将被视为标准存储对象。在这样的存储桶中创建的没有存储类的对象也被列为标准存储。如果存储桶位于多区域位置,则标准存储等同于多区域存储,当存储桶位于区域存储时,它被视为区域存储。

需要注意的是,定价也会相应发生变化。如果等同于多区域存储,将适用多区域存储的费用。

现在我们了解了不同的存储类别,让我们来了解一下云存储中对象的生命周期管理。

生命周期管理

许多应用程序需要在一定时间后删除或归档旧资源的功能。以下是一些示例用例:

  1. 将超过 1 年的文件从多区域存储移动到 Coldline 存储。

  2. 从 Coldline 存储中删除超过 5 年的文件。

  3. 如果启用了对象版本控制,只保留少量最近的对象版本。

幸运的是,Google Cloud Storage 提供了一个名为对象生命周期管理的功能,根据配置自动处理这种类型的操作。配置是一组适用于启用了此功能的存储桶的规则。

例如,以下规则指定删除超过 365 天的文件:

// lifecycle.json
{
  "lifecycle": {
    "rule":
    [
      {
        "action": {"type": "Delete"},
        "condition": {"age": 365}  
      }
    ]
  }
}

API 和工具

Google Cloud Platform 为云存储提供 SDK,还为不同平台的其他产品提供了一些 SDK,如 Node.js、Java、Python、Ruby、PHP 和 go。如果您不使用任何客户端库,它还提供 REST API。它还提供一个名为gsutil的命令行工具,允许您执行对象管理任务,包括以下内容:

  • 上传、下载和删除对象

  • 列出存储桶和对象

  • 移动、复制和重命名对象

  • 编辑对象和存储桶的 ACL

访问控制

有许多选项可用于管理存储桶和对象的访问权限。让我们看一下总结:

  1. 身份和访问管理IAM)权限:为您的项目和存储桶提供广泛的控制。它对于授予对存储桶的访问权限并允许对存储桶内的对象进行批量操作非常有用。

  2. 访问控制列表ACL):为用户授予对单个存储桶或对象的读取或写入访问权限提供了细粒度的控制。

  3. 签名 URL(查询字符串认证):通过签名 URL 在有限的时间内为对象授予读取或写入访问权限。

  4. 签名策略文档:允许您定义规则并对可以上传到存储桶的对象执行验证,例如,基于文件大小或内容类型进行限制。

  5. Firebase 安全规则:提供了细粒度和基于属性的规则语言,以使用 Firebase SDK 为云存储提供移动应用和 Web 应用的访问权限。

现在我们熟悉了 Google Cloud Storage 的关键概念,让我们回到 Firebase 的云存储。

Firebase 云存储的关键特性

Firebase 云存储继承了 Google 云存储的优势或特性。然而,它还具有一些额外的特性,比如声明性安全规则语言,用于指定安全规则。

云存储的关键特点如下:

  1. 易用性和健壮性:Firebase 云存储是一种简单而强大的解决方案,用于存储和检索用户生成的内容,如文档、照片、音频或视频。它提供了强大的上传和下载功能,使得文件传输在互联网连接中断时暂停,并在重新连接时从中断处恢复。这既节省时间又节省了互联网带宽。云存储的 API 也很简单,可以通过 Firebase SDK 来使用。

  2. 强大的安全性:当涉及到云存储时,我们首先想到的是安全性。它足够安全吗?我的文件会发生什么?这些问题显而易见,也很重要。答案是肯定的,Firebase 云存储非常安全。它拥有 Google 安全性的力量。它与 Firebase 身份验证集成,为开发人员提供直观的身份验证。您还可以使用声明性安全规则来限制对文件的访问,根据内容类型、名称或其他属性。

  3. 高可扩展性:Firebase 云存储由 Google 基础设施支持,提供了一个高度可扩展的存储环境,使您可以轻松地将应用程序从原型扩展到生产环境。这个基础设施已经支持了最流行和高流量的应用程序,如 Youtube、Google 照片和 Spotify。

  4. 成本效益:云存储是一种成本效益的解决方案,您只需为所使用的内容付费。您无需购买和维护用于托管文件的服务器。

  5. 与其他 Firebase 产品良好集成:云存储与其他 Firebase 产品良好集成,例如,在我们的上一章中,我们已经看到云存储触发器可以触发云函数,根据云存储上的文件操作执行一些逻辑。

我们已经了解了 Firebase 云存储的关键特点和优势。让我们看看它是如何实际运作的。

它是如何工作的?

Firebase SDK 用于云存储可以直接从客户端上传和下载文件。客户端能够重试或恢复操作,节省用户的时间和带宽。

在幕后,Cloud Storage 将您的文件存储在 Google Cloud Storage 存储桶中,因此可以通过 Firebase 和 Google Cloud 两者访问。这使您可以通过 Firebase SDK 从移动客户端上传和下载文件,并使用 Google Cloud 平台进行服务器端处理,例如生成图像缩略图或视频转码。由于 Cloud Storage 可以自动扩展,因此可以处理各种类型的应用程序数据,从小型到中型到大型应用程序。

在安全方面,Firebase Cloud Storage 的 SDK 与 Firebase 身份验证无缝集成,以识别用户。正如我们在第六章中所看到的,Firebase 安全性和规则,Firebase 还提供了声明性规则语言,让您控制对单个文件或文件组的访问。

让我们增强我们的 Helpdesk 应用程序,用户可以上传其个人资料图片。

设置 Cloud Storage

使用 Firebase SDK,我们可以轻松地在我们的应用程序中集成和设置 Firebase 的 Cloud Storage。

要设置 Cloud Storage,您将需要存储桶的 URL,您可以从我们的 Firebase 控制台获取。您可以从Storage菜单的Files选项卡中获取,如下所示:

一旦获得了引用,就可以将其添加到 Firebase 配置中。

考虑这个例子:


import firebase from 'firebase';

const config = {
    apiKey: "AIzaSyDO1VEnd5VmWd2OWQ9NQkkkkh-ehNXcoPTy-w",
    authDomain: "demoproject-7cc0d.firebaseapp.com",
    databaseURL: "https://demoproject-7cc0d.firebaseio.com",
    projectId: "demoproject-7cc0d",
    storageBucket: "gs://demoproject-7cc0d.appspot.com",
    messagingSenderId: "41428255555"
};

export const firebaseApp = firebase.initializeApp(config);

// Get a reference to the storage service,
var storage = firebase.storage();

现在我们准备使用 Cloud Storage。现在我们需要创建一个引用,用于在文件层次结构中导航。

我们可以通过调用ref()方法来获取引用,就像这样:

var storage = firebase.storage();

您还可以创建对树中特定下级节点的引用。例如,要获取对images/homepage.png的引用,我们可以这样写:

var homepageRef = storageRef.child('images/homepage.jpg');

您还可以在文件层次结构中导航到上层或下层:

// move to the parent of a reference - refers to images  var imagesRef = homepageRef.parent;

//move to highest parent or top of the bucket
var rootRef = homepageRef.root;

//chaining can be done for root, parent and child for multiple times
homepageRef.parent.child('test.jpg'); 

三个属性——fullPathnamebucket——可用于引用以更好地理解引用的文件:

// File path is 'images/homepage.jpg'
var path = homepageRef.fullPath

// File name is 'homepage.jpg'
var name = homepageRef.name

// Points to 'images'
var imagesRef = homepageRef.parent;

现在我们准备好进行上传功能。我们将扩展我们的 HelpDesk 应用程序,并为用户提供上传截图以及票务的其他细节的功能。我们将把上传的图片存储在 Cloud Storage for Firebase 中,并仅从那里检索。

上传文件

您可以上传文件或 Blob 类型、Uint8Array 或 base64 编码的字符串来上传文件到 Cloud Storage。对于我们的示例,我们将使用文件类型。如前所述,首先我们需要获取文件的完整路径的引用,包括文件名。

我们将修改AddTicketForm.jsx文件,以允许用户上传与票务相关的截图或图像。

现在,src/add-ticket/'AddTicketForm.jsx'文件看起来像下面这样。更改部分已用粗体标出并附有注释:

import React, { Component } from 'react';
import firebase from '../firebase/firebase-config';
import { ToastSuccess, ToastDanger } from 'react-toastr-basic';

class AddTicketForm extends Component {

  constructor(props) {
    super(props);
    this.handleSubmitEvent = this.handleSubmitEvent.bind(this);
    this.handleChange = this.handleChange.bind(this);
    this.onChange = this.onChange.bind(this);
    console.log(props.userInfo);

    this.state = {
      uId: props.userId,
      email: props.userInfo[0].email,
      issueType: "",
      department: "",
      comment: "",
      snapshot: null
    }
  }

  handleChange(event) {
    console.log(event.target.value);
    this.setState({
      [event.target.id]: event.target.value
    });
  }

  //handle onchange - set the snapshot value to the file selected
  onChange(e) {
 console.log("ff ",e.target.files[0] );
 this.setState({snapshot:e.target.files[0]})
 }

  handleSubmitEvent(e) {
    e.preventDefault();
    var storageRef = firebase.storage().ref();

 // Create a reference to 'image'
 var snapshotRef = storageRef.child('ticket_snapshots/'+this.state.snapshot.name);

 //get a reference to 'this' in a variable since in callback this will point to different object
 var _this = this;
 snapshotRef.put(this.state.snapshot).then(function(res) {
 console.log('Uploaded a blob or file!');
 console.log(res.metadata);

 const userId = _this.state.uId;
 var data = {
 date: Date(),
 email: _this.state.email,
 issueType: _this.state.issueType,
 department: _this.state.department,
 comments: _this.state.comment,
 status: "progress",
 snapshotURL: res.metadata.downloadURLs[0]  //save url in db to use it for download
 }

 console.log(data);

 var newTicketKey = firebase.database().ref('/helpdesk').child('tickets').push().key;
 // Write the new ticket data simultaneously in the tickets list and the user's ticket list.
 var updates = {};
 updates['/helpdesk/tickets/' + userId + '/' + newTicketKey] = data;
 updates['/helpdesk/tickets/all/' + newTicketKey] = data;

 return firebase.database().ref().update(updates).then(() => {
 ToastSuccess("Saved Successfully!!");
 this.setState({
 issueType: "",
 department: "",
 comment: "",
 snapshot: _this.state.snapshot
 });
 }).catch((error) => {
 ToastDanger(error.message);
 });

 });

    //React form data object

  }
 //render() method - snippet given below
}
export default AddTicketForm;

让我们理解上述代码:

  1. 在状态中添加一个 snapshot 属性。

  2. OnChange() - 注册onChange()事件,将文件设置在状态中的快照字段中。

  3. onHandleSubmit() - 我们已经创建了一个文件的引用,将其存储在名为'ticket_snapshots'的文件夹中,存储在 Firebase Cloud 存储中。一旦文件成功上传,我们将从响应元数据中获取一个下载 URL,并将其与其他票务详情一起存储在我们的实时数据库中。

您还需要在render()方法中进行一些 HTML 更改,以添加用于文件选择的输入字段:

 render() {
    var style = { color: "#ffaaaa" };
    return (
      <form onSubmit={this.handleSubmitEvent} >
        <div className="form-group">
          <label htmlFor="email">Email <span style={style}>*</span></label>
          <input type="text" id="email" className="form-control"
            placeholder="Enter email" value={this.state.email} disabled  
            required onChange={this.handleChange} />
        </div>
        <div className="form-group">
          <label htmlFor="issueType">Issue Type <span style={style}> *</span></label>
          <select className="form-control" value={this.state.issueType} 
          id="issueType" required onChange={this.handleChange}>
            <option value="">Select</option>
            <option value="Access Related Issue">Access Related 
            Issue</option>
            <option value="Email Related Issues">Email Related 
             Issues</option>
            <option value="Hardware Request">Hardware Request</option>
            <option value="Health & Safety">Health & Safety</option>
            <option value="Network">Network</option>
            <option value="Intranet">Intranet</option>
            <option value="Other">Other</option>
          </select>
        </div>
        <div className="form-group">
          <label htmlFor="department">Assign Department
        <span style={style}> *</span></label>
          <select className="form-control" value={this.state.department} id="department" required onChange={this.handleChange}>
            <option value="">Select</option>
            <option value="Admin">Admin</option>
            <option value="HR">HR</option>
            <option value="IT">IT</option>
            <option value="Development">Development</option>
          </select>
        </div>
        <div className="form-group">
          <label htmlFor="comments">Comments <span style={style}> *</span></label>
          (<span id="maxlength"> 200 </span> characters left)
            <textarea className="form-control" rows="3" id="comment" value={this.state.comment} onChange={this.handleChange} required></textarea>
        </div>
        <div className="form-group">
 <label htmlFor="fileUpload">Snapshot</label>
 <input id="snapshot" type="file" onChange={this.onChange} />
 </div>
        <div className="btn-group">
          <button type="submit" className="btn btn-
          primary">Submit</button>
          <button type="reset" className="btn btn-
          default">cancel</button>
        </div>
      </form>
    );
  }

我们的 add-ticket 表单看起来像这样:

然后,您可以检查您的 Firebase 控制台,看看文件上传是否正常工作。以下屏幕截图显示,我们上传的文件(helpdesk-db.png)已成功保存在 Firebase 的 Cloud Storage 中:

如前所述,Firebase 的 Cloud 存储与 Google Cloud 存储高度集成,并使用 Google Cloud 存储的存储桶来存储文件。您可以登录到 Google Cloud 平台的控制台console.cloud.google.com/storage并在存储部分进行检查。您还应该在那里看到您上传的所有文件。

下一个屏幕截图显示,文件可以从 Google Cloud 平台控制台中查看:

现在,您还可以检查您的数据库,看看已创建的票务是否具有快照 URL 属性和相应的值-文件的 downloadURL。

数据库的以下屏幕截图显示,快照 URL 已正确存储:

耶!云存储已与我们的应用集成。但是,我们还没有完成。我们需要允许用户查看已上传的图像,因此我们还将实现下载文件功能。但是,在我们转到下载文件功能之前,我想提到您应更新云存储的安全规则以控制对文件的访问。根据默认规则,要执行所有文件的.read.write操作,需要 Firebase 身份验证。

默认规则如下图所示:

但是,您应根据自己的需求进行更新。

添加文件元数据

当您上传文件时,还可以为该文件存储一些元数据,例如 Content-Type 或名称。

您可以创建一个带有键值对的 JSON 对象,并在上传文件时传递该对象。对于自定义元数据,您可以在元数据对象内创建一个对象,如下所示:

// Create file metadata including the content type  var metadata =  { contentType:  'image/jpeg',
 customMetadata: {
      'ticketNo':'12345'
  } };  // Upload the file and metadata  var uploadTask = storageRef.child('folder/file.jpg').put(file, metadata);

管理上传和错误处理

云存储允许您管理文件上传;您可以恢复、暂停或取消上传。相应的方法可在UploadTask上使用,该方法由put()putString()返回,可用作承诺或用于管理和监视上传的状态:

// Upload the file and metadata  var uploadTask = storageRef.child('folder/file.jpg').put(file);  // Pause the upload - state changes to pause uploadTask.pause();  // Resume the upload - state changes to running uploadTask.resume();  // Cancel the upload - returns an error indicating file upload is cancelled uploadTask.cancel();

您可以使用'state_change'观察者来监听进度事件。如果您想要为文件上传显示实时进度条,这非常有用:

事件类型 用途
运行中 当任务开始或恢复上传时,会触发此事件。
进度 当任何数据上传到云存储时,会触发此事件。用于显示上传进度条。
暂停 当上传暂停时,会触发此事件。

当事件发生时,将传回一个TaskSnapshot对象,可用于查看事件发生时的任务。

对象被传回。它包含以下属性:

属性 类型 描述
传输的字节数 数字 在拍摄快照时已传输的总字节数。
总字节数 数字 要上传的总字节数。
状态 firebase.storage.TaskState 当前上传状态
元数据 firebaseStorage.Metadata 包含服务器在上传完成时发送的元数据;在那之前,包含发送到服务器的元数据。
任务 firebaseStorage.UploadTask 可用于暂停、取消或恢复任务。
ref firebaseStorage.Reference 该任务来源的引用。

当您上传文件时,可能会发生一些错误。您可以使用回调中获得的错误对象来处理错误。

以下代码片段显示了管理文件上传和错误处理的示例代码:

// File
var file = this.state.snapshot;

// Create the file metadata
var metadata = {
  contentType: 'image/jpeg'
};

// Upload file and metadata to the object 'images/mountains.jpg'
var uploadTask = storageRef.child('ticket_snapshots/' + file.name).put(file, metadata);

// Listen for state changes, errors, and completion of the upload.
uploadTask.on(firebase.storage.TaskEvent.STATE_CHANGED, // or 'state_changed'
  function(snapshot) {
    // Get task progress, including the number of bytes uploaded and the total number of bytes to be uploaded
    var progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
    console.log('Upload is ' + progress + '% done');
    switch (snapshot.state) {
      case firebase.storage.TaskState.PAUSED: // or 'paused'
        console.log('Upload is paused');
        break;
      case firebase.storage.TaskState.RUNNING: // or 'running'
        console.log('Upload is running');
        break;
    }
  }, function(error) {

  // A full list of error codes is available at
  // https://firebase.google.com/docs/storage/web/handle-errors
  switch (error.code) {
    case 'storage/unauthorized':
      // User doesn't have permission to access the object
      break;

    case 'storage/canceled':
      // User canceled the upload
      break;

    case 'storage/unknown':
      // Unknown error occurred, inspect error.serverResponse
      break;
  }
}, function() {
  // Upload completed successfully, now we can get the download URL
  var downloadURL = uploadTask.snapshot.downloadURL;
});

现在,让我们转到下载文件部分。

下载文件

要下载文件,您需要使用文件的https://或 gs:// URL 获取对该文件的引用,或者您可以通过将子路径附加到存储根来构造它。

下一个代码片段显示了这些方法:

var storage = firebase.storage();  var pathReference = storage.ref('images/stars.jpg');  // Create a reference from a Google Cloud Storage URI  var gsReference = storage.refFromURL('gs://bucket/folder/file.jpg')  // Create a reference from an HTTPS URL  // Note that in the URL, characters are URL escaped!  var httpsReference = storage.refFromURL('https://firebasestorage..../file.jpg')

我们将扩展我们的 HelpDesk 应用程序,以允许用户查看已上传的票据的快照。您需要更新ticket-listing文件夹下的ViewTickets.jsx文件中的代码。我们已经从数据库中获取了一个 URL,因此我们不需要获取下载 URL 的引用:

 componentDidMount() {
    const itemsRef = firebase.database().ref('/helpdesk/tickets/'+this.props.userId);

    itemsRef.on('value', (snapshot) => {
      let tickets = snapshot.val();
      if(tickets != null){
        let ticketKeys = Object.keys(tickets);
        let newState = [];
        for (let ticket in tickets) {
          newState.push({
            id:ticketKeys,
            email:tickets[ticket].email,
            issueType:tickets[ticket].issueType,
            department:tickets[ticket].department,
            comments:tickets[ticket].comments,
            status:tickets[ticket].status,
            date:tickets[ticket].date,
            snapshotURL: tickets[ticket].snapshotURL
        });
      }
        this.setState({
          tickets: newState
        });
      }
    });
}

render() {
    return (
        <table className="table">
        <thead>
        <tr> 
            <th>Email</th>
            <th>Issue Type</th> 
            <th>Department</th> 
            <th>Comments</th>
            <th>Status</th> 
            <th>Date</th> 
            <th>Snapshot</th> 
        </tr>
        </thead>
        <tbody>
              {

                this.state.tickets.length > 0 ?
                this.state.tickets.map((item,index) => {
                return (

                  <tr key={item.id[index]}>
                    <td>{item.email}</td>
                    <td>{item.issueType}</td> 
                    <td>{item.department}</td> 
                    <td>{item.comments}</td>
                    <td>{item.status === 'progress'?'In Progress':''}</td> 
                    <td>{item.date}</td> 
                    <th><a target="_blank" href={item.snapshotURL}>View</a></th> 
                  </tr>
                )
              }) :
              <tr>
                <td colSpan="5" className="text-center">No tickets found.</td>
              </tr>
            }
        </tbody>
        </table>
    );

就像上传文件一样,您也需要以类似的方式处理下载的错误。

现在,让我们看看如何从云存储中删除文件。

删除文件

要删除文件,您首先需要获取对文件的引用,就像我们在上传和下载中看到的那样。一旦您获得了引用,就可以调用delete()方法来删除文件。它返回一个承诺,如果成功则解决,如果出现错误则拒绝。

考虑这个例子:

// Create a reference to the file to delete  var fileRef = storageRef.child('folder/file.jpg');  // Delete the file desertRef.delete().then(function()  {  // File deleted successfully  }).catch(function(error)  {  // an error occurred!  });

现在,让我们看看什么是 Google App Engine。

Google App Engine

Google App Engine 是一个“平台即服务”,它抽象了基础设施的担忧,让您只关注代码。它提供了一个根据接收的流量量自动扩展的平台。您只需要上传您的代码,它就会自动管理您的应用程序的可用性。Google App Engine 是向 Firebase 应用程序添加额外处理能力或受信任执行的简单快速的方法。

如果您有一个 App Engine 应用程序,您可以使用内置的 App Engine API 在 Firebase 和 App Engine 之间共享数据,因为 Firebase 云存储的 SDK 使用 Google App Engine 默认存储桶。这对于执行计算密集型的后台处理或图像操作非常有用,例如创建上传图像的缩略图。

Google App Engine 标准环境提供了一个环境,您的应用在其中以受支持的语言的运行时环境运行,即 Python 2.7、Java 8、Java 7、PHP 5.5 和 Go 1.8、1.6。如果您的应用代码需要这些语言的其他版本或需要其他语言,您可以使用 Google App Engine 灵活环境,在该环境中,您的应用在运行在 Google Cloud 虚拟机上的 docker 容器上。

这两个环境之间有许多不同之处,可以在 Google Cloud 文档中进行探索cloud.google.com/appengine/docs/the-appengine-environments

如果您想将现有的 Google Cloud Platform 项目导入 Firebase,并希望使任何现有的 App Engine 对象可用,您需要通过运行以下命令使用gsutil设置对象的默认访问控制,以允许 Firebase 访问它们。

gsutil -m acl ch -r -u firebase-storage@system.gserviceaccount.com:O gs://<your-cloud-storage-bucket>

Firebase 托管

Firebase Hosting 提供了一种安全且简单的方式来在 CDN 上托管您的静态网站和资源。Hosting 的主要特点如下:

  1. 通过安全连接提供:内容始终通过 SSL 安全地传输

  2. 更快的内容传递:文件在全球的 CDN 边缘被缓存,因此内容传递更快。

  3. 更快的部署:您可以在几秒钟内使用 Firebase CLI 部署您的应用

  4. 轻松快速的回滚:如果出现任何错误,只需一个命令即可回滚

Hosting 提供了部署和管理静态网站所需的所有基础设施、功能和工具,无论是单页面应用还是复杂的渐进式应用。

默认情况下,您的网站将托管在firebaseapp.com域的子域上。使用 Firebase CLI,您可以将计算机上的本地目录中的文件部署到您的托管服务器上。

当您将您的网站移至生产环境时,您可以将您自己的域名连接到 Firebase Hosting。

部署您的网站

您需要安装 Firebase CLI 来部署您的静态网页应用。

Firebase CLI 可以通过一个命令进行安装:

npm install -g firebase-tools

现在,让我们在云上部署我们的 HelpDesk 应用程序。我们有两个 HelpDesk 项目:react 应用(一个名为 code 的项目)和服务器应用(一个名为 node 的项目)。让我们首先在 Firebase Hosting 上托管或部署我们的客户端 react 应用。

进入您的项目目录(代码)并运行以下命令来初始化配置:

firebase init

如下截图所示,它会问您“您想为此文件夹设置哪个 Firebase 功能?”,您需要选择“Hosting”:

它将在项目的根目录中创建一个 firebase.json 文件。firebase.json 的结构将如下所示:

{
  "database": {
    "rules": "database.rules.json"
  },
  "hosting": {
    "public": "build",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  }
}

public 属性告诉 Firebase 要上传到托管的目录。该目录必须存在于您的项目目录中。

您现在可以使用以下命令部署您的站点:

firebase deploy

它会要求您进行 Firebase CLI 登录。您可以使用以下命令来执行:

firebase login --reauth

成功登录后,您可以再次运行 firebase deploy 命令来部署您的应用程序:

成功部署后,您将获得项目的 Hosting URL,类似于 https://YOUR-FIREBASE-APP>.firebaseapp.com。在我们的案例中,它是 demoproject-7cc0d.firebaseapp.com/现在您可以转到生成的 URL 并确认它是可访问的:

耶!我们已经在 Firebase Hosting 上部署了我们的第一个应用程序。您还可以在 Firebase 控制台的 Hosting 部分检查 URL:

您还可以通过单击连接域按钮来配置您的自定义域。它将引导您通过向导来配置您自己的域名。

摘要

本章介绍了 Google 云平台。它为您提供了对 Google 云存储和 Google 应用引擎的基本理解,以及我们如何将 Firebase 的云存储与 Google 云存储集成。我们探索了 Firebase 的云存储,并看到了如何将文件上传、下载和删除到云存储。我们还扩展了 HelpDesk 应用程序,允许用户上传屏幕截图以及工单详细信息,并查看/下载已上传的图像。此外,我们还探讨了如何在 Firebase Hosting 上部署我们的应用程序。

在下一章中,我们将讨论开发人员在使用 React 和 Firebase 时应遵循的编码标准和最佳实践,以获得更好的应用程序性能、减少错误数量,以及更易于管理的应用程序代码。

第九章:最佳实践

在深入探讨在处理 React 和 Firebase 时应遵循的最佳实践之前,让我们回顾一下之前章节中我们所看到的内容。

在之前的章节中,我们看到了 Firebase 账户设置,Firebase 与 ReactJs 的集成,使用 Firebase 身份验证提供程序进行登录认证,React 组件中的身份验证状态管理,基于角色和配置文件的数据安全,Firebase 与 React-Redux 的集成,Firebase 云消息传递,Firebase 云函数,以及在 React 组件中使用 Firebase Admin SDK API,希望你也享受了这段旅程。现在我们知道从哪里开始以及如何编写代码,但最重要的是如何遵循最佳实践编写标准的代码。

因此,当我们使用 React 和 Firebase 创建应用程序时,我们需要确保 Firebase 数据库中数据的结构以及将数据传递到 React 组件中是应用程序中最重要的部分。

在开发领域中,每个开发人员对于遵循最佳实践都有自己的看法,但我将与您分享我迄今为止观察和经验到的内容;你可能有不同的看法。

以下是本章将涵盖的主题列表:

  • Firebase 的最佳实践

  • React 和 Redux 的最佳实践

Firebase 的最佳实践

在 Firebase 中,我们都知道数据以 JSON 树格式存储,并且实时同步到每个连接的设备。因此,在使用 Firebase 构建跨平台应用程序(Web、iOS 和 Android)时,我们可以共享一个实例给所有应用程序,以接收来自实时数据库的最新更新和新数据。因此,当我们将数据添加到 JSON 树中时,它将成为现有 JSON 结构中的一个节点,并带有关联的键,因此我们始终需要计划如何保存数据以构建一个结构良好的数据库。

写入数据

在 Firebase 中,我们有四种方法可以将数据写入 Firebase 数据库:

set( ) 写入或替换数据到指定路径,比如 messages/tickets/<uid>
update( ) 更新节点的特定子节点而不替换其他子节点。我们还可以使用 update 方法将数据更新到多个位置。
push( ) 要在数据库中添加一系列数据,我们可以使用push()方法;每次调用时它都会生成一个唯一的 ID,比如 helpdesk/tickets/<unique-user-id>/<unique-ticket-id>
transaction( ) 当我们处理可能会被并发更新破坏的复杂数据时,我们可以使用这种方法,比如增量计数器。

现在,让我们看看我们的帮助台应用程序中的数据结构是如何构建的:

{
 "tickets": {
 "-L4L1BLYiU-UQdE6lKA_": {
    "comments": "Need extra 4GB RAM in my system"
    "date": "Fri Feb 02 2018 15:51:10 GMT+0530 (India Standa..."
    "department": "IT"
    "email": "harmeet_15_1991@yahoo.com"
    "issueType": "Hardware Request"
    "status": "progress"
 },
 "-L4K01hUSDzPXTIXY9oU": {
 "comments": "Need extra 4GB RAM in my system"
 "date": "Fri Feb 02 2018 15:51:10 GMT+0530 (India Standa..."
 "department": "IT"
 "email": "harmeet_15_1991@yahoo.com"
 "issueType": "Hardware Request"
 "status": "progress"
     }
  }
}

现在,让我们以前面的数据结构为例,使用set()方法存储具有自增整数的数据:

{
 "tickets": {
 "0": {
    "comments": "Need extra 4GB RAM in my system"
    "date": "Fri Feb 02 2018 15:51:10 GMT+0530 (India Standa..."
    "department": "IT"
    "email": "harmeet_15_1991@yahoo.com"
    "issueType": "Hardware Request"
    "status": "progress"
 },
 "1": {
 "comments": "Need extra 4GB RAM in my system"
 "date": "Fri Feb 02 2018 15:51:10 GMT+0530 (India Standa..."
 "department": "IT"
 "email": "harmeet_15_1991@yahoo.com"
 "issueType": "Hardware Request"
 "status": "progress"
     }
  }
}

现在,如果你看到前面的数据结构,新的票将被存储为/tickets/1。如果只有一个用户添加票,这将起作用,但在我们的应用程序中,许多用户可以同时添加票。如果两个员工同时写入/tickets/2,那么其中一个票将被另一个删除。因此,这不是推荐的做法,我们始终建议在处理数据列表时使用push()方法生成唯一 ID(参考前面的数据结构)。

避免嵌套数据

在 Firebase 实时数据库中,当我们从 JSON 树中获取数据时,我们还将获取该特定节点的所有子节点,因为当我们将数据添加到 JSON 树中时,它就成为现有 JSON 结构中的一个节点,并带有关联的键。 Firebase 实时数据库允许嵌套数据深达 32 级,因此当我们授予某人在特定节点上的读取或写入权限时,我们也在给予该节点下所有子节点的访问权限。因此,始终最佳实践是尽可能保持我们的数据结构扁平化。

让我向你展示为什么嵌套数据是不好的;请参考以下示例:

{
 // a poorly nested data architecture, because
 // iterating over "products" to get a list of names requires
 // potentially downloading hundreds of products of mobile
 "products": {
 "electronics": {
 "name": "mobile",
 "types": {
 "samsung": { "name": "Samsung S7 Edge (Black Pearl, 128 GB)", "description": "foo" },
 "apple": { ... },
 // a very long list of mobile products
 }
 }
 }
 }

使用这种嵌套数据结构,对数据进行迭代非常困难。即使是像列出产品名称这样的简单操作,也需要将整个产品树,包括所有产品列表和类型,下载到客户端。

Flattern 数据结构

在 Flattern 结构中,数据被分成不同的路径;只需在需要时轻松下载所需的节点:

{
      // products contains only meta info about each product
      // stored under the product's unique ID
      "products": {
        "electronics": {
          "name": "mobile"
        },
        "home_furniture": { ... },
        "sports": { ... }
      },
      // product types are easily accessible (or restricted)
      // we also store these by product Id
      "types": {
          "mobile":{
              "name":"samsung"
           },
      "laptop": {...},
      "computers":{...},
      "television":{...}
      "home_furniture": { ... },
      "sports": { ... }
      },
      // details are separate from data we may want to iterate quickly
      // but still easily paginated and queried, and organized by 
         product ID
      "detail": {
        "electronics": {
          "samsung": { "name": "Samsung S7 Edge (Black Pearl, 128 GB)", 
          "description": "foo" },
          "apple": { ... },
          "mi": { ... }
        },
        "home_furniture": { ... },
        "sports": { ... }
      }
    }

在前面的例子中,我们有一些轻度嵌套的数据(例如,每个产品的详细信息本身就是带有子元素的对象),但我们还按照它们将来的迭代和读取方式逻辑地组织了我们的数据。我们存储了重复的数据来定义对象之间的关系;这对于维护双向、多对多或一对多的冗余关系是必要的。这使我们能够快速有效地获取手机,即使产品或产品类型的列表扩展到数百万,或者 Firebase 规则和安全性将阻止访问某些记录。

现在可以通过每个产品只下载少量字节来迭代产品列表,快速获取用于在 UI 中显示产品的元数据。

在看到前面的扁平结构后,如果您认为在 Firebase 中逐个查找每条记录是可以的,那么是的,因为 Firebase 内部使用网络套接字和客户端库进行传入和传出请求的优化。即使我们有数万条记录,这种方法仍然可以,并且非常合理。

始终创建可以在应用用户增长时扩展的数据结构。

避免使用数组

Firebase 文档已经提到并澄清了这个话题,避免在 Firebase 数据库中使用数组,但我想强调一些使用数组存储数据的用例。

请参考以下几点;如果以下所有条件都成立,我们可以使用数组在 Firebase 中存储数据:

  • 如果一个客户端一次只能写入数据

  • 在删除键时,我们可以保存数组并进行切割,而不是使用.remove()

  • 当通过数组索引引用任何内容时,我们需要小心(可变键)

日期对象

当我们谈论在 Firebase 中对数据进行排序和过滤时,确保您在每个创建的对象中都添加了created_date键,以及日期时间戳,例如ref.set(new Date().toString())ref.set(new Date().getTime()),因为 Firebase 不支持 JavaScript 日期对象类型(ref.set(new Date());)。

自定义声明

Firebase Admin SDK 提供了在配置文件对象中添加自定义属性的功能;借助这一功能,我们可以为用户提供不同的访问控制,包括在 react-firebase 应用中基于角色的控制,因此它们并不是用来存储额外的数据(如配置文件和其他自定义数据)。我们知道这看起来是一个非常方便的方法,但强烈不建议这样做,因为这些声明存储在 ID 令牌中,这会影响性能问题,因为所有经过身份验证的请求都包含与已登录用户对应的 Firebase ID 令牌。

  • 自定义声明仅用于存储控制用户访问的数据

  • 自定义声明的大小受限,因此传递大于 1000 字节的自定义声明将引发错误

管理用户会话

管理用户会话并提示重新验证,因为每次用户登录时,用户凭据都会发送到 Firebase 身份验证后端并交换为 Firebase ID 令牌(JWT)和刷新令牌。

以下是我们需要管理用户会话的常见情况:

  • 用户被删除

  • 用户被禁用

  • 电子邮件地址和密码更改

Firebase Admin SDK 还提供了使用revokeRefreshToken()方法撤销特定用户会话的能力。它会撤销给定用户的活动刷新令牌。如果重置密码,Firebase 身份验证后端会自动撤销用户令牌。

当任何数据需要身份验证才能访问时,必须配置以下规则:

{
 "rules": {
 "users": {
 "$user_id": {
 ".read": "$user_id === auth.uid && auth.token.auth_time > (root.child('metadata').child(auth.uid).child('revokeTime').val() || 0)",
 ".write": "$user_id === auth.uid && auth.token.auth_time > (root.child('metadata').child(auth.uid).child('revokeTime').val() || 0)"
 }
 }
 }
}

在 JavaScript 中启用离线功能

当我们使用 Firebase 创建实时应用程序时,还需要监视客户端与数据库的连接和断开连接。Firebase 提供了一个简单的解决方案,可以在客户端从 Firebase 数据库服务器断开连接时写入数据库。我们可以在断开连接时执行所有操作,如写入、设置、更新和删除。

参考 Firebase 的onDisconnect()方法示例:

var presenceRef = firebase.database().ref("disconnectmessage");
// Write the string when client loses connection
presenceRef.onDisconnect().set("I disconnected!");

我们还可以附加回调函数以确保onDisconnect()方法被正确附加:

presenceRef.onDisconnect().remove(function(err) {
 if (err) {
 console.error('onDisconnect event not attached properly', err);
 }
});

要取消onDisconnect()方法,我们可以调用.cancel()方法onDisconnectRef.cancel();

为了检测连接状态,Firebase 实时数据库提供了特殊位置/.info/connected

每次应用连接状态更改时都会更新;它返回布尔值以检查客户端连接状态是否已连接:

var connectedRef = firebase.database().ref(".info/connected");
connectedRef.on("value", function(snap) {
 if (snap.val() === true) {
 alert("connected");
 } else {
 alert("not connected");
 }
});

优化数据库性能

还有一些需要关注的事项,比如在您的应用中优化 Firebase 实时数据库性能,以了解如何使用不同的实时数据库监控工具来优化您的实时数据库性能。

监控实时数据库

我们可以通过一些不同的工具收集我们的实时数据库性能数据:

  • 高级概述: 我们可以使用 Firebase 分析工具列出未索引的查询和实时读/写操作的概述。要使用分析工具,请确保已安装 Firebase CLI 并运行以下命令。

  • 计费使用估计: Firebase 使用指标在 Firebase 控制台中提供您的计费使用和高级性能指标。

  • 详细钻取: Stackdriver 监控工具为您提供了数据库随时间性能的更细粒度查看。

有关分析的更多详细信息,请访问firebase.google.com/docs/database/usage/profile

通过指标改善性能

收集数据后,根据您想要改进的性能领域,探索以下最佳实践和策略:

指标 描述 最佳实践

| 负载/利用率 | 优化数据库处理请求时的容量利用率(反映在负载io/database_load指标中)。 | 优化数据结构(firebase.google.com/docs/database/usage/optimize#data-structure)跨数据库共享数据(firebase.google.com/docs/database/usage/optimize#shard-data

提高监听器效率(firebase.google.com/docs/database/usage/optimize#efficient-listeners

使用基于查询的规则限制下载(firebase.google.com/docs/database/usage/optimize#query-rules)

优化连接(firebase.google.com/docs/database/usage/optimize#open-connections)|

活动连接 平衡数据库的同时和活动连接数,以保持在 100,000 个连接限制以下。 在数据库之间分片数据 (firebase.google.com/docs/database/usage/optimize#shard-data) 减少新连接 (firebase.google.com/docs/database/usage/optimize#open-connections)

| 出站带宽 | 如果从数据库下载的数据量比您想要的要高,您可以提高读操作的效率并减少加密开销。 | 优化连接 (firebase.google.com/docs/database/usage/optimize#open-connections) 优化数据结构 (firebase.google.com/docs/database/usage/optimize#data-structure) |

使用基于查询的规则限制下载 (firebase.google.com/docs/database/usage/optimize#query-rules)

重用 SSL 会话 (firebase.google.com/docs/database/usage/optimize#ssl-sessions)

提高监听器效率 (firebase.google.com/docs/database/usage/optimize#efficient-listeners)

限制对数据的访问 (firebase.google.com/docs/database/usage/optimize#secure-data) |

| 存储 | 确保您不存储未使用的数据,或者在其他数据库和/或 Firebase 产品之间平衡存储的数据,以保持在配额范围内。 | 清理未使用的数据 (firebase.google.com/docs/database/usage/optimize#cleanup-storage) 优化数据结构 (firebase.google.com/docs/database/usage/optimize#data-structure)

在数据库之间分片数据 (firebase.google.com/docs/database/usage/optimize#shard-data)

使用 Firebase 存储(firebase.google.com/docs/storage) |

来源:firebase.google.com/docs/database/usage/optimize

如果我们使用的是 Blaze 定价计划,我们可以创建多个实时数据库实例;然后,我们可以在同一个 Firebase 项目中创建多个数据库实例。

要从 Firebase CLI 编辑和部署规则,请按照以下步骤进行:

firebase target:apply database main my-db-1 my-db-2
firebase target:apply database other my-other-db-3
Update firebase.json with the deploy targets:
{
"database": [
{"target": "main", "rules", "foo.rules.json"},
{"target": "other", "rules": "bar.rules.json"}
]
}
firebase deploy 

确保您始终从同一位置编辑和部署规则。

将您的应用连接到多个数据库实例

使用数据库引用来访问存储在辅助数据库实例中的数据。您可以通过 URL 或应用程序获取特定数据库实例的引用。如果我们在.database()方法中不指定 URL,那么我们将获得应用程序的默认数据库实例的引用:

// Get the default database instance for an app
var database = firebase.database();
// Get a secondary database instance by URL
var database = firebase.database('https://reactfirebaseapp-9897.firebaseio.com');

要查看 Firebase 示例项目列表,请访问firebase.google.com/docs/samples/

查看 Firebase 库列表,请参考firebase.google.com/docs/libraries/

您也可以订阅www.youtube.com/channel/UCP4bf6IHJJQehibu6ai__cg频道以获取更新。

React 和 Redux 的最佳实践

每当我们有具有动态功能的组件时,数据就会出现;同样,在 React 中,我们必须处理动态数据,这似乎很容易,但并非每次都是这样。

听起来很混乱!

这很容易,但有时很困难,因为在 React 组件中,很容易通过多种方式传递属性来构建渲染树,但更新视图的清晰度不高。

在前面的章节中,这个声明已经清楚地显示出来了,所以如果你还不清楚,请参考那些。

Redux 的使用

我们知道,在单页面应用程序(SPA)中,当我们必须处理状态和时间时,难以掌握状态随时间的变化。在这里,Redux 非常有帮助,为什么?这是因为在 JavaScript 应用程序中,Redux 处理两种状态:一种是数据状态,另一种是 UI 状态,它是单页面应用程序(SPA)的标准选项。此外,请记住,Redux 可以与 Angular 或 Jquery 或 React JavaScript 库或框架一起使用。

Redux 和 Flux 之间的区别

Redux 是一个工具,而 Flux 只是一个模式,你不能像即插即用或下载一样使用它。我并不否认 Redux 受到 Flux 模式的一些影响,但正如我们无法说它百分之百看起来像 Flux 一样。

让我们继续参考一些区别。

Redux 遵循三个指导原则,如所示,这也将涵盖 Redux 和 Flux 之间的区别:

  1. 单一存储器方法: 我们在之前的图表中看到,存储器在应用程序和 Redux 中充当所有状态修改的“中间人”。它通过存储器控制两个组件之间的直接通信,是通信的单一点。在这里,Redux 和 Flux 之间的区别在于 Flux 有多个存储器方法,而 Redux 有单一存储器方法。

  2. 只读状态: 在 React 应用程序中,组件不能直接改变状态,而是必须通过“动作”将改变分派给存储器。在这里,存储器是一个对象,它有四种方法,如下所示:

  • store.dispatch(action)

  • store.subscribe(listener)

  • store.getState()

  • replaceReducer(nextReducer)

  1. 改变状态的 Reducer 函数: Reducer 函数将处理分派动作以改变状态,因为 Redux 工具不允许两个组件直接通信;因此它不仅会改变状态,还会描述状态改变的分派动作。这里的 Reducer 可以被视为纯函数。以下是编写 reducer 函数的一些特点:
  • 没有外部数据库或网络调用

  • 基于参数返回值

  • 参数是“不可变的”

  • 相同的参数返回相同的值

Reducer 函数被称为纯函数,因为它们纯粹地根据其设置的参数返回值;它没有任何其他后果。建议它们具有扁平状态。在 Flux 或 Redux 架构中,处理来自 API 返回的嵌套资源总是很困难,因此建议在组件中使用扁平状态,例如 normalize。

专业提示: const data = normalize(response, arrayOf(schema.user))  state = _.merge(state, data.entities)

不可变的 React 状态

在扁平状态中,我们可以处理嵌套资源,并且在不可变对象中,我们可以获得声明的状态不可修改的好处。

不可变对象的另一个好处是,通过它们的引用级别相等检查,我们可以获得出色的改进渲染性能。在不可变中,我们有一个shouldComponentUpdate的例子:

shouldComponentUpdate(nexProps) { 
 // instead of object deep comparsion
 return this.props.immutableFoo !== nexProps.immutableFoo
}

在 JavaScript 中,使用不可变性深冻结节点将帮助您在变异之前冻结节点,然后它将验证结果。以下示例显示了相同的逻辑:

return { 
  ...state,
  foo
}
return arr1.concat(arr2) 

我希望前面的例子已经清楚地说明了不可变 JS 的用途和好处。它也有一个不复杂的方式,但它的使用率非常低:

import { fromJS } from 'immutable'
const state = fromJS({ bar: 'biz' }) 
const newState = foo.set('bar', 'baz') 

在我看来,这是一个非常快速和美丽的功能。

React 路由

我们必须在客户端应用程序中使用路由,并且对于 ReactJS,我们还需要一个或另一个路由库,因此我建议您使用 react-router-dom 而不是 react-router。

优势:

  • 在标准化结构中声明视图可以帮助我们立即了解我们的应用视图是什么

  • 使用 react-router-dom,我们可以轻松处理嵌套视图及其渐进式视图分辨率

  • 使用浏览历史功能,用户可以向后/向前导航并恢复视图状态

  • 动态路由匹配

  • 导航时视图上的 CSS 过渡

  • 标准化的应用程序结构和行为,在团队合作时非常有用。

注意:React 路由器没有提供处理数据获取的任何方式。我们需要使用 async-props 或其他 React 数据获取机制。

很少有开发人员知道如何在 webpack 中将应用程序代码拆分成多个 JavaScript 文件:

require.ensure([], () => { 
  const Profile = require('./Profile.js')
  this.setState({
    currentComponent: Profile
  })
})

代码的拆分是必要的,因为每个代码对每个用户都没有用,也没有必要在每个页面加载该代码块,这对浏览器来说是一种负担,因此为了避免这种情况,我们应该将应用程序拆分成几个块。

现在,你可能会有一个问题,比如如果我们有更多的代码块,我们将不得不有更多的 HTTP 请求,这也会影响性能,但借助 HTTP/2 多路复用,你的问题将得到解决。您还可以将分块代码与分块哈希结合使用,这样每当更改代码时,还可以优化浏览器缓存比例。

JSX 组件

JSX 无非就是,简单来说,它只是 JavaScript 语法的扩展。此外,如果您观察 JSX 的语法或结构,您会发现它类似于 XML 编码。JSX 正在执行预处理步骤,将 XML 语法添加到 JavaScript 中。虽然您当然可以在没有 JSX 的情况下使用 React,但 JSX 使得 React 更加整洁和优雅。与 XML 类似,JSX 标记具有标记名称,属性和子级,并且在其中,如果属性值用引号括起来,该值将成为字符串。

JSX 的工作方式类似于 XML,具有平衡的开放和关闭标签,并且有助于使大型树更容易阅读,而不是“函数调用”或“对象文字”。

在 React 中使用 JSX 的优势

  • JSX 比 JavaScript 函数更容易理解和思考

  • JSX 的标记更熟悉于设计师和您团队的其他成员

  • 您的标记变得更语义化,结构化和更有意义

它有多容易可视化?

正如我所说,结构/语法在 JSX 格式中更容易可视化/注意到,这意味着与 JavaScript 相比更清晰和可读。

语义/结构化语法

在我们的应用程序中,我们可以看到 JSX 语法易于理解和可视化;在其背后,有一个具有语义化语法结构的重要原因。JSX 愉快地将您的 JavaScript 代码转换为更具语义和有意义的结构化标记。这使您能够声明组件结构和信息倾注使用类似 HTML 的语法,知道它将转换为简单的 JavaScript 函数。React 概述了您在 React.DOM 命名空间中期望的所有 HTML 元素。好处是它还允许您在标记中使用您自己编写的自定义组件。

在 React 组件中使用 PropType

在 React 组件中,我们可以从更高级别的组件传递属性,因此对属性的了解是必须的,因为这将使您能够更灵活地扩展组件并节省时间:

MyComponent.propTypes = { 
  isLoading: PropTypes.bool.isRequired,
  items: ImmutablePropTypes.listOf(
    ImmutablePropTypes.contains({
      name: PropTypes.string.isRequired,
    })
  ).isRequired
}

您还可以验证您的属性,就像我们可以使用 react 不可变 proptypes 验证不可变 JS 的属性一样。

高阶组件的好处

高阶组件只是原始组件的扩展版本:

PassData({ foo: 'bar' })(MyComponent) 

使用它的主要好处是我们可以在多种情况下使用它,例如身份验证或登录验证:

requireAuth({ role: 'admin' })(MyComponent) 

另一个好处是,使用高阶组件,您可以单独获取数据并设置逻辑以简单地查看您的视图。

Redux 架构的好处

与其他框架相比,它有更多的优点:

  1. 它可能没有任何其他影响。

  2. 正如我们所知,不需要绑定,因为组件不能直接交互。

  3. 状态是全局管理的,因此管理不当的可能性较小。

  4. 有时,对于中间件,管理其他方式的影响可能会很困难。

从上述观点来看,Redux 架构非常强大,而且具有可重用性。

我们还可以使用 ReactFire 库构建 React-Firebase 应用程序,只需几行 JavaScript。我们可以通过 ReactFireMixin 将 Firebase 数据集成到 React 应用程序中。

总结

在本书的最后一章中,我们介绍了在使用 React 和 Firebase 时应遵循的最佳实践。我们还看到了如何使用不同的工具来监视应用程序性能,以减少错误的数量。我们还谈到了 Firebase 实时数据库中数据结构的重要性,并讨论了动态数据传递给 React 组件。我们还研究了其他关键因素,如 JSX、React 路由和 React PropTypes,在 React 应用程序中是最常用的元素。我们还了解到 Redux 在维护单页应用程序SPAs)的状态方面有很大帮助。

posted @ 2024-05-16 14:50  绝不原创的飞龙  阅读(37)  评论(0编辑  收藏  举报