五、掌握 Redux

本章将介绍以下配方:

  • 创建 Redux 存储
  • 创建动作创建者和分派动作
  • 用 Redux 实现 Firebase

介绍

Redux 是 JavaScript 应用的可预测状态容器。这意味着 Redux 可以与普通 JavaScript 或框架/库(如 Angular 和 jQuery)一起使用。Redux 主要是一个库,负责发布状态更新和对操作的响应。Redux 广泛用于 React。不是直接修改应用的状态,而是通过发出称为操作的事件来处理修改。这些事件是始终返回两个关键属性的函数(也称为动作创建者),一个是type(表示正在执行的动作类型,这些类型通常应定义为字符串常量)和一个payload(要在动作中传递的数据)。这些函数发出还原程序订阅的事件。减缩器是纯函数,用于确定每个操作将如何转换应用的状态。所有状态更改都在一个地方处理:Redux 存储。

如果没有 Redux,就需要复杂的模式来在应用组件之间传递更改。Redux 通过使用应用存储向组件广播状态更改来简化这一过程。在 React Redux 应用中,组件将订阅存储,而存储将向组件广播更改。此图完美地描述了 Redux 的工作原理:

Redux proposes to handle our Redux state as immutable. However, the objects and arrays in JavaScript are not, which can cause us to mutate the state by mistake directly.

以下是 Redux 的三个原则:

  • 单一真相来源:整个应用的状态存储在单个存储中的对象树中。
  • 状态为只读:更改状态的唯一方法是发出一个动作,一个描述发生了什么的对象。
  • 使用纯函数进行更改:要指定操作如何转换状态树,您需要编写纯还原器。

This information was extracted from the Official site of Redux. To read more, visit https://redux.js.org/introduction/three-principles.

什么是动作?

操作是将数据从应用发送到应用商店的有效信息负载。它们是商店唯一的信息来源。您可以使用store.dispatch()将它们发送到商店。这些动作是简单的 JavaScript 对象,必须具有一个名为type的属性,该属性指示正在执行的动作的类型,以及一个payload属性,即动作中包含的信息。

什么是不变性?

不变性是 Redux 中的一个基本概念。要更改状态,必须返回一个新对象。

以下是 JavaScript 中的不可变类型:

  • 数字
  • 一串
  • 布尔值
  • 未定义
  • 无效的

以下是 JavaScript 中的可变类型:

  • 阵列
  • 功能
  • 物体

为什么是不变性?

  • 更清晰:我们知道是谁改变了状态(减速器)
  • 性能更好
  • 调试简单:我们可以使用 Redux 开发工具(我们将在第 12 章测试和调试中介绍该主题)

我们可以通过以下方式处理不变性:

  • ES6
    • Object.assign
    • Spread操作员(…)
  • 图书馆
    • Immutable.js
    • Lodash(合并和扩展)

什么是减速器?

减速机类似于绞肉机。在绞肉机中,我们在顶部添加配料(状态和动作),在另一端得到结果(新状态):

从技术上讲,reducer 是一个纯函数,它接收两个参数(当前状态和操作),并且根据操作返回一个新的不可变状态。

部件类型

容器:

  • 关注事物如何运作
  • 已连接到 Redux
  • 发送重复操作
  • react-redux生成

表象的:

  • 关注事物的外观
  • 未连接到 Redux
  • 通过道具接收数据或功能
  • 大部分时间是无国籍的

重复流量

当我们从 UI(React组件)调用操作时,Redux 流开始。此操作将信息(typepayload发送到存储,存储与还原器交互,根据操作类型更新状态。一旦 reducer 更新了状态,它将值返回到存储,然后存储将新值发送到 React 应用:

创建 Redux 存储

存储区保存应用的整个状态,更改内部状态的唯一方法是发送一个操作。商店不是一个阶级;它只是一个对象,上面有一些方法。

存储方法如下所示:

  • getState()返回应用的当前状态
  • dispatch(action):发送一个动作,这是触发状态更改的唯一方式
  • subscribe(listener):添加一个更改侦听器,该侦听器在调度操作时随时调用
  • replaceReducer(nextReducer)替换存储当前用于计算状态的减速器

**# 准备

要使用 Redux,我们需要安装以下软件包:

npm install redux react-redux 

怎么做。。。

首先,我们需要在src/shared/redux/configureStore.js为我们的商店创建一个文件:

  1. 让我们继续编写以下代码:
 // Dependencies
  import { createStore } from 'redux';

 // Root Reducer
  import rootReducer from '../reducers';

  export default function configureStore(initialState) {
    return createStore(
      rootReducer,
      initialState
    );
  }

File: src/shared/redux/configureStore.js

  1. 我们需要做的第二件事是在public/index.html文件中创建initialState变量。现在,我们将创建一个设备状态来检测用户是使用手机还是桌面:
<body>
  <div id="root"></div>

  <script>
    // Detecting the user device
    const isMobile = /iPhone|Android/i.test(navigator.userAgent);

    // Creating our initialState
    const initialState = {
      device: {
        isMobile
      }
    };

    // Saving our initialState to the window object
    window.initialState = initialState;
  </script>
</body>

File: public/index.html

  1. 我们需要在共享文件夹中创建一个reducers目录。我们需要创建的第一个减速器是deviceReducer
export default function deviceReducer(state = {}) {
  return state;
}

File: src/shared/reducers/deviceReducer.js

  1. 一旦我们创建了deviceReducer,我们需要创建一个index.js文件,在这里我们将导入所有的减速器,并将它们合并成一个rootReducer
// Dependencies
import { combineReducers } from 'redux';

// Shared Reducers
import device from './deviceReducer';

const rootReducer = combineReducers({
  device
});

export default rootReducer;

File: src/shared/reducers/index.js

  1. 现在让我们修改src/index.js文件。我们需要创建 Redux 存储并将其传递给提供商:
import React from 'react';
import { render } from 'react-dom';
import { BrowserRouter as Router } from 'react-router-dom';
import { Provider } from 'react-redux';
import './index.css';

// Redux Store
import configureStore from './shared/redux/configureStore';

// Routes
import AppRoutes from './routes';

// Configuring Redux Store
const store = configureStore(window.initialState);

// DOM
const rootElement = document.getElementById('root');

// App Wrapper
const renderApp = Component => {
  render(
    <Provider store={store}>
      <Router>
        <Component />
      </Router>
    </Provider>,
    rootElement
  );
};

// Rendering our App
renderApp(AppRoutes);
  1. 现在我们可以编辑我们的Home组件。我们需要使用react-redux中的connect将我们的组件连接到 Redux,然后使用mapStateToProps检索设备的状态:
import React from 'react';
import { bool } from 'prop-types';
import { connect } from 'react-redux';

const Home = props => {
  const { isMobile } = props;

  return (
    <div className="Home">
      <h1>Home</h1>

      <p>
        You are using: 
        <strong>{isMobile ? 'mobile' : 'desktop'}</strong>
      </p>
    </div>
  );
};

Home.propTypes = {
  isMobile: bool
};

function mapStateToProps(state) {
  return {
    isMobile: state.device.isMobile
  };
}

function mapDispatchToProps() {
  return {};
}

export default connect(mapStateToProps, mapDispatchToProps)(Home);

它是如何工作的。。。

如果您正确地遵循了所有步骤,您应该能够在桌面上使用 Chrome 查看此视图:

如果您激活 Chrome 设备模拟器,或者使用真实设备或 iPhone 模拟器,您将看到以下视图:

什么是 MapStateTrops?

mapStateToProps函数通常会让很多人感到困惑,但它很容易理解。它获取状态的一部分(从存储中),并将其作为prop传递到组件中。换句话说,接收到mapStateToProps的参数是 Redux 状态,在内部您将拥有您在rootReducer中定义的所有还原器,然后您返回一个对象,其中包含您需要发送到组件的数据。下面是一个例子:

function mapStateToProps(state) {
  return {
    isMobile: state.device.isMobile
  };
}

如您所见,该状态有一个device节点,即我们的deviceReducer;很多时候,还有其他方法让很多人感到困惑。一种方法是使用 ES6 解构和箭头函数,如下所示:

const mapStateToProps = ({ device }) => ({
  isMobile: device.isMobile
});

另外,在connect中间件中还有另一种直接实现的方法。通常,一开始,这可能会让人困惑,但一旦你习惯了,就应该这样做。我通常会这样做:

export default connect(({ device }) => ({
  isMobile: device.isMobile
}), null)(Home);

在我们将 Redux 状态映射到 props 之后,我们可以像这样检索数据:

const { isMobile } = props;

如您所见,对于第二个参数mapDispatchToProps,我直接发送了一个空值,因为我们还没有在这个组件中分派操作。在下一个食谱中,我将讨论mapDispatchToProps

创建动作创建者和分派动作

行动是 Redux 最关键的部分;他们负责在我们的 Redux 商店中触发状态更新。在此配方中,我们将展示上列出的前 100 种加密货币 http://www.coinmarketcap.com 使用他们的公共 API。

准备

对于这个方法,我们需要安装 Axios(浏览器和 Node.js 的基于 promise 的 HTTP 客户端)和 Redux Thunk(Thunk 是一个包装表达式以延迟其计算的函数):

npm install axios redux-thunk

怎么做。。。

我们将使用上一个配方(Repository: /Chapter05/Recipe1/store中创建的相同代码,并添加一些修改:

  1. 首先,我们需要创建新文件夹:src/actionssrc/reducerssrc/components/Coinssrc/shared/utils

  2. 我们需要创建的第一个文件是src/actions/actionTypes.js,在这里我们需要为我们的操作添加常量:

export const FETCH_COINS_REQUEST = 'FETCH_COINS_REQUEST';
export const FETCH_COINS_SUCCESS = 'FETCH_COINS_SUCCESS';
export const FETCH_COINS_ERROR = 'FETCH_COINS_ERROR';

File: src/actions/actionTypes.js

  1. 也许您想知道为什么我们需要创建一个与字符串同名的常量。这是因为,当使用常量时,我们不能有重复的常量名称(如果我们错误地重复一个,我们将得到一个错误)。另一个原因是,操作在两个文件中使用,在实际操作文件中,然后在我们的 reducer 中。为了避免重复字符串,我决定创建actionTypes.js文件并编写一次常量。
  2. 我喜欢将我的行为分为三部分:requestreceivederror。我将这些主要操作称为基本操作,我们需要在src/shared/redux/baseActions.js中为这些操作创建一个文件:
// Base Actions
export const request = type => ({
  type
});

export const received = (type, payload) => ({
  type,
  payload
});

export const error = type => ({
  type
});

File: src/shared/redux/baseActions.js

  1. 在我们构建了baseActions.js文件之后,我们需要为我们的操作创建另一个文件,这个文件应该在src/actions/coinsActions.js中。对于此配方,我们将使用来自CoinMarketCap的公共 APIhttps://api.coinmarketcap.com/v1/ticker/
// Dependencies
import axios from 'axios';

// Action Types
import {
  FETCH_COINS_REQUEST,
  FETCH_COINS_SUCCESS,
  FETCH_COINS_ERROR
} from './actionTypes';

// Base Actions
 import { request, received, error } from '../shared/redux/baseActions';

export const fetchCoins = () => dispatch => {
  // Dispatching our request action
  dispatch(request(FETCH_COINS_REQUEST));

  // Axios Data
  const axiosData = {
    method: 'GET',
    url: 'https://api.coinmarketcap.com/v1/ticker/',
    headers: {
      Accept: 'application/json',
      'Content-Type': 'application/json'
    }
  };

  // If everything is correct we dispatch our received action   
  // otherwise our error action.
  return axios(axiosData)
    .then(response => dispatch(received(FETCH_COINS_SUCCESS, response.data)))
    .catch(err => {
      // eslint-disable-next-line no-console
      console.log('AXIOS ERROR:', err.response); 
      dispatch(error(FETCH_COINS_ERROR));
    });
};

File: src/actions/coinsActions.js

  1. 一旦动作文件准备就绪,我们需要创建 reducer 文件,根据动作更新 Redux 状态。让我们在src/reducers/coinsReducer.js: 中创建一个文件
// Action Types
import {
  FETCH_COINS_SUCCESS,
  FETCH_SINGLE_COIN_SUCCESS
} from '../actions/actionTypes';

// Utils
import { getNewState } from '../shared/utils/frontend';

// Initial State
const initialState = {
  coins: []
};

export default function coinsReducer(state = initialState, action) {
  switch (action.type) {
    case FETCH_COINS_SUCCESS: {
      const { payload: coins } = action;

      return getNewState(state, {
        coins
      });
    }

    default:
      return state;
  }
};

File: src/reducers/coinsReducer.js

  1. 然后我们需要将减速器添加到src/shared/reducers/index.js中的combineReducers
// Dependencies
import { combineReducers } from 'redux';

// Components Reducers
import coins from '../../reducers/coinsReducer';

// Shared Reducers
import device from './deviceReducer';

const rootReducer = combineReducers({
  coins,
  device
});

export default rootReducer;

File: src/shared/reducers/index.js

  1. 如您所见,我包括了getNewStateutil;这是一个执行Object.assign的基本函数,但更明确、更容易理解,因此让我们在src/shared/utils/frontend.js创建utils文件。在我们第一次尝试渲染时,组件需要使用isFirstRender函数来验证我们的数据是否为空:
export function getNewState(state, newState) {
  return Object.assign({}, state, newState);
}

export function isFirstRender(items) {
  return !items || items.length === 0 || Object.keys(items).length === 0;
}

File: src/shared/utils/frontend.js

  1. 现在我们需要在src/components/Coins/index.js处创建一个Container组件。在引言中,我提到了两种类型的组件:containerpresentational。容器必须连接到 Redux,并且不应该有任何 JSX 代码,只有我们的mapStateToPropsmapDispatchToProps,然后在导出时,我们可以传递要渲染的presentational组件,作为道具传递操作值和 Redux 状态。要创建mapDispatchToProps函数,我们需要使用 Redux 库中的bindActionCreators方法。这会将我们的dispatch方法绑定到我们通过的所有操作。在没有bindActionCreators的情况下,有不同的方法可以做到这一点,但使用这种方法被认为是良好的做法:
// Dependencies
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';

// Components
import Coins from './Coins';

// Actions
import { fetchCoins } from '../../actions/coinsActions';

// Mapping our Redux State to Props
const mapStateToProps = ({ coins }) => ({
  coins
});

// Binding our fetchCoins action.
const mapDispatchToProps = dispatch => bindActionCreators(
  {
    fetchCoins
  },
  dispatch
);

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Coins);

File: src/components/Coins/index.js

  1. 我们在集装箱中进口的Coins组件如下:
// Dependencies
import React, { Component } from 'react';
import { array } from 'prop-types';

// Utils
import { isFirstRender } from '../../shared/utils/frontend';

// Styles
import './Coins.css';

class Coins extends Component {
  static propTypes = {
    coins: array
  };

  componentWillMount() {
    const { fetchCoins } = this.props;

    // Fetching coins action.
    fetchCoins();
  }

  render() {
    const { coins: { coins } } = this.props;

    // If the coins const is an empty array, 
    // then we return null.
    if (isFirstRender(coins)) {
      return null;
    }
    return (
      <div className="Coins">
        <h1>Top 100 Coins</h1>

        <ul>
          {coins.map((coin, key) => (
            <li key={key}>
              <span className="left">
                {coin.rank} {coin.name} {coin.symbol}
              </span>
              <span className="right">${coin.price_usd}</span>
            </li>
          ))}
        </ul>
      </div>
    );
  }
}

export default Coins;

File: src/components/Coins/Coins.jsx

  1. 该组件的 CSS 如下所示:
.Coins ul {
    margin: 0 auto;
    margin-bottom: 20px;
    padding: 0;
    list-style: none;
    width: 300px;
}

.Coins ul a {
    display: block;
    color: #333;
    text-decoration: none;
    background: #5ed4ff;
}

.Coins ul a:hover {
    color: #333;
    text-decoration: none;
    background: #baecff;
}

.Coins ul li {
    border-bottom: 1px solid black;
    text-align: left;
    padding: 10px;
    display: flex;
    justify-content: space-between;
}

File: src/components/Coins/Coins.css

  1. 在我们的src/shared/redux/configureStore.js文件中,我们需要导入redux-thunk并使用applyMiddleware方法在我们的 Redux 存储中使用此库:
// Dependencies
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';

// Root Reducer
import rootReducer from '../reducers';

export default function configureStore(initialState) {
  const middleware = [
    thunk
  ];

  return createStore(
    rootReducer,
    initialState,
    applyMiddleware(...middleware)
  );
}
  1. 让我们在Header组件中添加/coins的链接:
import React from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import logo from '../img/logo.svg';

// We created a component with a simple arrow function.
const Header = props => {
  const {
    title = 'Welcome to React',
    url = 'http://localhost:3000'
  } = props;

  return (
    <header className="App-header">
      <a href={url}>
        <img src={logo} className="App-logo" alt="logo" />
      </a>

      <h1 className="App-title">{title}</h1>

      <ul>
        <li><Link to="/">Home</Link></li>
        <li><Link to="/about">About</Link></li>
        <li><Link to="/coins">Coins</Link></li>
        <li><Link to="/notes">Notes</Link></li>
        <li><Link to="/contact">Contact</Link></li>
      </ul>
    </header>
  );
};

// Even with Functional Components we are able to validate our PropTypes.
Header.propTypes = {
  title: PropTypes.string.isRequired,
  url: PropTypes.string
};

export default Header;
  1. 最后,拼图的最后一块是将我们的组件(容器)添加到我们的src/routes.jsx文件中:
// Dependencies
import React from 'react';
import { Route, Switch } from 'react-router-dom';

// Components
import App from './components/App';
import About from './components/About';
import Coins from './components/Coins';
import Contact from './components/Contact';
import Home from './components/Home';
import Notes from './components/Notes';
import Error404 from './components/Error/404';

const AppRoutes = () => (
  <App>
    <Switch>
      <Route path="/" component={Home} exact />
      <Route path="/about" component={About} exact />
      <Route path="/coins" component={Coins} exact />
      <Route path="/contact" component={Contact} exact />
      <Route path="/notes" component={Notes} exact />
      <Route path="/notes/:noteId" component={Notes} exact />
      <Route component={Error404} />
    </Switch>
  </App>
);

export default AppRoutes;

它是如何工作的。。。

如果您打开 API(https://api.coinmarketcap.com/v1/ticker/ )您将看到如下 JSON 对象:

我们将在中获得一组拥有前 100 枚硬币的物品 https://coinmarketcap.com 。如果正确遵循所有步骤,您将能够看到以下视图:

用 Redux 实现 Firebase

Firebase 是一个后端即服务(BaaS),是谷歌云平台的一部分。Firebase 最流行的服务之一是实时数据库,它使用 WebSocket 同步数据。Firebase 还提供文件存储、身份验证(社交媒体和电子邮件/密码身份验证)、托管等服务。

您可以将 Firebase 主要用于实时应用,但如果愿意,也可以将其用作非实时应用的常规数据库。Firebase 受到多种语言(如 JavaScript、Java、Python 和 Go)以及 Android、iOS 和 web 等平台的支持。

Firebase 是免费的,但是,当然,如果您需要更多容量,他们会根据您的项目要求制定不同的计划。您可以在查看价格 https://firebase.google.com/pricing

对于这个食谱,我们将使用 Firebase 的免费服务来展示一些流行的短语。这意味着您需要使用您的谷歌电子邮件创建一个帐户 https://firebase.google.com

准备

在 Firebase 上注册后,您需要通过单击 Firebase 控制台中的添加项目来创建新项目:

我会将我的项目命名为codejobs;当然,您可以随意命名:

如您所见,Firebase 自动向我们的项目 ID 添加了一个随机代码,但如果您想确保项目 ID 不存在,并且在您必须接受条款和条件并单击“创建项目”按钮后,您可以对其进行编辑:

现在,您必须选择将 Firebase 添加到 web 应用选项,您将获得有关应用的信息:

现在,在仪表板中开发数据库,并单击创建数据库按钮:

*

之后,在锁定模式下选择启动选项,然后单击启用按钮:

然后,在页面顶部,选择下拉列表并选择实时数据库选项:

一旦我们创建了实时数据库,让我们导入一些数据。为此,您可以在下拉列表中选择导入 JSON 选项:

让我们创建一个基本 JSON 文件来导入短语数据:

  {
    "phrases": [
      {
        "phrase": "A room without books is like a body without a 
       soul.",
        "author": "Marcus Tullius Cicero"
      },
      {
        "phrase": "Two things are infinite: the universe and human 
        stupidity; and I'm not sure about the universe.",
        "author": "Albert Einstein"
      },
      {
        "phrase": "You only live once, but if you do it right, once is 
         enough.",
        "author": "Mae West"
      },
      {
        "phrase": "If you tell the truth, you don't have to remember 
         anything.",
        "author": "Mark Twain"
      },
      {
        "phrase": "Be yourself; everyone else is already taken.",
        "author": "Oscar Wilde"
      }
    ]
  }

File: src/data/phrases.json

将此文件保存在数据目录中,然后将其导入 Firebase 数据库:

As you can see in the red warning, All data at this location will be overwritten. This means that if you have any old data in the database, it will be replaced, so be careful with importing new data into your database.

如果所有操作都正确,您将看到如下导入的数据:

现在我们需要更改权限,以便能够在数据库中读写。如果转到“规则”选项卡,您将看到如下内容:

现在,让我们将其更改为 true,然后单击“发布”按钮:

最后,我们在 Firebase 上完成了所有需要的步骤。现在,让我们在 React 中创建 Firebase 应用。我们将重复使用CoinMarketCapRepository: Chapter05/Recipe2/coinmarketcap的最后一个配方。我们需要做的第一件事是安装 firebase 依赖项:

    npm install firebase      

怎么做。。。

我从上一个配方中删除了一些组件,我只关注短语应用。让我们按照以下步骤创建它:

  1. 复制项目配置并将其替换到文件中:
 export const fbConfig = {
    ref: 'phrases',
    app: {
      apiKey: 'AIzaSyASppMJh_6QIGTeXVBeYszzz7iTNTADxRU',
      authDomain: 'codejobs-2240b.firebaseapp.com',
      databaseURL: 'https://codejobs-2240b.firebaseio.com',
      projectId: 'codejobs-2240b',
      storageBucket: 'codejobs-2240b.appspot.com',
      messagingSenderId: '278058258089'
    }
  };

File: src/config/firebase.js

  1. 在此之后,我们需要创建一个文件来管理我们的 Firebase 数据库,我们将导出我们的ref(我们的短语表):
  import firebase from 'firebase';
  import { fbConfig } from '../../config/firebase';

  firebase.initializeApp(fbConfig.app);

 export default firebase.database().ref(fbConfig.ref);

File: src/shared/firebase/database.js

  1. 让我们为我们的组件准备一切。首先,转到routes文件并将Phrases容器添加到路由的根路径:
  // Dependencies
  import React from 'react';
  import { Route, Switch } from 'react-router-dom';

 // Components
  import App from './components/App';
  import Error404 from './components/Error/404';
  import Phrases from './components/Phrases';

  const AppRoutes = () => (
    <App>
      <Switch>
        <Route path="/" component={Phrases} exact />
        <Route component={Error404} />
      </Switch>
    </App>
  );

 export default AppRoutes;

File: src/routes.jsx

  1. 现在让我们创建actionTypes文件:
 export const FETCH_PHRASE_REQUEST = 'FETCH_PHRASE_REQUEST';
  export const FETCH_PHRASE_SUCCESS = 'FETCH_PHRASE_SUCCESS';

  export const ADD_PHRASE_REQUEST = 'ADD_PHRASE_REQUEST';

  export const DELETE_PHRASE_REQUEST = 'DELETE_PHRASE_REQUEST';
  export const DELETE_PHRASE_SUCCESS = 'DELETE_PHRASE_SUCCESS';

  export const UPDATE_PHRASE_REQUEST = 'UPDATE_PHRASE_REQUEST';
  export const UPDATE_PHRASE_SUCCESS = 'UPDATE_PHRASE_SUCCESS';
  export const UPDATE_PHRASE_ERROR = 'UPDATE_PHRASE_ERROR';

File: src/actions/actionTypes.js

  1. 现在,在我们的操作中,我们将执行四个任务(获取、添加、删除和更新),就像 CRUD(创建、读取、更新和删除)一样:
 // Firebase Database
  import database from '../shared/firebase/database';

 // Action Types
 import {
    FETCH_PHRASE_REQUEST,
    FETCH_PHRASE_SUCCESS,
    ADD_PHRASE_REQUEST,
    DELETE_PHRASE_REQUEST,
    DELETE_PHRASE_SUCCESS,
    UPDATE_PHRASE_REQUEST,
    UPDATE_PHRASE_SUCCESS,
    UPDATE_PHRASE_ERROR
  } from './actionTypes';

  // Base Actions
 import { request, received } from '../shared/redux/baseActions';

  export const fetchPhrases = () => dispatch => {
    // Dispatching our FETCH_PHRASE_REQUEST action
    dispatch(request(FETCH_PHRASE_REQUEST));

    // Listening for added rows
    database.on('child_added', snapshot => {
      dispatch(received(
        FETCH_PHRASE_SUCCESS, 
        { 
          key: snapshot.key, 
          ...snapshot.val() 
        }
      ));
    });

    // Listening for updated rows
    database.on('child_changed', snapshot => {
      dispatch(received(
        UPDATE_PHRASE_SUCCESS, 
        { 
          key: snapshot.key, 
          ...snapshot.val() 
        }
      ));
    });

    // Lisetining for removed rows
    database.on('child_removed', snapshot => {
      dispatch(received(
        DELETE_PHRASE_SUCCESS, 
        { 
          key: snapshot.key 
        }
      ));
    });
  };

 export const addPhrase = (phrase, author) => dispatch => {
    // Dispatching our ADD_PHRASE_REQUEST action
    dispatch(request(ADD_PHRASE_REQUEST));

    // Adding a new element by pushing to the ref.
 // NOTE: Once this is executed the listener    // will be on fetchPhrases (child_added).
    database.push({
      phrase,
      author
    });
  }

  export const deletePhrase = key => dispatch => {
    // Dispatching our DELETE_PHRASE_REQUEST action
    dispatch(request(DELETE_PHRASE_REQUEST));

 // Removing element by key
 // NOTE: Once this is executed the listener 
 // will be on fetchPhrases (child_removed).
    database.child(key).remove();
  }

  export const updatePhrase = (key, phrase, author) => dispatch => {
    // Dispatching our UPDATE_PHRASE_REQUEST action
    dispatch(request(UPDATE_PHRASE_REQUEST));

    // Collecting our data...
    const data = {
      phrase,
      author
    };

    // Updating an element by key and data
    database
      // First we select our element by key
      .child(key) 
      // Updating the data in this point
      .update(data) 
      // Returning the updated data
      .then(() => database.once('value')) 
      // Getting the actual values of the snapshat
      .then(snapshot => snapshot.val()) 
      .catch(error => {
        // If there is an error we dispatch our error action
        dispatch(request(UPDATE_PHRASE_ERROR));

        return {
          errorCode: error.code,
          errorMessage: error.message
        };
      });
  };

File: src/actions/phrasesActions.js In Firebase, we don't use a regular ID. Instead, Firebase uses a key value as an ID. The imported data is like a basic array, with keys 0, 1, 2, 3, 4, and so on, so for that data, each key is used as an ID. But when we create data through Firebase, the keys are going to be unique string values with random code, such as -lg4fgFQkfm.

  1. 添加操作后,我们可以创建 reducer 文件:
  // Action Types
  import {
    FETCH_PHRASE_SUCCESS,
    DELETE_PHRASE_SUCCESS,
    UPDATE_PHRASE_SUCCESS,
  } from '../actions/actionTypes';

  // Utils
  import { getNewState } from '../shared/utils/frontend';

  // Initial State
  const initialState = {
    phrases: []
  };

  export default function phrasesReducer(state = initialState, action) {
    switch (action.type) {
      case FETCH_PHRASE_SUCCESS: {
        const { payload: phrase } = action;

        const newPhrases = [...state.phrases, phrase];

        return getNewState(state, {
          phrases: newPhrases
        });
      }

      case DELETE_PHRASE_SUCCESS: {
        const { payload: deletedPhrase } = action;

        const filteredPhrases = state.phrases.filter(
          phrase => phrase.key !== deletedPhrase.key
        );

        return getNewState(state, {
          phrases: filteredPhrases
        });
      }

      case UPDATE_PHRASE_SUCCESS: {
        const { payload: updatedPhrase } = action;

        const index = state.phrases.findIndex(
          phrase => phrase.key === updatedPhrase.key
        );

        state.phrases[index] = updatedPhrase;

        return getNewState({}, {
          phrases: state.phrases
        });
      }

      default:
       return state;
    }
  };

File: src/reducers/phrasesReducer.js

  1. 现在让我们创建 Redux 容器。我们将在我们的组件中包含我们将分派的所有操作,并连接 Redux 以获取短语状态:
  // Dependencies
  import { connect } from 'react-redux';
  import { bindActionCreators } from 'redux';

  // Components
  import Phrases from './Phrases';

 // Actions
  import {
    addPhrase,
    deletePhrase,
    fetchPhrases,
    updatePhrase
  } from '../../actions/phrasesActions';

  const mapStateToProps = ({ phrases }) => ({
    phrases: phrases.phrases
  });

  const mapDispatchToProps = dispatch => bindActionCreators(
    {
      addPhrase,
      deletePhrase,
      fetchPhrases,
      updatePhrase
    },
    dispatch
  );

 export default connect(
    mapStateToProps,
    mapDispatchToProps
  )(Phrases);

File: src/components/Phrases/index.js

  1. 那么我们的Phrases组件将如下所示:
  // Dependencies
  import React, { Component } from 'react';
  import { array } from 'prop-types';

  // Styles
  import './Phrases.css';

  class Phrases extends Component {
    static propTypes = {
      phrases: array
    };

    state = {
      phrase: '',
      author: '',
      editKey: false
    };

    componentWillMount() {
      this.props.fetchPhrases();
    }

    handleOnChange = e => {
      const { target: { name, value } } = e;

      this.setState({
        [name]: value
      });
    }

    handleAddNewPhrase = () => {
      if (this.state.phrase && this.state.author) {
        this.props.addPhrase(
          this.state.phrase, 
          this.state.author
        );

        // After we created the new phrase we clean the states
        this.setState({
          phrase: '',
          author: ''
        });
      }
    }

    handleDeleteElement = key => {
      this.props.deletePhrase(key);
    }

    handleEditElement = (key, phrase, author) => {
      this.setState({
        editKey: key,
        phrase,
        author
      });
    }

    handleUpdatePhrase = () => {
      if (this.state.phrase && this.state.author) {
        this.props.updatePhrase(
          this.state.editKey,
          this.state.phrase,
          this.state.author
        );

        this.setState({
          phrase: '',
          author: '',
          editKey: false
        });
      }
    }

    render() {
      const { phrases } = this.props;

      return (
        <div className="phrases">
          <div className="add">
            <p>Phrase: </p>

            <textarea 
              name="phrase" 
              value={this.state.phrase} 
              onChange={this.handleOnChange}
            ></textarea>

            <p>Author</p>

            <input 
              name="author" 
              type="text" 
              value={this.state.author} 
              onChange={this.handleOnChange} 
            />

            <p>
              <button 
                onClick={
                  this.state.editKey 
                    ? this.handleUpdatePhrase 
                    : this.handleAddNewPhrase
                }
              >
                {this.state.editKey 
                  ? 'Edit Phrase' 
                  : 'Add New Phrase'}
              </button>
            </p>
          </div>

          {phrases && phrases.map(({ key, phrase, author }) => (
            <blockquote key={key} className="phrase">
              <p className="mark">
                “
              </p>

              <p className="text">
                {phrase}
              </p>

              <hr />

              <p className="author">
                {author}
              </p>

              <a 
 onClick={() => { 
                  this.handleDeleteElement(key);
                }}
              >
                X
              </a>
              <a 
                onClick={
                  () => this.handleEditElement(key, phrase, author)
                }
              >
                Edit
              </a>
            </blockquote>
          ))}
        </div>
      );
    }
  }

  export default Phrases;

File: src/components/Phrases/Phrases.jsx

  1. 最后,我们的样式文件如下所示:
 hr {
    width: 98%;
    border: 1px solid white;
  }

 .phrase {
    background-color: #2db2ff;
    border-radius: 17px;
    box-shadow: 2px 2px 2px 2px #E0E0E0;
    color: white;
    font-size: 20px;
    margin-top: 25px;
    overflow: hidden;
    border-left: none;
    padding: 20px;
  }

 .mark {
    color: white;
    font-family: "Times New Roman", Georgia, Serif;
    font-size: 100px;
    font-weight: bold;
    margin-top: -20px;
    text-align: left;
    text-indent: 20px;
  }

 .text {
    font-size: 30px;
    font-style: italic;
    margin: 0 auto;
    margin-top: -65px;
    text-align: center;
    width: 90%;
  }

 .author {
    font-size: 30px;
  }

  textarea {
    width: 50%;
    font-size: 30px;
    padding: 10px;
    border: 1px solid #333;
  }

  input {
    font-size: 30px;
    border: 1px solid #333;
  }

  a {
    cursor: pointer;
    float: right;
    margin-right: 10px;
  }

File: src/components/Phrases/Phrases.css

它是如何工作的。。。

理解 Firebase 如何与 Redux 协同工作的关键在于,您需要知道 Firebase 使用 WebSocket 来同步数据,这意味着数据是实时流式传输的。检测数据变化的方法是使用database.on()方法。

fetchPhrases()动作中,我们有三个 Firebase 侦听器:

  • database.on('child_added'):有两个功能。第一个从 Firebase(第一次)逐行带来数据。第二个功能是检测何时向数据库中添加新行并实时更新数据。
  • database.on('child_changed'):检测现有行的变化。当我们执行一行的更新时,这会起作用。
  • database.on('child_removed'):检测行何时被删除。

还有另一个名为database.once('value')的方法,它的作用与child_added相同,但返回数组中的数据,并且只返回一次。这意味着它不会检测到像child_added这样的动态变化。

如果运行应用,您将看到以下视图:

区块报价太大,无法全部放入,但我们的最后一个是:

让我们修改我们的phrases.json并添加一个新行:

  {
    "phrases": [
      {
        "phrase": "A room without books is like a body without a 
        soul.",
        "author": "Marcus Tullius Cicero"
      },
      {
        "phrase": "Two things are infinite: the universe and human 
         stupidity; and 
         I'm not sure about the universe.",
        "author": "Albert Einstein"
      },
      {
        "phrase": "You only live once, but if you do it right, once is 
        enough.",
        "author": "Mae West"
      },
      {
        "phrase": "If you tell the truth, you don't have to remember 
        anything.",
        "author": "Mark Twain"
      },
      {
        "phrase": "Be yourself; everyone else is already taken.",
        "author": "Oscar Wilde"
      },
      {
        "phrase": "Hasta la vista, baby!",
        "author": "Terminator"
      }
    ]
  }

如果我们转到 Firebase 并再次导入 JSON,我们将看到数据将实时更新,而不会刷新页面:

现在,如果你看到删除短语的链接,让我们删除第一个(马库斯·图利乌斯·西塞罗)。如果在另一个选项卡中打开 Firebase 页面,您将看到数据正在被实时删除:

此外,如果您添加一个新行(使用 textarea 和 input),您将看到实时反映:

正如我前面提到的,当我们从 React 应用添加新数据时,Firebase 将为新数据生成唯一的密钥,而不是导入 JSON。在本例中,对于我添加的新短语,-LJSYCHLHEe9QWiAiak4键被创建。

即使我们更新了一行,我们也可以看到更改是实时反映的:

正如您所看到的,所有操作都很容易实现,使用 Firebase,我们节省了大量的时间,否则这些时间将花在后端服务上。Firebase 太棒了!***