React-秘籍-二-

52 阅读41分钟

React 秘籍(二)

原文:zh.annas-archive.org/md5/AADE5F3EA1B3765C530CB4A24FAA7E7E

译者:飞龙

协议:CC BY-NC-SA 4.0

第三章:处理事件,绑定和有用的 React 包

在本章中,将涵盖以下示例:

  • 使用构造函数绑定方法与使用箭头函数

  • 创建带有事件的表单元素

  • 使用 react-popup 在模态框中显示信息

  • 实施 Airbnb React/JSX 样式指南

  • 使用 React Helmet 更新我们的标题和 meta 标签

介绍

本章包含与处理事件、在 React 中绑定方法相关的示例,并且我们将实现一些最有用的 React 包。

使用构造函数绑定方法与使用箭头函数

在这个示例中,我们将学习在 React 中绑定方法的两种方式:使用构造函数和使用箭头函数。

如何做...

这个示例很简单,目标是使用类构造函数和箭头函数绑定方法:

  1. 让我们创建一个名为Calculator的新组件。我们将创建一个带有两个输入和一个按钮的基本计算器。我们组件的框架如下:
  import React, { Component } from 'react';
  import './Calculator.css';

  class Calculator extends Component {
    constructor() {
      super();

      this.state = {
        number1: 0,
        number2: 0,
        result: 0
      };
    }

    render() {
      return (
        <div className="Calculator">
          <input 
            name="number1" 
            type="text" 
            value={this.state.number1} 
          />
          {' + '}
          <input 
            name="number2" 
            type="text" 
            value={this.state.number2} 
          />

          <p><button>=</button></p>
          <p className="result">{this.state.result}</p>
        </div>
      );
    }
  }

  export default Calculator;

文件:src/components/Calculator/Calculator.js

  1. 现在我们将添加两种新方法,一种用于处理输入(onChange事件),一种用于管理结果按钮(onClick)。我们可以使用相同的handleOnChange方法来处理两个输入。由于我们有字段的名称(与状态相同),我们可以动态更新每个状态,在handleResult方法中,我们只需对两个数字求和。
    handleOnChange(e) {
      const { target: { value, name } } = e;

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

    handleResult(e) {
      this.setState({
        result: this.state.number1 + this.state.number2
      });
    }
  1. 现在在我们的render方法中,我们需要为输入和按钮添加事件:
    render() {
      return (
        <div className="Calculator">
          <input 
 onChange={this.handleOnChange} 
            name="number1" 
            type="text" 
            value={this.state.number1} 
          />
          {' + '}
          <input 
 onChange={this.handleOnChange} 
            name="number2" 
            type="text" 
            value={this.state.number2}
          />
          <p>
            <button onClick={this.handleResult}>=</button>
          </p>
          <p className="result">{this.state.result}</p>
        </div>
      );
    }
  1. 我们的 CSS 代码如下:
  .Calculator {
    margin: 0 auto;
    padding: 50px;
  }

  .Calculator input {
    border: 1px solid #eee;
    font-size: 16px;
    text-align: center;
    height: 50px;
    width: 100px;
  }

  .Calculator button {
    background: #0072ff;
    border: none;
    color: #fff;
    font-size: 16px;
    height: 54px;
    width: 150px;
  }

  .Calculator .result {
    border: 10px solid red;
    background: #eee;
    margin: 0 auto;
    font-size: 24px;
    line-height: 100px;
    height: 100px;
    width: 100px;
  }

文件:src/components/Calculator/Calculator.css

  1. 如果您现在运行应用程序,您会发现如果尝试在输入框中输入内容或单击按钮,您将收到如下错误:

  1. 原因是我们需要将这些方法绑定到类上才能访问它。让我们首先使用构造函数绑定我们的方法:
    constructor() {
      super();

      this.state = {
        number1: 0,
        number2: 0,
        result: 0
      };

      // Binding methods
      this.handleOnChange = this.handleOnChange.bind(this);
      this.handleResult = this.handleResult.bind(this);
    }
  1. 如果您想要在组件顶部列出所有方法,使用构造函数绑定方法是一个不错的选择。如果您查看Calculator组件,它应该是这样的:

  1. 现在让我们使用箭头函数来自动绑定我们的方法,而不是在构造函数中进行绑定。为此,您需要在构造函数中删除绑定方法,并将handleOnChangehandleResult方法更改为箭头函数:
    constructor() {
      super();

      this.state = {
        number1: 0,
        number2: 0,
        result: 0
      };
    }

    // Changing this method to be an arrow function
    handleOnChange = e => {
      const { target: { value, name } } = e;

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

    // Changing this method to be an arrow function
    handleResult = e => {
      this.setState({
        result: this.state.number1 + this.state.number2
      });
    }
  1. 你会得到相同的结果。我更喜欢使用箭头函数来绑定方法,因为你使用的代码更少,而且你不需要手动将方法添加到构造函数中。

它是如何工作的...

如你所见,你有两种选项来绑定你的 React 组件中的方法。目前最常用的是构造函数选项,但箭头函数变得越来越受欢迎。你可以决定哪种绑定选项你最喜欢。

使用事件创建表单元素

你可能已经注意到在上一章中,我们使用了一些简单的带有事件的表单,但在这个示例中,我们将更深入地了解这个主题。在第六章中,使用 Redux Form 创建表单,我们将学习如何处理带有 Redux Form 的表单。

如何做到...

让我们创建一个名为Person的新组件:

  1. 我们将在此组件中使用的骨架如下:
 import React, { Component } from 'react';
  import './Person.css';

  class Person extends Component {
    constructor() {
      super();

      this.state = {
        firstName: '',
        lastName: '',
        email: '',
        phone: ''
      };
    }

    render() {
      return (
        <div className="Person">

        </div>
      );
    }
  }

  export default Person;

文件:src/components/Person/Person.js

  1. 让我们向我们的表单添加firstNamelastNameemailphone字段。render方法应该如下所示:
  render() {
    return (
      <div className="Person">
        <form>
          <div>
            <p><strong>First Name:</strong></p>
            <p><input name="firstName" type="text" /></p>
          </div>

          <div>
            <p><strong>Last Name:</strong></p>
            <p><input name="lastName" type="text" /></p>
          </div>

          <div>
            <p><strong>Email:</strong></p>
            <p><input name="email" type="email" /></p>
          </div>

          <div>
            <p><strong>Phone:</strong></p>
            <p><input name="phone" type="tel" /></p>
          </div>

          <p>
            <button>Save Information</button>
          </p>
        </form>
      </div>
    );
  }
  1. 让我们为我们的表单使用这些 CSS 样式:
  .Person {
    margin: 0 auto;
  }

  .Person form input {
    font-size: 16px;
    height: 50px;
    width: 300px;
  }

  .Person form button {
    background: #0072ff;
    border: none;
    color: #fff;
    font-size: 16px;
    height: 50px;
    width: 300px;
  }

文件:src/components/Person/Person.css

  1. 如果你运行你的应用程序,你应该看到这个视图:

  1. 让我们在输入中使用我们的本地状态。在 React 中,我们从输入中检索值的唯一方法是将每个字段的值连接到特定的本地状态,就像这样:
  render() {
    return (
      <div className="Person">
        <form>
          <div>
            <p><strong>First Name:</strong></p>
            <p>
              <input 
                name="firstName" 
                type="text" 
 value={this.state.firstName} 
              />
            </p>
          </div>

          <div>
            <p><strong>Last Name:</strong></p>
            <p>
              <input 
                name="lastName" 
                type="text" 
 value={this.state.lastName} 
              />
            </p>
          </div>

          <div>
            <p><strong>Email:</strong></p>
            <p>
              <input 
                name="email" 
                type="email" 
 value={this.state.email} 
              />
            </p>
          </div>

          <div>
            <p><strong>Phone:</strong></p>
            <p>
              <input 
                name="phone" 
                type="tel" 
 value={this.state.phone} 
              />
            </p>
          </div>

          <p>
            <button>Save Information</button>
          </p>
        </form>
      </div>
    );
  }

如果你尝试输入一些内容,你会注意到你无法写任何东西,这是因为所有的输入都连接到本地状态,我们更新本地状态的唯一方法是重新渲染已输入的文本。

  1. 正如你所想象的,我们更新本地状态的唯一方法是检测输入的变化,这将在用户输入时发生。让我们为onChange事件添加一个方法:
  handleOnChange = e => {
    const { target: { value } } = e;

    this.setState({
      firstName: value
    });
  }

就像我在上一个示例中提到的,当我们在方法中使用箭头函数时,我们会自动将类绑定到方法。否则,你需要在构造函数中绑定方法。在我们的firstName输入中,我们需要在onChange方法中调用这个方法:

    <input 
      name="firstName" 
      type="text" 
      value={this.state.firstName} 
 onChange={this.handleOnChange} 
    />
  1. 但是这里有一个问题。如果我们有四个字段,那么您可能会认为您需要创建四种不同的方法(每个状态一个),但是有一种更好的解决方法:在e (e.target.name)对象中获取输入名称的值。这样,我们可以使用相同的方法更新所有状态。我们的handleOnChange方法现在应该是这样的:
    handleOnChange = e => {
      const { target: { value, name } } = e;

      this.setState({
        [name]: value
      });
    }
  1. 通过对象中的([name])语法,我们可以动态更新表单中的所有状态。现在我们需要将这个方法添加到所有输入的onChange中。完成后,您将能够在输入框中输入内容:
    render() {
      return (
        <div className="Person">
          <form>
            <div>
              <p><strong>First Name:</strong></p>
              <p>
                <input 
                  name="firstName" 
                  type="text" 
                  value={this.state.firstName} 
 onChange={this.handleOnChange} 
                />
              </p>
            </div>

            <div>
              <p><strong>Last Name:</strong></p>
              <p>
                <input 
                  name="lastName" 
                  type="text" 
                  value={this.state.lastName} 
 onChange={this.handleOnChange} 
                />
              </p>
            </div>

            <div>
              <p><strong>Email:</strong></p>
              <p>
                <input 
                  name="email" 
                  type="email" 
                  value={this.state.email} 
 onChange={this.handleOnChange} 
                />
              </p>
            </div>

            <div>
              <p><strong>Phone:</strong></p>
              <p>
                <input 
                  name="phone" 
                  type="tel" 
                  value={this.state.phone} 
 onChange={this.handleOnChange} 
                />
              </p>
            </div>

            <p>
              <button>Save Information</button>
            </p>
          </form>
        </div>
      );
    }
  1. 所有表单都需要提交它们从用户那里收集到的信息。我们需要使用表单的onSubmit事件,并调用handleOnSubmit方法通过本地状态检索所有输入值:
  handleOnSubmit = e => {
    // The e.preventDefault() method cancels the event if it is                            
    // cancelable, meaning that the default action that belongs to  
    // the event won't occur.
    e.preventDefault();

    const { firstName, lastName, email, phone } = this.state;
    const data = {
      firstName,
      lastName,
      email,
      phone
    };

    // Once we have the data collected we can call a Redux Action  
    // or process the data as we need it.
    console.log('Data:', data);
  }
  1. 创建完这个方法后,我们需要在form标签的onSubmit事件上调用它:
  <form onSubmit={this.handleOnSubmit}>
  1. 现在您可以测试这个。打开您的浏览器控制台,当您在输入框中输入一些值时,您将能够看到数据:

  1. 我们需要验证必填字段。假设firstNamelastName字段是必填的。如果用户没有在字段中填写值,我们希望添加一个错误类来显示输入框周围的红色边框。您需要做的第一件事是为错误添加一个新的本地状态:
      this.state = {
        firstName: '',
        lastName: '',
        email: '',
        phone: '',
        errors: {
          firstName: false,
          lastName: false
        }
      };
  1. 您可以在这里添加任何您想要验证的字段,并且值是布尔值(true表示有错误,false表示没有错误)。然后,在handleOnSubmit方法中,如果有错误,我们需要更新状态:
    handleOnSubmit = e => {
     // The e.preventDefault() method cancels the event if it is   
     // cancelable, meaning that the default action that belongs to  
     // event won't occur.
    e.preventDefault();

      const { firstName, lastName, email, phone } = this.state;

      // If firstName or lastName is missing then we update the   
      // local state with true
      this.setState({
 errors: {
 firstName: firstName === '',
 lastName: lastName === ''
 }
 });

      const data = {
        firstName,
        lastName,
        email,
        phone
      };

      // Once we have the data collected we can call a Redux Action  
      // or process the data as we need it.
      console.log('Data:', data);
    }
  1. 现在,在您的render方法中,您需要在firstNamelastName字段的className属性中添加一个三元验证,如果您想要更花哨,您还可以在输入框下方添加一个错误消息:
    render() {
      return (
        <div className="Person">
          <form onSubmit={this.handleOnSubmit}>
            <div>
              <p><strong>First Name:</strong></p>
              <p>
                <input
                  name="firstName"
                  type="text"
                  value={this.state.firstName}
                  onChange={this.handleOnChange}
                  className={
                    this.state.errors.firstName ? 'error' : ''
                  }                
                />
                {this.state.errors.firstName 
                  && (<div className="errorMessage">Required 
                field</div>)}
              </p>
            </div>

            <div>
              <p><strong>Last Name:</strong></p>
              <p>
                <input
                  name="lastName"
                  type="text"
                  value={this.state.lastName}
                  onChange={this.handleOnChange}
                  className={
                    this.state.errors.lastName ? 'error' : ''
                  }
                />
                {this.state.errors.lastName 
                  && <div className="errorMessage">Required 
                field</div>}
              </p>
            </div>

            <div>
              <p><strong>Email:</strong></p>
              <p>
                <input 
                  name="email" 
                  type="email" 
                  value={this.state.email} 
                  onChange={this.handleOnChange} 
                />
              </p>
            </div>

            <div>
              <p><strong>Phone:</strong></p>
              <p>
                <input name="phone" type="tel" value=
                {this.state.phone} 
                 onChange={this.handleOnChange} />
              </p>
            </div>

            <p>
              <button>Save Information</button>
            </p>
          </form>
        </div>
      );
    }
  1. 最后一步是添加错误类,.error.errorMessage
    .Person .error {
      border: 1px solid red;
    }

    .Person .errorMessage {
      color: red;
      font-size: 10px;
    }
  1. 如果您现在提交表单而没有填写firstNamelastName,您将会得到这个视图:

  1. 完整的Person组件应该是这样的:
  import React, { Component } from 'react';
  import './Person.css';

  class Person extends Component {
    constructor() {
      super();

      this.state = {
        firstName: '',
        lastName: '',
        email: '',
        phone: '',
        errors: {
          firstName: false,
          lastName: false
        }
      };
    }

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

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

    handleOnSubmit = e => {
 // The e.preventDefault() method cancels the event if it is 
      // cancelable, meaning that the default action that belongs 
      // to the event won't occur.
      e.preventDefault();

      const { firstName, lastName, email, phone } = this.state;

      // If firstName or lastName is missing we add an error class
      this.setState({
        errors: {
          firstName: firstName === '',
          lastName: lastName === ''
        }
      });

      const data = {
        firstName,
        lastName,
        email,
        phone
      };

      // Once we have the data collected we can call a Redux Action     
      // or process the data as we need it.
      console.log('Data:', data);
    }

    render() {
      return (
        <div className="Person">
          <form onSubmit={this.handleOnSubmit}>
            <div>
              <p><strong>First Name:</strong></p>
              <p>
                <input
                  name="firstName"
                  type="text"
                  value={this.state.firstName}
                  onChange={this.handleOnChange}
                  className={
                    this.state.errors.firstName ? 'error' : ''
                  }
                />
                {this.state.errors.firstName 
 && <div className="errorMessage">Required 
                field</div>}
              </p>
            </div>

            <div>
              <p><strong>Last Name:</strong></p>
              <p>
                <input
                  name="lastName"
                  type="text"
                  value={this.state.lastName}
                  onChange={this.handleOnChange}
                  className={
                    this.state.errors.lastName ? 'error' : ''
                  }
                />
                {this.state.errors.lastName 
 && <div className="errorMessage">Required 
                field</div>}
              </p>
            </div>

            <div>
              <p><strong>Email:</strong></p>
              <p>
                <input 
                  name="email" 
                  type="email" 
 value={this.state.email} 
 onChange={this.handleOnChange} 
                />
              </p>
            </div>

            <div>
              <p><strong>Phone:</strong></p>
              <p>
                <input 
                  name="phone" 
                  type="tel" 
 value={this.state.phone} 
 onChange={this.handleOnChange} 
                />
              </p>
            </div>

            <p>
              <button>Save Information</button>
            </p>
          </form>
        </div>
      );
    }
  }

  export default Person;

文件:src/components/Person/Person.js

它是如何工作的...

表单对于任何 web 应用程序都是必不可少的,使用 React 处理它们很容易,可以使用本地状态,但这不是管理它们的唯一方式。如果您的表单很复杂,有多个步骤(通常用于用户注册),您可能需要在整个过程中保留值。在这种情况下,使用 Redux Form 轻松处理表单,我们将在第六章中学习,创建 Redux Form 表单。

还有更多...

在 React 中还有更多事件可以使用:

键盘事件

  • onKeyDown 当按键被按下时执行

  • onKeyPress 在释放按键后执行,但在触发 onKeyUp 之前

  • onKeyUp 在按键按下后执行

焦点事件

  • onFocus 当控件获得焦点时执行

  • onBlur 当控件失去焦点时执行

表单事件

  • onChange 当用户更改表单控件中的值时执行

  • onSubmit<form> 的一个特定属性,当按下按钮或用户在字段内按下 return 键时调用

鼠标事件

  • onClick 当鼠标按钮被按下并释放时

  • onContextMenu 当按下右键时

  • onDoubleClick 当用户执行双击时

  • onMouseDown 当鼠标按钮被按下时

  • onMouseEnter 当鼠标移动到元素或其子元素上时

  • onMouseLeave 当鼠标离开元素时

  • onMouseMove 当鼠标移动时

  • onMouseOut 当鼠标移出元素或移动到其子元素上时

  • onMouseOver 当鼠标移动到元素上时

  • onMouseUp 当鼠标按钮释放时

拖放事件

  • onDrag

  • onDragEnd

  • onDragEnter

  • onDragExit

  • onDragLeave

  • onDragOver

  • onDragStart

  • onDrop

对于拖放事件,我建议使用 react-dnd (github.com/react-dnd/react-dnd) 库。

使用 react-popup 在模态框中显示信息

模态框是显示在当前窗口上的对话框/弹出窗口,几乎适用于所有项目。在这个示例中,我们将学习如何使用 react-popup 包实现一个基本的模态框。

准备就绪

对于这个示例,您需要安装 react-popup。让我们用这个命令来做:

npm install react-popup

如何做...

使用上一个示例的代码,我们将添加一个基本的弹出窗口,以显示我们在表单中注册的人的信息:

  1. 打开你的App.jsx文件,并从react-popup中导入Popup对象。现在,我们将导入Popup.css(代码太大,无法放在这里,但你可以从该项目的代码库中复制和粘贴 CSS 演示代码:Chapter03/Recipe3/popup/src/components/Popup.css)。然后,在<Footer />之后添加<Popup />组件:
  import React from 'react';
  import Popup from 'react-popup';
  import Person from './Person/Person';
  import Header from '../shared/components/layout/Header';
  import Content from '../shared/components/layout/Content';
  import Footer from '../shared/components/layout/Footer';
  import './App.css';
  import './Popup.css';

  const App = () => (
    <div className="App">
      <Header title="Personal Information" />

      <Content>
        <Person />
      </Content>

      <Footer />

      <Popup />
    </div>
  );

 export default App;

文件:src/components/App.js

  1. 现在,在我们的Person.js文件中,我们也需要包含弹出窗口:
import React, { Component } from 'react';
import Popup from 'react-popup';
import './Person.css';
  1. 让我们修改我们的handleOnSubmit方法来实现弹出窗口。首先,我们需要验证我们至少收到了firstNamelastNameemail(电话是可选的)。如果我们得到了所有必要的信息,那么我们将创建一个弹出窗口并显示用户的信息。我喜欢react-popup的一点是它允许我们在其内容中使用 JSX 代码:
  handleOnSubmit = e => {
    e.preventDefault();

    const {
      firstName,
      lastName,
      email,
      phone
    } = this.state;

    // If firstName or lastName is missing we add an error class
    this.setState({
      errors: {
        firstName: firstName === '',
        lastName: lastName === ''
      }
    });

    // We will display the popup just if the data is received...
    if (firstName !== '' && lastName !== '' && email !== '') {
      Popup.create({
        title: 'Person Information',
        content: (
          <div>
            <p><strong>Name:</strong> {firstName} {lastName}</p>
            <p><strong>Email:</strong> {email}</p>
            {phone && <p><strong>Phone:</strong> {phone}</p>}
          </div>
        ),
        buttons: {
          right: [{
            text: 'Close',
             action: popup => popup.close() // Closes the popup                                                                                                       
          }],
        },
      });
    }
  }

它是如何工作的...

如果你做的一切正确,你应该能够看到这样的弹出窗口:

如你在代码中所见,电话是可选的,所以如果我们不包括它,我们就不会渲染它:

还有更多...

react-popup提供配置来执行一个动作。在我们的例子中,我们使用该动作来在用户按下Close按钮时关闭弹出窗口,但我们可以传递 Redux 动作来做其他事情,比如发送一些信息,甚至在我们的弹出窗口内添加表单。

实施 Airbnb React/JSX 风格指南

Airbnb React/JSX 风格指南是 React 编码中最受欢迎的风格指南。在这个教程中,我们将实现带有 Airbnb React/JSX 风格指南规则的 ESLint。

准备工作

要实施 Airbnb React/JSX 风格指南,我们需要安装一些包,比如eslinteslint-config-airbnbeslint-plugin-babeleslint-plugin-react

我不喜欢强迫任何人使用特定的 IDE,但我想推荐一些最好的编辑器来与 React 一起工作。

  • Atomatom.io

  • 在我个人看来,Atom 是与 React 一起工作的最佳 IDE。在这个教程中,我们将使用 Atom。

  • 优点

  • MIT 许可证(开源)

  • 易于安装和配置

  • 有很多插件和主题

  • 与 React 完美配合

  • 支持 Mac、Linux 和 Windows

  • 您可以使用 Nuclide 来进行 React Native 开发(nuclide.io)

  • 缺点

  • 与其他 IDE 相比速度较慢(如果你有 8GB 的 RAM,应该没问题)

  • Visual Studio Code(VSC)- code.visualstudio.com

  • VSC 是另一个用于 React 的好的 IDE。

  • 优点

  • MIT 许可证(开源)

  • 易于安装

  • 它有很多插件和主题。

  • 与 React 完美配合

  • 支持 Mac、Linux 和 Windows

  • 缺点

  • 微软(我不是微软的大粉丝)

  • 在开始时配置可能会令人困惑

  • Sublime Text -www.sublimetext.com

  • Sublime Text 是我的初恋,但我不得不承认 Atom 已经取代了它。

  • 优点

  • 易于安装

  • 有很多插件和主题

  • 支持 Mac、Linux 和 Windows

  • 缺点

  • 不是免费的(每个许可证 80 美元)。

  • 仍然不够成熟来用于 React。

  • 有些插件很难配置。

安装所有必要的包:

npm install eslint eslint-config-airbnb eslint-plugin-react eslint-plugin-jsx-a11y

有一些 Airbnb React/JSX Style Guide 的规则我宁愿不使用或者稍微改变默认值,但这取决于你是否保留它们或者移除它们。

你可以在官方网站(eslint.org/docs/rules)上检查所有的 ESLint 规则,以及在github.com/yannickcr/eslint-plugin-react/tree/master/docs/rules上检查所有特殊的 React ESLint 规则。

我宁愿不使用的规则或者我宁愿改变默认值的规则如下:

  • comma-dangle: 关闭

  • arrow-parens: 关闭

  • max-len: 120

  • no-param-reassign: 关闭

  • function-paren-newline: 关闭

  • react/require-default-props: 关闭

如何做...

为了启用我们的 ESLint,我们需要创建一个.eslintrc文件,并添加我们想要关闭的规则:

  1. 创建.eslintrc文件。你需要在根目录下创建一个名为.eslintrc的新文件:
  {
    "parser": "babel-eslint",
    "extends": "airbnb",
    "rules": {
      "arrow-parens": "off",
      "comma-dangle": "off",
      "function-paren-newline": "off",
      "max-len": [1, 120],
      "no-param-reassign": "off",
      "react/require-default-props": "off"
    }
  }
  1. 添加一个脚本来运行代码检查工具。在你的package.json文件中,你需要添加一个新的脚本来运行代码检查工具:
  {
    "name": "airbnb",
    "version": "0.1.0",
    "private": true,
    "engines": { 
      "node": ">= 10.8"  
    },
    "dependencies": {
      "eslint": "⁴.18.2",
      "eslint-config-airbnb": "¹⁶.1.0",
      "eslint-plugin-babel": "⁴.1.2",
      "eslint-plugin-react": "⁷.7.0",
      "prop-types": "¹⁵.6.1",
      "react": "¹⁶.2.0",
      "react-dom": "¹⁶.2.0",
      "react-scripts": "1.1.0"
    },
    "scripts": {
      "start": "react-scripts start",
      "build": "react-scripts build",
      "test": "react-scripts test --env=jsdom",
      "eject": "react-scripts eject",
      "lint": "eslint --ext .jsx,.js src"
    }
  }
  1. 一旦你添加了lint脚本,你可以用这个命令运行代码检查工具验证:
 npm run lint
  1. 现在你可以看到你项目中的代码检查工具错误:

  1. 现在我们需要修复代码检查工具的错误。第一个错误是 Component 应该被写成一个纯函数react/prefer-stateless-function。这意味着我们的App组件可以被写成一个函数组件,因为我们不使用任何本地状态:
  import React from 'react';
  import Person from './Person/Person';
  import Header from '../shared/components/layout/Header';
  import Content from '../shared/components/layout/Content';
  import Footer from '../shared/components/layout/Footer';
  import './App.css';

  const App = () => (
    <div className="App">
      <Header title="Personal Information" />

      <Content>
        <Person />
      </Content>

      <Footer />
    </div>
  );

  export default App;

文件:src/components/App.js

  1. 接下来,我们有这个错误:不允许在扩展名为'.js'的文件中使用 JSX /react/jsx-filename-extension。这个错误意味着在我们使用 JSX 代码的文件中,我们需要使用.jsx扩展名,而不是.js。我们有六个文件出现了这个问题(App.jsPerson.jsindex.jsContent.jsFooter.jsHeader.js)。我们只需要重命名这些文件并将扩展名改为.jsxApp.jsxPerson.jsxContent.jsxFooter.jsxHeader.jsx)。由于react-scripts,我们暂时不会将我们的index.js改为index.jsx。否则,我们会得到这样的错误:

在第十章*,精通 Webpack 4.x*中,我们将能够将所有的 JSX 文件重命名为.jsx扩展名。

  1. 我们需要抑制 linter 错误。我们必须在我们的index.js文件顶部写下这个注释:
/* eslint react/jsx-filename-extension: "off" */
import React from 'react';
...
  1. 让我们来看看这个错误:在这个开括号后面期望有一个换行符/object-curly-newline,以及这个错误:在这个闭括号前面期望有一个换行符/object-curly-newline。在我们的Person.jsx文件中,在handleOnChange方法中有这个对象:
  const { firstName, lastName, email, phone } = this.state;
  1. 规则说我们需要在对象之前和之后添加一个换行符:
    const {
      firstName,
      lastName,
      email,
      phone
    } = this.state;
  1. 现在让我们看看警告:意外的控制台语句/no-console。console.log 在我们的 linter 中生成了一个警告,这不会影响我们,但如果你需要有一个控制台并且想要避免警告,你可以通过 ESLint 注释添加一个异常,就像这样:
console.log('Data:', data); // eslint-disable-line no-console 
  1. 更多的 ESLint 注释可以做同样的事情:
 // eslint-disable-next-line no-console
    console.log('Data:', data);
  1. 如果你想在整个文件中禁用控制台,那么在文件开头你可以这样做:
/* eslint no-console: "off" */
import React, { Component } from 'react';
...
  1. 错误:'document'未定义/no-undef。在我们的index.jsx中使用全局对象 document 时,有两种方法可以修复这个错误。第一种方法是添加一个特殊的注释来指定 document 对象是一个全局变量:
/* global document */
import React from 'react';
import ReactDOM from 'react-dom';
...
  1. 我不喜欢这种方式。我更喜欢在我们的.eslintrc文件中添加一个globals节点:
{
  "parser": "babel-eslint",
  "extends": "airbnb",
 "globals": {
 "document": "true"
 },
  "rules": {
    "arrow-parens": "off",
    "comma-dangle": "off",
    "function-paren-newline": "off",
    "max-len": [1, 120],
    "no-param-reassign": "off",
    "react/require-default-props": "off"
  }
}

它是如何工作的...

linter 验证对于任何项目都是必不可少的。有时,这是一个讨论的话题,因为大多数开发人员不喜欢遵循标准,但一旦每个人都熟悉了这个样式指南,一切都会更加舒适,你将会交付更高质量的代码。

到目前为止,我们知道如何在终端中运行 linter 验证,但你也可以将 ESLint 验证器添加到你的 IDE(Atom 和 VSC)。在这个例子中,我们将使用 Atom。

安装 Atom 插件

在 Atom(Mac 上)中,你可以转到首选项|+安装,然后你可以找到 Atom 插件。我会给你一个我用来改进我的 IDE 并提高我的生产力的插件列表:

  • linter-eslint:使用 ESLint 实时 lint JS

  • editorconfig:帮助开发人员在不同的编辑器之间保持一致的编码风格

  • language-babel:支持 React 语法

  • minimap:全源代码的预览

  • pigments:在项目和文件中显示颜色的包

  • sort-lines:对你的行进行排序

  • teletype:与团队成员共享你的工作区,并允许他们实时协作编码

安装了这些包之后,如果你打开一个有 lint 错误的文件,你将能够看到它们:

配置 EditorConfig

当我们团队中的人使用不同的编辑器时,EditorConfig 也非常有用,可以帮助维护一致的编码风格。EditorConfig 得到了许多编辑器的支持。你可以在官方网站editorconfig.org上检查你的编辑器是否得到支持。

我使用的配置是这样的;你需要在你的目录下创建一个名为.editorconfig的文件:

 root = true

 [*]
 indent_style = space
 indent_size = 2
 end_of_line = lf
 charset = utf-8
 trim_trailing_whitespace = true
 insert_final_newline = true

 [*.html]
 indent_size = 4

 [*.css]
 indent_size = 4

 [*.md]
 trim_trailing_whitespace = false

你可以影响所有的文件[],也可以使用[.extension]来影响特定的文件**.**

还有更多...

在我们的 IDE 中运行 linter 验证或者通过终端运行是不够的,不能确保我们将 100%验证我们的代码,并且不会向我们的 Git 存储库中注入任何 linter 错误。确保我们将经过验证的代码发送到我们的 Git 存储库的最有效方法是使用 Git hooks。这意味着你在执行提交之前(pre-commit)或推送之前(pre-push)运行 linter 验证器。我更喜欢在 pre-commit 上运行 linter,而在 pre-push 上运行单元测试(我们将在第十二章中介绍单元测试).

Husky 是我们将用来修改 Git hooks 的包;你可以使用以下命令安装它:

 npm install husky

一旦我们添加了这个包,我们需要修改我们的package.json并添加新的脚本:

{
  "name": "airbnb",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "eslint": "⁴.18.2",
    "eslint-config-airbnb": "¹⁶.1.0",
    "eslint-plugin-babel": "⁴.1.2",
    "eslint-plugin-jsx-a11y": "⁶.0.3",
    "eslint-plugin-react": "⁷.7.0",
    "husky": "⁰.14.3",
    "prop-types": "¹⁵.6.1",
    "react": "¹⁶.2.0",
    "react-dom": "¹⁶.2.0",
    "react-scripts": "1.1.0"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject",
    "lint": "eslint --ext .jsx,.js src",
    "precommit": "npm run lint",
    "postmerge": "npm install", 
    "postrewrite": "npm install",
  }
}

我们使用四个脚本:

  • precommit:在执行提交之前运行。

  • postmerge:在执行合并后运行。

  • postrewrite:这个 hook 是由重写提交的命令调用的(git commit --amendgit-rebase;目前,git-filter-branch不会调用它!)。

  • *prepush:我目前没有添加这个 Git 钩子,但这对于运行我们的单元测试("prepush": "npm test")非常有用,我们将在第十二章中添加这个 Git 钩子,测试和调试,当我们涵盖单元测试主题时。

在这种情况下,在我们的precommit中,我们将运行我们的 linter 验证器,如果验证器失败,提交将不会执行,直到您修复所有 linter 错误。postmerge 和 postrewrite 钩子帮助我们同步我们的 npm 包,因此,例如,如果用户 A 添加了新的 npm 包,然后用户 B 拉取新代码,将自动运行npm install命令在用户 B 的本地机器上安装新包。

使用 React Helmet 更新我们的标题和元标记

在所有项目中,能够更改我们的站点标题和每个特定页面上的元标记以使其对 SEO 友好非常重要。

准备工作

对于这个示例,我们需要安装一个名为react-helmet的包:

npm install react-helmet

如何做...

React Helmet 是处理标题和元标记以改善我们网站 SEO 的最佳方式:

  1. 一旦我们使用App.jsx的相同组件安装了react-helmet包,我们需要导入 React Helmet:
 import Helmet from 'react-helmet';
  1. 我们可以通过将标题属性添加到Helmet组件来更改页面的标题,就像这样:
      <Helmet title="Person Information" />
  1. 如果您启动您的应用程序,您将在浏览器中看到标题:

  1. 如果您想更改您的元标记,您可以这样做:
    <Helmet
      title="Person Information"
      meta={[
        { name: 'title', content: 'Person Information' },
        { name: 'description', content: 'This recipe talks about React 
 Helmet' }
      ]}
    />

它是如何工作的...

有了那段代码,我们将得到这个输出:

如果您想直接将 HTML 代码添加到Helmet组件中,也可以这样做:

    <Helmet>
      <title>Person Information</title>
      <meta name="title" content="Person Information" />
      <meta name="description" content="This recipe talks about React Helmet" />
    </Helmet>

您可能已经注意到在页面第一次加载时标题会闪烁变化,这是因为在我们的index.html文件中,默认情况下有标题React App。您可以通过编辑此文件来更改它:

  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, 
    shrink-to-fit=no">
    <meta name="theme-color" content="#000000">
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json">
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
    <title>Personal Information</title>
  </head>

文件:public/index.html

还有更多...

到目前为止,我们只在主组件(<App />)中更改了我们的标题,但在第四章*,使用 React Router V4 为我们的应用程序添加路由*,我们将能够根据路由在不同组件中更改我们的标题和元标记。

此外,在第十一章中,实现服务器端渲染,我们将学习如何在应用程序中实现服务器端渲染。您也可以在服务器端渲染中使用 React Helmet,但需要进行一些更改。

首先,在您的index.html文件中(注意:此文件将在第十一章*,实现服务器端渲染*中更改为 JavaScript 文件;请不要尝试将此内容添加到您当前的index.html文件中),您需要添加类似以下内容:

  return  `
    <head>
      <meta charset="utf-8">
      <title>Personal Information</title>
      ${helmet.title.toString()}
      ${helmet.meta.toString()}
      <link rel="shortcut icon" href="images/favicon.png" 
      type="image/x-icon">
    </head>
  `;

有了这个,我们就能够使用服务器端渲染来更新我们的标题和元标签。

第四章:使用 React Router 在我们的应用程序中添加路由

在本章中,将涵盖以下示例:

  • 实现 React Router v4

  • 创建嵌套路由并向我们的路径添加参数

介绍

在本章中,我们将学习如何使用 React Router v4 在我们的项目中添加动态路由。

实现 React Router v4

与 Angular 不同,React 是一个库而不是一个框架,这意味着特定功能,例如路由或propTypes,不是 React 核心的一部分。相反,路由由一个名为 React Router 的第三方库处理。

准备工作

我们将使用我们在实施 Airbnb React/JSX 样式指南Repository: Chapter03/Recipe4/airbnb)中的代码来启用 linter 验证。

我们需要做的第一件事是安装 React Router v4,我们可以使用以下命令来完成:

 npm install react-router-dom

您可能会对我们为什么安装react-router-dom而不是react-router感到困惑。React Router 包含了react-router-domreact-router-native的所有常见组件。这意味着如果您在 Web 上使用 React,应该使用react-router-dom,如果您在使用 React Native,则需要使用react-router-nativereact-router-dom包最初是为了包含版本 4,而react-router使用的是版本 3。react-router-domreact-router有一些改进。它们在这里列出:

  • 改进的<Link>组件(渲染为<a>)。

  • 包括<BrowserRouter>,它与浏览器的window.history交互。

  • 包括<NavLink>,它是一个<Link>包装器,知道它是否处于活动状态。

  • 包括<HashRouter>,它使用 URL 中的哈希来渲染组件。如果您有一个静态页面,应该使用这个组件而不是<BrowserRouter>

如何做...

在这个示例中,我们将根据路由显示一些组件:

  1. 我们需要创建四个功能组件(AboutContactHomeError 404)并将它们命名为它们目录中的index.jsx

  2. 创建Home组件:

import React from 'react';

const Home = () => (
  <div className="Home">
    <h1>Home</h1>
  </div>
);

export default Home;

文件:src/components/Home/index.jsx

  1. 创建About组件:
import React from 'react';

const About = () => (
  <div className="About">
    <h1>About</h1>
  </div>
);

export default About;

文件:src/components/About/index.jsx

  1. 创建Contact组件:
      import React from 'react';

      const Contact = () => (
        <div className="Contact">
          <h1>Contact</h1>
        </div>
      );

      export default Contact;

文件:src/components/Contact/index.jsx

  1. 创建Error 404组件:
      import React from 'react';

      const Error404 = () => (
        <div className="Error404">
          <h1>Error404</h1>
        </div>
      );

 export default Error404;

文件:src/components/Error/404.jsx

  1. 在我们的src/index.js文件中,我们需要包含我们将在下一步创建的路由。我们需要从react-router-dom中导入BrowserRouter对象,并将其重命名为 Router:
import React from 'react';
import { render } from 'react-dom';
import { BrowserRouter as Router } from 'react-router-dom';
import './index.css';

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

render(
  <Router>
    <AppRoutes />
  </Router>,
  document.getElementById('root')
);

文件:src/index.js

  1. 现在我们需要创建src/routes.jsx文件,我们将在其中导入我们的AppHome组件,并使用Route组件为用户访问根路径(/)时添加一个路由来执行我们的Home组件:
// Dependencies
import React from 'react';
import { Route } from 'react-router-dom';

// Components
import App from './components/App';
import Home from './components/Home';

const AppRoutes = () => (
  <App>
    <Route path="/" component={Home} />
  </App>
);

export default AppRoutes;

文件:src/routes.jsx

  1. 之后,我们需要修改我们的App.jsx文件,将路由组件渲染为子组件:
      import React from 'react';
      import { element } from 'prop-types';
      import Header from '../shared/components/layout/Header';
      import Content from '../shared/components/layout/Content';
      import Footer from '../shared/components/layout/Footer';
      import './App.css';

      const App = props => (
        <div className="App">
          <Header title="Routing" />

          <Content>
            {props.children}
          </Content>

          <Footer />
        </div>
      );

      App.propTypes = {
        children: element
      };

 export default App;

文件:src/components/App.jsx

  1. 如果你运行你的应用程序,你会在根路径(/)看到Home组件:

  1. 现在,让我们在用户尝试访问任何其他路由时添加我们的Error 404
// Dependencies
import React from 'react';
import { Route } from 'react-router-dom';

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

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

export default AppRoutes;

文件:src/routes.jsx

  1. 如果你运行应用程序,你会看到它同时渲染了两个组件(HomeError 404)。你可能想知道为什么:

  1. 这是因为我们需要使用<Switch>组件来执行路径匹配时的一个组件。为此,我们需要导入Switch组件,并将其作为路由的包装器添加进去:
// Dependencies
import React from 'react';
import { Route, Switch } from 'react-router-dom';

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

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

export default AppRoutes;

文件:src/routes.jsx

  1. 现在,如果我们去到根路径(/),我们会看到我们的Home组件,而Error404不会同时执行(只会执行Home组件),但如果我们去到/somefakeurl,我们会看到Home组件也被执行了,这是一个问题:

  1. 为了解决这个问题,我们需要在我们想要精确匹配的路由中添加 exact 属性。问题在于/somefakeurl会匹配我们的根路径(/),但如果我们想要非常具体地匹配路径,我们需要在Home路由中添加 exact 属性:
// Dependencies
import React from 'react';
import { Route, Switch } from 'react-router-dom';

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

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

export default AppRoutes;
  1. 现在如果你去到/somefakeurl,你将能够看到Error404组件:

它是如何工作的...

正如你所看到的,实现 React Router 库非常容易。现在我们可以为我们的About/about)和Contact/contact)组件添加更多的路由:

// Dependencies
import React from 'react';
import { Route, Switch } from 'react-router-dom';

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

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

export default AppRoutes;

如果你去到/about,你会看到这个视图:

如果你去到/contact,你会看到这个视图:

还有更多...

到目前为止,我们已经学会了如何在我们的项目中创建简单的路由,但在下一个教程中,我们将学习如何在我们的路由中包含参数,如何添加嵌套路由,以及如何使用<Link>组件在我们的网站中导航。

向我们的路由添加参数

对于这个教程,我们将使用与上一个教程相同的代码,并添加一些参数,展示如何将它们传递到我们的组件中。

如何做...

在这个教程中,我们将创建一个简单的Notes组件,以在访问/notes路由时显示所有的笔记,但当用户访问/notes/:noteId时,我们将显示一个笔记(我们将使用noteId来过滤笔记):

  1. 我们需要创建一个名为 Notes 的新组件(src/components/Notes/index.jsx),这是我们的Notes组件的骨架:
    import React, { Component } from 'react';
    import './Notes.css';
    class Notes extends Component {
      constructor() {
        super();

        // For now we are going to add our notes to our 
        // local state, but normally this should come
        // from some service.
        this.state = {
          notes: [
            {
              id: 1,
              title: 'My note 1'
            },
            {
              id: 2,
              title: 'My note 2'
            },
            {
              id: 3,
              title: 'My note 3'
            },
          ]
        };
      }
      render() {
        return (
          <div className="Notes">
            <h1>Notes</h1>
          </div>
        );
      }
    }
    export default Notes;

文件:src/components/Notes/index.jsx

  1. CSS 文件如下:
    .Notes ul {
      list-style: none;
      margin: 0;
      margin-bottom: 20px;
      padding: 0;
    }

 .Notes ul li {
      padding: 10px;
    }

    .Notes a {
      color: #555;
      text-decoration: none;
    }

    .Notes a:hover {
      color: #ccc;
      text-decoration: none;
    }

文件:src/components/Notes/Notes.css

  1. 一旦我们创建了我们的Notes组件,我们需要将它导入到我们的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 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="/contact" component={Contact} exact />
      <Route path="/notes" component={Notes} exact />
      <Route component={Error404} />
    </Switch>
  </App>
);

export default AppRoutes;

文件:src/routes.jsx

  1. 现在我们可以在/notesURL 下看到我们的 Notes 组件:

  1. 现在我们的Notes组件已经连接到 React Router,让我们将我们的笔记渲染为列表:
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import './Notes.css';

class Notes extends Component {
  constructor() {
    super();

    this.state = {
      notes: [
        {
          id: 1,
          title: 'My note 1'
        },
        {
          id: 2,
          title: 'My note 2'
        },
        {
          id: 3,
          title: 'My note 3'
        },
      ]
    };
  }

  renderNotes = notes => (
    <ul>
      {notes.map((note, key) => (
        <li key={key}>
          <Link to={`/notes/${note.id}`}>{note.title}</Link>
        </li>
      ))}
    </ul>
  );

  render() {
    const { notes } = this.state;

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

        {this.renderNotes(notes)}
      </div>
    );
  }
}

export default Notes;

文件:src/components/Notes/index.jsx

  1. 你可能已经注意到我们正在使用<Link>(这将生成一个<a>标签)组件,指向/notes/notes.id,这是因为我们将在我们的src/routes.jsx文件中添加一个新的嵌套路由来匹配笔记的id
// Dependencies
import React from 'react';
import { Route, Switch } from 'react-router-dom';

// Components
import App from './components/App';
import About from './components/About';
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="/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;

文件:src/routes.jsx

  1. React Router 有一个特殊的属性叫做match,它是一个包含有关我们执行的路由的所有信息的对象,如果我们有参数,我们将能够在match对象中看到它们,就像这样:
render() {
  // Let's see what contains our props object.
  console.log(this.props); 

  // We got the noteId param from match object.
  const { match: { params: { noteId } } } = this.props;
  const { notes } = this.state;

  // By default our selectedNote is false
  let selectedNote = false;

  if (noteId > 0) {
    // If the note id is higher than 0 then we filter it from our 
    // notes array.
    selectedNote = notes.filter(
      note => note.id === Number(noteId)
    );
  }

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

      {/* We render our selectedNote or all notes */}
      {this.renderNotes(selectedNote || notes)}
    </div>
  );
}

文件:src/components/Notes/index.jsx

  1. match属性看起来像这样。

它是如何工作的...

match对象包含了许多有用的信息。React Router 还包括了对象的历史和位置。正如你所看到的,我们可以在match对象中获取我们在路由中传递的所有参数。

如果你运行应用程序并转到/notesURL,你会看到这个视图:

如果你点击任何链接(我点击了我的笔记 2),你会看到这个视图:

之后,我们可以在我们的Header组件中添加一个菜单来访问所有我们的路由:

import React from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import logo from '../../images/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="/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;

文件:src/shared/components/layout/Header.jsx

之后,我们需要修改我们的src/components/App.css文件来为我们的菜单添加样式。只需在 CSS 文件的末尾添加以下代码:

.App-header ul {
  margin: 0;
  padding: 0;
  list-style: none;
}

.App-header ul li {
  display: inline-block;
  padding: 0 10px;
}

.App-header ul li a {
  color: #fff;
  text-decoration: none;
}

.App-header ul li a:hover {
  color: #ccc;
}

文件:src/components/App.css

现在你可以看到菜单像这样:

第五章:精通 Redux

在本章中,将介绍以下配方:

  • 创建 Redux 存储

  • 创建操作创建者和分派操作

  • 使用 Redux 实现 Firebase

介绍

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

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

Redux 建议将 Redux 状态处理为不可变的。然而,JavaScript 中的对象和数组并非如此,这可能会导致我们错误地直接改变状态*。*

这些是 Redux 的三个原则:

  • **单一数据源:**整个应用程序的状态存储在单个存储中的对象树中。

  • **状态是只读的:**更改状态的唯一方法是发出操作,描述发生了什么的对象。

  • **使用纯函数进行更改:**为了指定状态树如何被操作转换,您编写纯减速器。

此信息摘自 Redux 的官方网站。要了解更多,请访问redux.js.org/introduction/three-principles

什么是操作?

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

什么是不可变性?

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

这些是 JavaScript 中的不可变类型:

  • 数字

  • 字符串

  • 布尔值

  • 未定义

这些是 JavaScript 中的可变类型:

  • 数组

  • 函数

  • 对象

为什么要不可变性?

  • 更清晰:我们知道谁改变了状态(reducer)

  • 更好的性能

  • **易于调试:**我们可以使用 Redux DevTools(我们将在第十二章中介绍该主题,测试和调试

我们可以通过以下方式使用不可变性:

  • ES6:**

  • Object.assign

  • Spread操作符(...)

  • 库:**

  • Immutable.js

  • Lodash(合并和扩展)

什么是 reducer?

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

在技术术语中,reducer 是一个纯函数,它接收两个参数(当前状态和动作),并根据动作返回一个新的不可变状态。

组件类型

容器:

  • 专注于工作原理

  • 连接到 Redux

  • 分派 Redux 动作

  • react-redux生成

展示性:

  • 专注于外观

  • 未连接到 Redux

  • 通过 props 接收数据或函数

  • 大多数时间是无状态的

Redux 流程

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

创建 Redux 存储

存储保存应用程序的整个状态,更改内部状态的唯一方法是分派动作。存储不是一个类;它只是一个带有一些方法的对象。

存储方法如下:

  • getState()**: **返回应用程序的当前状态

  • dispatch(action): 分派一个动作,是触发状态改变的唯一方式

  • subscribe(listener): 添加一个变更监听器,每次分派一个动作时都会调用它

  • replaceReducer(nextReducer): 替换当前由存储使用的 reducer 来计算状态

准备工作

使用 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
    );
  }

文件: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>

文件:public/index.html

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

文件:src/shared/reducers/deviceReducer.js

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

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

const rootReducer = combineReducers({
  device
});

export default rootReducer;

文件: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 模拟器,您将看到这个视图:

什么是 mapStateToProps?

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

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 Store 中的状态更新。在这个示例中,我们将使用它们的公共 API 显示www.coinmarketcap.com上列出的前 100 种加密货币。

准备工作

对于这个示例,我们需要安装 Axios(一个基于承诺的浏览器和 Node.js 的 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';

文件:src/actions/actionTypes.js

  1. 也许您想知道为什么我们需要创建一个与字符串相同名称的常量。这是因为在使用常量时,我们不能有重复的常量名称(如果我们错误地重复一个常量名称,我们将会收到错误)。另一个原因是动作在两个文件中使用,在实际的动作文件中以及在我们的减速器中。为了避免重复字符串,我决定创建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
});

文件:src/shared/redux/baseActions.js

  1. 在我们构建了baseActions.js文件之后,我们需要为我们的 actions 创建另一个文件,这应该在src/actions/coinsActions.js内。对于这个示例,我们将使用CoinMarketCap的公共 API(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));
    });
};

文件:src/actions/coinsActions.js

  1. 一旦我们的 actions 文件准备好了,我们需要创建我们的 reducer 文件来根据我们的 actions 更新我们的 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;
  }
};

文件:src/reducers/coinsReducer.js

  1. 然后我们需要将我们的 reducer 添加到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;

文件:src/shared/reducers/index.js

  1. 如您所见,我包含了getNewState工具;这是一个执行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;
}

文件:src/shared/utils/frontend.js

  1. 现在我们需要在src/components/Coins/index.js创建一个Container组件。在介绍中,我提到了有两种类型的组件:containerpresentational。容器必须连接到 Redux,并且不应该有任何 JSX 代码,只有我们的mapStateToPropsmapDispatchToProps,然后在导出时,我们可以传递我们要渲染的presentational组件,将 actions 的值和我们的 Redux 状态作为 props 传递。要创建我们的mapDispatchToProps函数,我们需要使用 Redux 库中的bindActionCreators方法。这将把我们的dispatch方法绑定到我们传递的所有 actions 上。有不使用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);

文件: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;

文件: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;
}

文件:src/components/Coins/Coins.css

  1. 在我们的src/shared/redux/configureStore.js文件中,我们需要导入redux-thunk并使用applyMiddleware方法在 Redux Store 中使用这个库:
// 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)
  );
}

文件:src/shared/redux/configureStore.js

  1. 让我们在Header组件中添加到/coins的链接:
import React from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import logo from '../../images/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;

文件:src/shared/components/layout/Header.jsx

  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;

文件:src/routes.jsx

它是如何工作的...

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

我们将获得一个包含前 100 个硬币的对象数组coinmarketcap.com。如果您正确地按照所有步骤操作,您将能够看到这个视图:

使用 Redux 实现 Firebase

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

您可以主要用 Firebase 来进行实时应用程序,但如果您愿意,您也可以将其用作非实时应用程序的常规数据库。Firebase 支持许多语言(如 JavaScript、Java、Python 和 Go)和平台,如 Android、iOS 和 Web。

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

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

准备就绪

一旦您在 Firebase 上注册,您需要通过在 Firebase 控制台中点击“添加项目”来创建一个新项目:

我将我的项目命名为codejobs;当然,您可以根据自己的喜好命名它:

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

现在您必须选择“将 Firebase 添加到您的 Web 应用程序”选项,并且您将获得有关您的应用程序的信息:

不要将这些信息分享给任何人。我与您分享这些信息是因为我想向您展示如何将您的应用程序连接到 Firebase。

现在转到仪表板中的 Develop | Database,然后单击“创建数据库”按钮:

之后,选择“启动”选项以锁定模式,并单击“启用”按钮:

然后,在页面顶部,选择下拉菜单并选择“实时数据库”选项:

一旦我们创建了实时数据库,让我们导入一些数据。要做到这一点,您可以在下拉菜单中选择“导入 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"
      }
    ]
  }

文件:src/data/phrases.json

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

正如您在红色警告中所看到的,此位置的所有数据将被覆盖。这意味着如果您在数据库中有任何旧数据,它将被替换,因此在将新数据导入数据库时要小心。

如果您一切都做对了,您应该看到导入的数据如下:

现在我们需要修改我们的权限以便能够在我们的数据库中读取和写入。如果您转到规则选项卡,您将看到类似于这样的内容:

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

最后,我们已经完成了 Firebase 上所有需要的步骤。现在让我们在 React 中创建 Firebase 应用程序。我们将重用CoinMarketCap的最后一个配方(存储库:Chapter05/Recipe2/coinmarketcap)。我们需要做的第一件事是安装 firebase 依赖项:

    npm install firebase      

如何做...

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

  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'
    }
  };

文件: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);

文件: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;

文件: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';

文件: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
        };
      });
  };

文件:src/actions/phrasesActions.js 在 Firebase 中,我们不使用常规 ID。相反,Firebase 使用键值作为 ID。导入的数据就像一个基本数组,带有键 0、1、2、3、4 等,因此对于该数据,每个键都被用作 ID。但是当我们通过 Firebase 创建数据时,键将成为具有随机代码的唯一字符串值,例如-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;
    }
  };

文件: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);

文件: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;

文件: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;
  }

文件: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,我们将看到实时更新数据而无需刷新页面:

现在,如果您看到一个X链接来删除短语,让我们删除第一个(Marcus Tullius Cicero)。如果您在另一个标签页中打开 Firebase 页面,您将看到数据正在实时删除:

此外,如果您添加新行(使用文本区域和输入),您将实时看到反映出来:

正如我之前提到的,当我们从 React 应用程序中添加新数据时,Firebase 将为新数据生成唯一键,而不是导入 JSON。在这种情况下,为新添加的短语生成了-LJSYCHLHEe9QWiAiak4键。

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

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