Skip to content

Latest commit

 

History

History
1202 lines (902 loc) · 43.7 KB

File metadata and controls

1202 lines (902 loc) · 43.7 KB

四、连接 React 到 Redux 和 Firebase

第 3 章与 Firebase的认证中,我们了解了如何构建 React 组件以及它们如何管理自己的状态。在本章中,我们将了解如何有效地管理应用程序状态。我们将详细探讨 Redux,并了解如何以及何时需要在 React 应用程序中使用 Redux。我们还将看到如何将 React、Redux 和 Firebase 这三个应用程序与一个示例座位预订应用程序集成。它将是一个通用的座位预订应用程序,可用于任何座位预订,如公共汽车座位预订、体育场座位预订或剧院座位预订,但数据结构有一些细微的变化。

以下是我们将在本章中介绍的主题列表:

  • 使用 React 启动器套件进行 React 设置
  • Firebase 实时数据库与 React 的集成
  • 重演
  • 集成 React、Redux 和 Firebase 实时数据库
  • 座位预订应用程序实际上涵盖了上述所有概念

让我们建立我们的开发环境。

To set up the React development environment, you will need to have node version 6.0 or greater.

React 设置

要设置我们的开发环境,第一步是 React 设置。有不同的选项可用于安装 React。如果您已经有一个现有的应用程序,并且想要添加 React,您可以使用软件包管理器(如npm使用以下命令安装它:

npm init
npm install --save react react-dom

但是,如果您正在启动一个新项目,最简单的入门方法是使用 React Starter 工具包。只需转到命令提示符并执行以下命令即可安装 React Starter 工具包:

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命令。

如果在可视化代码编辑器中看到项目结构,则其外观如下所示:

创建的应用程序结构足够好,可以开始使用,但是对于我们的座位预订应用程序,我们需要在更好的包结构中组织我们的源代码。

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

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

首先,我们的座位预订应用程序中将包含以下组件:

  • SeatSeat应用程序的对象和基本构建块
  • SeatRow:代表一排座位
  • SeatList:代表所有席位的列表
  • Cart:表示包含所选座位信息的购物车

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

让我们从我们的第一个组件开始,称为座椅。这将在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 语法,我们已经在第 2 章中看到,将 React 应用程序与 Firebase集成。

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

  • number:指给该座位的号码或 ID
  • price:指预订该座位的费用金额
  • status:如果座位已预订或可用,则表示座位状态

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

在我们的座位预订应用程序中,当用户选择座位时,我们需要将其添加到购物车中,以便用户可以签出/预订车票。要做到这一点,我们需要处理一个座位的onClick()。现在,我们只是在 click handler 函数中打印一个 console 语句,但是我们需要编写一个逻辑来将所选座位推送到购物车。当我们将 Redux 集成到我们的应用程序中时,我们将在后面的部分研究它。

如果已经预订了任何座位,显然我们不允许用户选择,因此根据状态,我们将禁用已预订的座位。

Seat 是我们的基本构建块,它将从父组件(即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;

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

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

我们的下一个组件是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:节点表示可以呈现任何内容:数字、字符串、元素或包含这些类型的数组(或片段)。

我们应用程序中的下一个也是最后一个组件是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 了。虽然我们已经在第 2 章*中看到了 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 添加 Firebase 依赖项:

npm install firebase

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

import firebase from 'firebase';

导入语句是 ES6 功能,因此如果您不知道,请参阅中的 ES6 文档 http://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事件。此方法在连接侦听器时执行一次,每次数据更改(包括子项)时执行一次。事件回调传递一个快照,其中包含该位置的所有数据,包括子数据。如果没有数据,则返回的快照为空。

It is important to note that the value event will be fired every time the data is changed at the given path, including data changes in children. Hence, it is recommended that we attach the listener only at the lowest level needed to limit the snapshot size.

在这里,我们从 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 应用程序中的数据和事件的常用选择。这是通量模式的简化版本。让我们详细探讨 Redux。

什么是 Redux?

在这个技术时代,由于 web 应用程序的需求变得越来越复杂,应用程序的状态管理在代码级别上面临着很多挑战。例如,在实时应用程序中,除了保存在数据库中的数据外,许多数据都存储在缓存中,以便更快地检索。类似地,在 UI 端,由于复杂的用户界面,例如多个选项卡、多个路由、分页、过滤器和面包屑等,应用程序状态管理成为一项非常困难的任务。

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

为了减少此类问题,React 等库删除了直接 DOM 操作和异步。但是,它仅应用于视图或表示层。数据的状态管理取决于应用程序开发人员。这就是 Redux 出现的地方。

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

Redux is a predictable state container for JavaScript apps.

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

Redux 非常简单。当我们谈论 Redux 时,我们需要记住三个核心术语:存储、操作和还原。它们是这样的:

  1. Store:应用程序的状态将由 Store 管理,Store 是维护应用程序状态树的对象。记住,在你的 Redux 应用程序中应该有一个商店。此外,由于强加的限制,您不能直接操作或修改应用程序存储。
  2. 动作:若要更改商店中的某些内容,您需要发送一个动作。动作只不过是一个简单的 JavaScript 对象,它描述了所发生的事情。通过操作,我们可以了解应用程序中发生的事情以及原因,从而使状态可预测。
  3. Reducer:最后,为了将动作和状态粘合在一起,我们编写了一个 Reducer,它是一个简单的 JavaScript 函数,将状态和动作作为参数,并返回应用程序的新状态。

既然我们现在已经对 Redux 有了基本的了解,那么让我们检查一下 Redux 的三个基本原则,如下所示:

  1. 单一真相来源:存储只是一个状态容器。如前所述,在你的 React 应用程序中,应该只有一个商店,因此它被认为是真相的来源。单个对象树也使调试应用程序变得容易。
  2. 状态为只读:要更改状态,应用程序必须发出一个描述发生了什么的操作。没有视图或其他函数可以直接写入状态。此限制可防止任何不必要的状态更改。每个操作都将在一个集中的对象上按顺序执行,以便我们可以在任何时间点获得最新状态。由于操作只是普通对象,我们可以调试和序列化它们以进行测试。
  3. 使用纯函数进行更改:为了描述通过调度动作对状态树的转换,请编写纯函数/减缩器。纯函数这个术语是什么意思?如果函数每次向其传递一组给定的参数时返回相同的值,则该函数是纯函数。纯函数不修改其输入参数。相反,它们使用输入计算一个值,然后返回该计算值。我们的归约器是纯函数,将状态和动作作为输入,并返回新状态,而不是突变状态。您可以拥有任意数量的缩减器,并且还建议您将大的缩减器拆分为较小的缩减器,以管理应用程序树的特定部分。Reducer 是 JavaScript 函数,因此您可以向它们传递额外的数据。它们可以像普通函数一样构建,可以在表示组件和容器组件的应用程序中使用。

表示和容器组件

在我们的应用程序中,席位列表负责获取数据并进行渲染。这是可以的,对于小型或示例应用程序来说效果很好,但这样一来,我们就失去了 React 的一些好处,其中之一就是可重用性。SeatList除非在完全相同的情况下,否则不容易重复使用,那么解决方案是什么?

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

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

React bindings for Redux 也接受了分离**表示组件和容器组件的想法。**表示组件关注的是用户对事物的看法,而不是事物的工作方式。类似地,容器组件关心的是事物如何工作,而不是事物的外观。

让我们看看下表中的比较:

| 表象成分 | 集装箱组件 | | 关注用户视图或事物的外观 | 关注数据和事情将如何运作 | | 作为道具从父组件获取/读取数据 | 已连接到 Redux 状态 | | 拥有自己的 DOM 标记和样式 | 很少或没有 DOM 标记,也没有自己的样式 | | 很少有状态 | 通常是有状态的 | | 手写的 | 可以手写或由 React Redux 生成 |

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

  • 组件的可重用性
  • 可以减少重复代码,使应用程序更易于管理
  • 不同的团队,如设计师和 JS/应用程序开发人员可以并行工作

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

Redux 的基础知识

Redux 非常简单,所以不要害怕,只要看看一些花哨的术语,比如减缩器、动作等等。我们将介绍 Redux 应用程序的基本构建块,您也会有同样的感受。

行动

正如我们在本章开头所看到的,动作只不过是一个简单的 JavaScript 对象,它描述了所发生的事情。更改状态就是发出一个描述发生了什么的操作。此外,对于商店来说,行动只是真相或信息的来源。

下面是一个动作创建者示例。

每个动作类型应定义为一个常量:

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

动作的type描述已经发生的动作类型。如果应用程序足够大,则可以将操作类型作为字符串常量分离到单独的模块/文件中,并在操作中使用它。

现在,你可能有一个问题,我的行动应该是什么结构?我们 有 here 类型,然后直接添加行。你的问题的答案是,除了类型,你可以有你的行动的任何结构。但是,有一个定义动作的标准。

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

动作创造者

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

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

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

还原剂

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

(previousState, action) =>newState

在减速器内,您不应该做的事情:

  • 修改其参数
  • API 调用和路由
  • 调用其他非纯函数,例如,Date.now()

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

建议尽可能规范化状态对象,避免对象嵌套。

百货商店

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

以下方法可用于存储:

  • getState():给出应用程序的当前状态树
  • dispatch(action):用于发送动作
  • subscribe(listener):订阅门店变更
  • replaceReducer(nextReducer):这是一个高级 API,用 Store 替换当前使用的减速机

数据流

我们已经看到了 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 存储可用于组件层次结构。请注意,如果不在提供程序中包装父组件,则无法使用connect()
  • <SeatBookingApp>:是我们的父组件,将在容器组件包下定义。它的代码将类似于我们前面看到的App.js中的代码。
  • Middleware:它就像其他语言中的拦截器,提供了一个第三方扩展点,从发送一个动作到它到达 reducer 的那一刻,例如日志或记录器。如果不应用中间件,则需要在所有操作和还原程序中手动添加记录器。
  • applyMiddleware:告诉 Redux store 如何处理和设置中间件。注意 rest 参数(…中间件)的用法;它表示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;

我们的 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 提供的函数或对象,以便容器可以轻松地将该函数传递给其道具上的子组件。我们正在传递bookSeats函数,以便 Cart 组件中的Checkout按钮可以调用它。
  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'

We will have three actions:

  • GET_SEATS:从 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/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,并将它们组合成一个对象

类似地,我们将有一个购物车减速器。

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

它公开了与购物车操作相关的减速器功能。

现在,我们将有一个最终的联合收割机减速器。

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,并了解如何实现用户和访问管理。