二、征服组件和 JSX

本章将介绍以下配方:

  • 创建我们的第一个 React 组件
  • 组织我们的 React 应用
  • 使用 CSS 类和内联样式设置组件的样式
  • 将道具传递给组件并使用道具类型验证它们
  • 在组件中使用本地状态
  • 制作功能性或无状态组件
  • 了解 React 生命周期方法
  • 理解纯组分
  • 防止 React 中的 XSS 漏洞

介绍

本章包含与如何在 React 中创建组件相关的配方。我们将学习如何创建 React 组件(类组件、纯组件和功能组件)并组织我们的项目结构。我们还将学习如何使用 React local state,实现所有 React 生命周期方法,最后,我们将了解如何防止 XSS 漏洞。

创建我们的第一个 React 组件

该组分是 React 的重要组成部分。使用 React,您可以构建交互式和可重用的组件。在此配方中,您将创建第一个 React 组件。

准备

首先,我们需要使用create-react-app创建 React 应用。完成后,可以继续创建第一个 React 组件。

在安装create-react-app之前,请记住您需要从www.nodejs.org下载并安装 Node。您可以在 Mac、Linux 和 Windows 上安装它。

通过在终端中键入以下命令,全局安装create-react-app

 npm install -g create-react-app

或者您可以使用快捷方式:

 npm i -g create-react-app

怎么做。。。

让我们按照以下步骤构建第一个 React 应用:

  1. 使用以下命令创建 React 应用:
 create-react-app my-first-react-app
  1. 使用cd my-first-react-app进入新应用,并使用npm start启动。
  2. 应用现在应该在http://localhost:3000处运行。
  3. 在您的src文件夹中创建一个名为Home.js的新文件:
import React, { Component } from 'react';

class Home extends Component {
  render() {
    return <h1>I'm Home Component</h1>;
  }
}

export default Home;

File: src/Home.js

  1. 您可能已经注意到,我们将在文件末尾导出类组件,但是直接在类声明中导出它是可以的,如下所示:
 import React, { Component } from 'react';
 export default class Home extends Component {
      render() {
        return <h1>I'm Home Component</h1>;
      }
    }

File: src/Home.js I prefer to export it at the end of the file, but some people like to do it in this way, so it depends on your preferences.

  1. 现在我们已经创建了第一个组件,我们需要渲染它。所以我们需要打开App.js文件,导入Home组件,然后将其添加到App组件的渲染方法中。如果我们第一次打开此文件,可能会看到如下代码:
 import React, { Component } from 'react';
 import logo from './logo.svg';
 import './App.css';
 class App extends Component {
      render() {
        return (
          <div className="App">
            <header className="App-header">
              <img src={logo} className="App-logo" alt="logo" />
              <h1 className="App-title">Welcome to React</h1>
            </header>
            <p className="App-intro">
              To get started, edit <code>src/App.js</code> 
              and save to reload.
            </p>
          </div>
        );
      }
    }

 export default App;

File: src/App.js

  1. 让我们稍微修改一下代码。如前所述,我们需要导入Home组件,然后将其添加到 JSX 中。我们还需要用我们的组件替换<p>元素,如下所示:
 import React, { Component } from 'react';
 import logo from './logo.svg';

 // We import our Home component here...
 import Home from './Home';
 import './App.css';

 class App extends Component {
      render() {
        return (
          <div className="App">
            <header className="App-header">
              <img src={logo} className="App-logo" alt="logo" />
              <h1 className="App-title">Welcome to React</h1>
            </header>
            {/* Here we add our Home component to be render it */}
            <Home />
          </div>
        );
      }
    }

 export default App;

File: src/App.js

它是如何工作的。。。

如您所见,我们从 React 库中导入了ReactComponent。您可能注意到我们没有直接使用React对象。要在 JSX 中编写代码,需要导入React。JSX 类似于 HTML,但有一些区别。在下面的食谱中,您将了解更多关于 JSX 的信息。

该组件称为class组件(React.Component,有不同的类型:纯组件(React.PureComponent)和功能组件,也称为无状态组件,我们将在下面的配方中介绍。

如果运行该应用,您应该会看到如下内容:

还有更多。。。

在我们的示例中,我们创建了Home.js文件,组件的名称为Home

All React component names should start with the first letter capitalized in both the file and the class name. To begin with, it might feel uncomfortable for you to see this, but this is the best practice in React.

JSX 和 HTML 之间的一些主要区别是属性名。您可能已经注意到,我们使用的是className而不是class。这是唯一的特殊属性名称。其他由破折号分隔的两个单词需要转换为 camelCase,例如,onClicksrcSettabIndexaria-*data-*属性仍然使用相同的命名法(data-somethingaria-label

组织我们的 React 应用

在本食谱中,我们将学习如何以更好的方式构建我们的项目。

怎么做。。。

我们可以使用create-react-app提供的默认结构创建 React 组件,但在这个配方中,我将向您展示一种更好的方法来组织项目,以便我们在应用增长时做好准备。

  1. 我们需要创建一个新的 React 应用(如果您尚未创建 React 应用,请检查上一个配方)

  2. 目前,我们的 React 应用目录树如下所示:

  1. 我们需要创建src/componentssrc/shared目录
  2. 在此之后,我们需要为我们的组件创建src/components/Home目录,并将Home.js移动到此文件夹中
  3. App.js文件保持在src/components级别
  4. 此外,App.cssApp.test.js将保持在src/components水平
  5. logo.svg文件移动到src/shared/images
  6. 我们的index.js将保持在src/水平
  7. 现在,您的目录树应该如下所示:

I highly recommend that you create another directory for shared components, src/shared/components.I'll explain more about this in the next recipes.

  1. App.js文件中,更改logoHome导入:
 import logo from '../sharimg/logo.svg';
 import Home from './Home/Home';

File: src/components/App.js

  1. 在您更改后,我们需要打开index.js并修复App组件的导入路径:
import App from './components/App';

File: src/index.js

它是如何工作的。。。

这种新的结构将给我们更大的灵活性,以灵活地组合我们的 React 组件。有了这个新的结构,我们将能够创建子组件,如果我们需要的话,这在使用 React 开发复杂应用时非常重要。

在接下来的食谱中,我们将看到如何在应用中共享组件。

使用 CSS 类和内联样式设置组件的样式

在上一个配方中,我们学习了如何创建类组件。现在,让我们向Home组件添加一些 CSS。

在 React 中,最佳实践之一是将样式文件与组件放在同一目录中。如果您使用过 PHP、Node 或任何其他服务器语言,您可能会在一个style.css文件中编写样式,并使用link标记将其包含在模板中。React 使用 Webpack,这是目前最流行的模块绑定器。使用 Webpack,我们可以配置处理样式的方式(直接使用 CSS 或使用 CSS 预处理器,如 Sass、Stylus 或更少的 CSS),使用 Webpack,我们可以实现 CSS 模块。这是避免 CSS 的三个主要问题的强大方法:

  • 不再有冲突(无意中的 CSS 覆盖)
  • 显式依赖项(每个组件的样式)
  • 没有全局范围

第 10 章中,掌握 Webpack 4.x,我们将涵盖 Webpack,并且我们将能够在我们的项目中使用 Sass 或手写笔实现 CSS 模块。

怎么做。。。

我们现在将着手向Home组件添加 CSS:

  1. 创建新应用,或使用上一个应用(my-first-react-app

  2. 然后为我们的Home组件创建一个新的 CSS 文件。让我们重用上一个配方中创建的Home组件。现在您需要创建一个与您的Home.js文件相同级别的Home.css文件(在components文件夹中)。在创建此文件之前,让我们稍微修改一下Home组件:

 import React, { Component } from 'react';

 // We import our Home.css file here
 import './Home.css';

 class Home extends Component {
      render() {
        return (
          <div className="Home">
            <h1>Welcome to Codejobs</h1>

            <p>
              In this recipe you will learn how to add styles to 
              components. If you want to learn more you can visit 
              our Youtube Channel at 
              <a href="http://youtube.com/codejobs">Codejobs</a>.
            </p>
          </div>
        );
      }
    }

 export default Home;

File: src/components/Home/Home.js

  1. 现在我们将在Home.css中添加样式。基本上,我们将组件包装成一个带有className主页的div,内部有一个带有文本Welcome to Codejobs<h1>标签,然后是一个带有消息的<p>标签。我们需要直接导入我们的Home.css文件,然后我们的 CSS 文件将如下所示:
  .Home {
      margin: 0 auto;
      width: 960px;
  }

  .Home h1 {
      font-size: 32px;
      color: #333;
  }

  .Home p {
      color: #333;
      text-align: center;
  }

  .Home a {
      color: #56D5FA;
      text-decoration: none;
  }

  .Home a:hover {
      color: #333;
  }

File: src/components/Home/Home.css

  1. 现在让我们假设您需要添加一个内联样式。我们使用 style 属性来执行此操作,CSS 属性需要以 camelCase 和{{ }}之间的格式编写,如下所示:
 import React, { Component } from 'react';

 // We import our Home.css file here
 import './Home.css';

 class Home extends Component {
    render() {
      return (
        <div className="Home">
          <h1>Welcome to Codejobs</h1>
          <p>
            In this recipe you will learn how to add styles to 
            components. If you want to learn more you can visit 
            our Youtube Channel at 
            <a href="http://youtube.com/codejobs">Codejobs</a>.
          </p>

          <p>
            <button 
 style={{ 
 backgroundColor: 'gray', 
 border: '1px solid black' 
 }}            >
              Click me!
            </button>
          </p>
        </div>
      );
    }
  }

 export default Home;

File: src/components/Home/Home.js

  1. 您还可以向style属性传递对象,如下所示:
 import React, { Component } from 'react';

 // We import our Home.css file here
 import './Home.css';

 class Home extends Component {
    render() {
      // Style object...
      const buttonStyle = {
        backgroundColor: 'gray',
        border: '1px solid black'
      };

      return (
        <div className="Home">
          <h1>Welcome to Codejobs</h1>
          <p>
            In this recipe you will learn how to add styles to 
            components. If you want to learn more you can visit 
            our Youtube Channel at 
            <a href="http://youtube.com/codejobs">Codejobs</a>.
          </p>
          <p>
            <button style={buttonStyle}>Click me!</button>
          </p>
        </div>
      );
    }
  }

 export default Home;

File: src/components/Home/Home.js

它是如何工作的。。。

正如您所见,将 CSS 文件连接到我们的组件非常简单,如果您正确地遵循了所有步骤,您的站点应该如下所示:

还有更多。。。

您可能对如何将 CSS 代码添加到浏览器感到好奇,因为我们没有直接将 CSS 文件导入到项目中(例如,通过使用<link>标记)。好的,您会惊讶地看到 CSS 代码被注入到我们的<head>标记中,每个导入的样式表都使用<style>标记。如果您使用 Chrome DevTools 检查项目,您将看到如下内容:

这种行为是因为style-loader是一个 Webpack 加载器,当我们使用create-react-app创建它时,它在我们的应用中默认使用:

当我们使用create-react-app时,无法直接修改 Webpack 配置,因为它使用的是一个名为react-scripts的包,但在第 10 章中,掌握 Webpack时,我们将看到如何配置我们的 Webpack 而不使用启动工具包,如create-react-app

有更多的 Webpack 加载器可以做不同的事情,例如css-loader用于 CSS 模块,sass-loader用于实现 Sass,stylus-loader用于实现手写笔,extract-text-plugin用于将 CSS 代码移动到.css文件,而不是将其注入 DOM(通常,这仅用于生产)。

将道具传递给组件并使用道具类型验证它们

到目前为止,您已经熟悉了 React 组件,但它不仅仅是呈现静态 HTML。像任何应用一样,我们需要能够将信息(通过道具)发送到不同的元素。在这个配方中,我们将创建新的组件:HeaderContentFooter(我们将这些组件分组到一个名为layout的文件夹中),我们将发送一些道具(作为属性和子项)并用PropTypes进行验证。

怎么做。。。

以我们之前创建的 React 应用为例,让我们首先创建Header组件

  1. 此时,我们当前的标题位于App.js
 import React, { Component } from 'react';
 import logo from '../sharimg/logo.svg';
 import Home from './Home/Home';
 import './App.css';

 class App extends Component {
    render() {
      return (
        <div className="App">
          <header className="App-header">
 <img src={logo} className="App-logo" alt="logo" />
 <h1 className="App-title">Welcome to React</h1>
 </header>

          <Home />
        </div>
      );
    }
  }

 export default App;

File: src/components/App.js

  1. 让我们将该标题移动到新的Header组件,然后将其导入App组件。因为布局组件是全局的或共享的,所以我们需要在共享组件目录(src/shared/components/layout中)中创建一个布局目录。
  2. 在继续之前,必须安装名为prop-types的软件包以使用PropTypes验证:
npm install prop-types
  1. PropTypes最初作为 React 核心模块的一部分发布,通常与 React 组件一起使用。PropTypes用于记录传递给组件的预期属性类型。React 将根据这些定义检查传递给组件的道具,如果它们不匹配,它将在开发过程中发出警告:
 import React, { Component } from 'react';
 import PropTypes from 'prop-types';
 import logo from '../img/logo.svg';

 class Header extends Component {
      // Here you can define your PropTypes.
      static propTypes = {
        title: PropTypes.string.isRequired,
        url: PropTypes.string
      };

      render() {
        const { 
 title = 'Welcome to React', 
 url = 'http://localhost:3000' 
        } = this.props;

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

 export default Header;

File: src/shared/components/layout/Header.js

  1. staticPropTypes 属性基本上是一个需要定义要传递的道具类型的对象。arrayboolfuncnumberobjectstringsymbol是基本类型,但也有特殊类型,如nodeelementinstanceOfoneOfoneOfTypearrayOfobjectOfshape and any。有一个名为isRequired的可选属性,如果必须使用道具,则可以将其添加到任何类型,如果未定义道具,则会生成 React 警告。
  2. 导入并呈现我们的Header组件:
 import React, { Component } from 'react';
 import Home from './Home/Home';
 import Header from '../shared/components/layout/Header';
 import './App.css';

 class App extends Component {
      render() {
        return (
          <div className="App">
            <Header title="Welcome to Codejobs" />
            <Home />
          </div>
        );
      }
    }

 export default App;

File: src/components/App.js Don't get confused with the <Header/> component, it is not the same as the <header> tag from HTML5, that's why in React is recommended to use capital letters in the class names.

  1. 传递给我们组件的所有属性都包含在此道具中。您可能已经注意到,我们只发送title道具,因为它是唯一需要的道具。url道具是可选的,并且在解构(http://localhost:3000中也有一个默认值。如果我们没有通过标题道具,即使我们有一个默认值欢迎在解构中做出反应,我们也会得到如下警告:

  1. 创建我们的Footer组件:
 import React, { Component } from 'react';

 class Footer extends Component {
      render() {
        return (
       <footer>&copy; Codejobs {(new Date()).getFullYear()}</footer>
        );
      }
    }

 export default Footer;

File: src/shared/components/layout/Footer.js

  1. 到目前为止,我们只将道具作为属性传递(使用自关闭组件<Component />),但还有另一种方式将道具作为子级传递(<Component>Children Content</Component>。让我们创建一个Content组件,并将我们的Home组件作为内容的子级发送:
 import React, { Component } from 'react';
 import PropTypes from 'prop-types';

 class Content extends Component {
    static propTypes = {
      children: PropTypes.element.isRequired
    };

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

      return (
        <main>
          {children}
        </main>
      );
    }
  }

 export default Content;

File: src/shared/components/layout/Content.js

  1. 通过这些更改,我们的App.js文件现在应该如下所示:
 import React, { Component } from 'react';
 import Home from './Home/Home';

  // Layout Components
 import Header from '../shared/components/layout/Header';
 import Content from '../shared/components/layout/Content';
 import Footer from '../shared/components/layout/Footer';

 import './App.css';

 class App extends Component {
    render() {
      return (
        <div className="App">
          <Header title="Welcome to Codejobs" />

          <Content>
            <Home />
          </Content>

          <Footer />
        </div>
      );
    }
  }

 export default App;

File: src/components/App.js

它是如何工作的。。。

PropTypes 验证对于开发人员来说非常重要,因为它们迫使我们定义我们将在组件中接收哪种类型的 prop,并验证其中的一些是否是必需的。

如果您正确地遵循了所有步骤,您将看到如下内容:

还有更多。。。

如您所见,有许多方法可以向组件发送道具。接收道具的方法还有很多,比如使用 Redux(通过容器)或 React 路由,但这些都是我们将在下一章中介绍的主题。

在组件中使用本地状态

局部状态是 React 创建动态组件的基本功能。局部状态仅在类组件上可用,每个组件管理其状态。您可以在组件的构造函数上定义状态的初始值,当您更新状态的值时,组件将重新呈现自身。

本地状态有助于切换,用于处理表单,并用于管理同一组件中的信息。如果需要在不同组件之间共享数据,则不建议使用本地状态。在那个场景中,我们需要实现 Redux 状态,我们将在Chapter5掌握 Redux中介绍。

怎么做。。。

让我们定义初始状态。让我们看看当本地状态更新时组件的render方法是如何工作的:

**1. 使用Home组件,我们将添加一个构造函数并定义初始状态:

 import React, { Component } from 'react';
 import './Home.css';

 class Home extends Component {
    constructor() {
 // We need to define super() at the beginning of the 
 // constructor to have access to 'this'
      super();

      // Here we initialize our local state as an object
      this.state = {
        name: 'Carlos'
      };
    }

    render() {
      return (
        <div className="Home">
          {/* Here we render our state name */}
          <p>Hi my name is {this.state.name}</p>
        </div>
      );
    }
  }

 export default Home;

File: src/components/Home/Home.js

  1. 在本例中,我们将构造函数中的局部状态定义为对象,并在渲染中直接打印值。我们在构造函数的开头使用了super()。这是用来调用父构造函数(React.Component),如果不包含,会出现如下错误:

  1. 添加super()后,需要将初始状态定义为常规对象:
  this.state = {
    name: 'Carlos'
  };
  1. 正在用this.setState()更新我们的本地状态,这只是一个没有更新的状态。这意味着该组件将不再重新渲染。要更新状态,我们需要使用this.setState()方法并传递状态的新值。我们可以在 1 秒(1000 毫秒)后添加一个setTimeout来更新名称状态,所以我们需要修改render方法如下:
 render() {
    setTimeout(() => {
      this.setState({
        name: 'Cristina' // Here we update the value of the state
      });
    }, 1000);

    console.log('Name:', this.state.name);

    return (
      <div className="Home">
        <p>Hi my name is {this.state.name}</p>
      </div>
    );
  }
  1. 如果在浏览器中运行此命令,您将看到状态的第一个值是 Carlos,在这之后 1 秒它将更改为 Cristina。我已经添加了一个console.log来记录州名称的值。如果打开浏览器控制台,您将看到:

  1. componentDidMount生命周期方法中更新我们的本地状态:您可能想知道为什么会重复这么多次。它很简单;这就是 React 的工作方式。每次我们更新一个状态,就会触发方法 render,在这段代码中,我们添加了一个setTimeout,它会在一秒钟后更新状态。这意味着每秒都在调用render方法,导致不定式循环。这将影响应用的性能,这就是为什么在更新状态时需要小心的原因。正如您所看到的,在渲染方法中更新它不是一个好主意。那么,我应该在哪里更新状态?好的,这取决于您的应用,但现在,我将向您展示一种方法,它是 React 生命周期的一部分,名为componentDidMount()
 import React, { Component } from 'react';
 import './Home.css';

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

      this.state = {
        name: 'Carlos'
      };
    }

    componentDidMount() {
      setTimeout(() => {
        this.setState({
          name: 'Cristina'
        });
      }, 1000);
    }

    render() {
      console.log('Name:', this.state.name);

      return (
        <div className="Home">
          <p>Hi my name is {this.state.name}</p>
        </div>
      );
    }
  }

 export default Home;

File: src/components/Home/Home.js

  1. 如果运行此代码并看到控制台,现在将看到:

它是如何工作的。。。

使用componentDidMount,我们避免了无限循环。这是一种更好的方法的原因是,当组件已经装入时,componentDidMount只执行一次,在该方法中,我们只执行setTimeout并更新名称状态一次。在下面的菜谱中,我们将学习更多关于 React 生命周期方法的知识。

还有更多。。。

Local state 也用于处理表单,但我们将在第 6 章中介绍表单,使用 Redux 表单创建表单。

制作功能性或无状态组件

到目前为止,我们只学习了如何在 React 中创建类组件。当您需要处理本地状态时,这些组件非常有用,但在某些情况下,我们需要呈现静态标记。对于静态组件,我们需要使用功能组件也称为无状态组件这将提高我们应用的性能。

向组件传递道具并使用 PropTypes配方对其进行验证的过程中,我们创建了一些布局组件(HeaderContentFooter。正如您所想象的,这些组件通常不是动态的(除非您希望在标题中有一个切换菜单或一些用户信息),因此在这种情况下,我们可以将它们转换为功能组件。

怎么做。。。

现在是将我们的Header组件转换为功能组件的时候了:

  1. 首先,让我们看看当前的Header组件是什么样子的:
 import React, { Component } from 'react';
 import PropTypes from 'prop-types';
 import logo from '../img/logo.svg';

 class Header extends Component {
    static propTypes = {
      title: PropTypes.string.isRequired,
      url: PropTypes.string
    };

    render() {
      const { 
        title = 'Welcome to React', 
        url = 'http://localhost:3000' 
      } = this.props;

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

 export default Header;

File: src/shared/components/layout/Header.js

  1. 首先要做的是将我们的类组件转换成一个 arrow 函数,通过这个更改,我们不再需要导入React.Component。迁移的第二部分是将道具作为参数传递到函数中,而不是从this.props获取道具,最后一步是将静态propTypes作为函数的节点进行移动。在这些更改之后,我们的代码应该如下所示:
 import React from 'react';
 import PropTypes from 'prop-types';
 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>
      </header>
    );
  };

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

 export default Header;

File: src/shared/components/layout/Header.js A functional component is an equivalent to just having the render method. That's why we only need to return the JSX directly.

  1. 迁移Header组件后,我们将迁移Footer组件;这更容易,因为它没有道具。首先,让我们看看我们的Footer组件是什么样子的:
 import React, { Component } from 'react';

 class Footer extends Component {
    render() {
      return (
        <footer>
          &copy; Codejobs {(new Date()).getFullYear()}
 </footer>
      );
    }
  }

 export default Footer;

File: src/shared/components/layout/Footer.js

  1. 现在,作为一个功能组件,它应该如下所示:
 import React from 'react';

  // Since we don't have props, we can directly return our JSX.
 const Footer = () => (
    <footer>&copy; Codejobs {(new Date()).getFullYear()}</footer>
  );

 export default Footer;

File: src/shared/components/layout/Footer.js In this case, as you can see, we need to create an arrow function without parameters (because we don't have any props) and directly return the JSX we need to render.

  1. Content组件转换为功能组件:
 import React, { Component } from 'react';
 import PropTypes from 'prop-types';

 class Content extends Component {
    static propTypes = {
      children: PropTypes.element.isRequired
    };

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

      return (
        <main>
          {children}
        </main>
      );
    }
  }

 export default Content;

File: src/shared/components/layout/Content.js

  1. 此组件与我们的Header组件类似。我们需要将道具作为参数传递,并保持我们的propTypes
 import React from 'react';
 import PropTypes from 'prop-types';

 const Content = props => { 
    const { children } = props;

    return ( 
      <main>
        {children} 
      </main>
    ); 
  };

  Content.propTypes = {
    children: PropTypes.element.isRequired
  };

 export default Content;

File: src/shared/components/layout/Content.js

它是如何工作的。。。

即使使用功能组件,我们也可以验证我们的PropTypes。请记住,如果不需要任何动态数据或本地状态,则应该考虑使用无状态的组件。这将提高应用的性能。

还有更多。。。

功能组件不仅没有状态,而且也没有反应生命周期方法

了解 React 生命周期方法

React 提供了在组件生命周期中处理数据的方法。当我们需要在特定时间更新应用时,这非常有用。

怎么做。。。

在本节中,我们将独立地解释每个示例。

待办事项列表–实现组件 willmount

在本食谱中,您将了解 React 中的生命周期方法。我们将看到信息是如何通过这些方法流动的,因为组件是预安装、安装和卸载的。我们将在此配方中开发的待办事项列表如下所示:

  1. 对于这个待办事项列表,我们需要在我们的components目录中创建一个名为Todo的新文件夹,您还需要创建名为Todo.jsTodo.css的文件。这是Todo组件的骨架:
import React, { Component } from 'react';
import './Todo.css';

class Todo extends Component {
  constructor() {
    super();
  }

  componentWillMount() {

  }

  render() {
    return (
      <div className="Todo">
        <h1>New Task:</h1>
      </div>
    );
  }
}

export default Todo;

File: src/components/Todo/Todo.js

  1. 构造函数r:构造函数是在对象初始化之前执行的唯一方法,构造函数可以使用super关键字调用超类(父类)的构造函数。此方法用于初始化我们的本地状态或绑定我们的方法。对于 Todo 列表,我们需要使用任务和items数组中的一些值初始化构造函数中的本地状态:
constructor() {
  super();

  // Initial state...
  this.state = {
    task: '',
    items: []
  };
}
  1. componentWillMount方法在组件安装前执行一次。在这种情况下,在安装组件之前,我们需要使用默认任务更新items状态:
componentWillMount() {
  // Setting default tasks...
  this.setState({
    items: [
      {
        id: uuidv4(),
        task: 'Pay the rent',
        completed: false
      },
      {
        id: uuidv4(),
        task: 'Go to the gym',
        completed: false
      },
      {
        id: uuidv4(),
        task: 'Do my homework',
        completed: false
      }
    ]
  });
}
  1. 我们正在使用uuidv4生成随机 ID。要安装此软件包,您需要运行以下命令:
 npm install uuid
  1. 然后你需要像这样导入它:
 import uuidv4 from 'uuid/v4';
  1. 定义默认任务后,让我们看看如何呈现待办事项列表:
 render() {
    return (
      <div className="Todo">
        <h1>New Task:</h1>

        <form onSubmit={this.handleOnSubmit}>
          <input 
            value={this.state.task} 
            onChange={this.handleOnChange} 
          />
        </form>

        <List
          items={this.state.items}
          markAsCompleted={this.markAsCompleted}
          removeTask={this.removeTask}
        />
      </div>
    );
  }
  1. 我们的 JSX 分为两部分。第一个是一个表单,其输入连接到我们的本地状态(this.state.task),我们将在用户提交表单(onSubmit时保存任务。第二部分是组件列表,我们将在其中显示 Todo 列表(或任务列表),传递项数组和markAsCompleted(将任务标记为已完成)和removeTask(将任务从列表中删除)功能。
  2. handleOnChange方法用于连接我们的输入值和我们的状态任务:
 handleOnChange = e => {
    const { target: { value } } = e;

    // Updating our task state with the input value...
    this.setState({
      task: value
    });
  }
  1. handleOnSubmit方法是更新items状态,将新任务推送到阵列:
 handleOnSubmit = e => {
    // Prevent default to avoid the actual form submit...
    e.preventDefault();

 // Once is submited we reset the task value and we push 
 // the new task to the items array.
    if (this.state.task.trim() !== '') {
      this.setState({
        task: '',
        items: [
          ...this.state.items,
          {
            id: uuidv4(),
            task: this.state.task,
            complete: false
          }
        ]
      });
    }
  }
  1. markAsCompleted函数将从我们的List组件调用,需要接收我们要标记为已完成的任务的id。这样,我们可以在 items 数组中找到特定任务,将节点修改为 completed,然后更新本地状态:
 markAsCompleted = id => {
    // Finding the task by id...
    const foundTask = this.state.items.find(
      task => task.id === id
    );

    // Updating the completed status...
    foundTask.completed = true;

    // Updating the state with the new updated task...
    this.setState({
      items: [
        ...this.state.items,
        ...foundTask
      ]
    });
  }
  1. removeTask函数也是从List组件调用的,像markAsCompleted一样,我们需要接收id来移除具体任务:
 removeTask = id => {
    // Filtering the tasks by removing the specific task id...
    const filteredTasks = this.state.items.filter(
      task => task.id !== id
    );

    // Updating items state...
    this.setState({
      items: filteredTasks
    });
  }
  1. 让我们把所有的部分放在一起。我们的Todo组件应该如下所示:
 import React, { Component } from 'react';
 import uuidv4 from 'uuid/v4';
 import List from './List';
 import './Todo.css';

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

      // Initial state...
      this.state = {
        task: '',
        items: []
      };
    }

    componentWillMount() {
      // Setting default tasks...
      this.setState({
        items: [
          {
            id: uuidv4(),
            task: 'Pay the rent',
            completed: false
          },
          {
            id: uuidv4(),
            task: 'Go to the gym',
            completed: false
          },
          {
            id: uuidv4(),
            task: 'Do my homework',
            completed: false
          }
        ]
      });
    }

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

      // Updating our task state with the input value...
      this.setState({
        task: value
      });
    }

    handleOnSubmit = e => {
      // Prevent default to avoid the actual form submit...
      e.preventDefault();

      // Once is submitted we reset the task value and 
      // we push the new task to the items array.
      if (this.state.task.trim() !== '') {
        this.setState({
          task: '',
          items: [
            ...this.state.items,
            {
              id: uuidv4(),
              task: this.state.task,
              complete: false
            }
          ]
        });
      }
    }

    markAsCompleted = id => {
      // Finding the task by id...
      const foundTask = this.state.items.find(
        task => task.id === id
      );

      // Updating the completed status...
      foundTask.completed = true;

      // Updating the state with the new updated task...
      this.setState({
        items: [
          ...this.state.items,
          ...foundTask
        ]
      });
    }

    removeTask = id => {
      // Filtering the tasks by removing the specific task id...
      const filteredTasks=this.state.items.filter(
        task => task.id !== id
      );

      // Updating items state...
      this.setState({
        items: filteredTasks
      });
    }

    render() {
      return (
        <div className="Todo">
          <h1>New Task:</h1>

          <form onSubmit={this.handleOnSubmit}>
            <input 
              value={this.state.task} 
 onChange={this.handleOnChange}
            />
          </form>

          <List
            items={this.state.items}
            markAsCompleted={this.markAsCompleted}
            removeTask={this.removeTask}
          />
        </div>
      );
    }
  }

 export default Todo;

File: src/components/Todo/Todo.js

  1. 现在我们已经完成了Todo组件,让我们看看List组件是什么样子的:
 import React from 'react';

 const List = props => (
    <ul>
      {props.items.map((item, key) => (
        <li 
 key={key} 
          className={`${item.completed ? 'completed' : 'pending'}`}                        
        >
          {/* 
            * If the task is completed we assign the * .completed class otherwise .pending 
            */}
          {item.task}

          <div className="actions">
            {/* 
              * Using a callback on the onClick we call our 
              * markAsCompleted function 
              */}
            <span 
              className={item.completed ? 'hide' : 'done'} 
 onClick={() => props.markAsCompleted(item.id)}
            >
              <i className="fa fa-check"></i>
            </span>

            {/* 
              * Using a callback on the onClick we call 
              * our removeTask function 
              */}
            <span 
              className="trash" 
 onClick={() => props.removeTask(item.id)}
            >
              <i className="fa fa-trash"></i>
            </span>
          </div>
        </li>
      ))}
    </ul>
  );

 export default List;

File: src/components/Todo/List.js

  1. 每次我们使用.map函数渲染一个数组中的多个 React 元素时,我们必须将关键道具添加到我们创建的每个项目中。否则,我们将收到如下 React 警告:

  1. 您可能已经注意到,我们还包括一些字体可怕的图标,为了使其正常工作,我们需要将字体可怕的 CDN 添加到主index.html文件中:
  <head>
    <title>React App</title>
    <link 
 href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" 
 rel="stylesheet" 
    />
  </head>

File: public/index.html

  1. 最后一部分是 Todo 列表的 CSS(如果愿意,您可以自由更改样式):
 .Todo {
     background-color: #f5f5f5;
     border-radius: 4px;
     border: 1px solid #e3e3e3;
     box-shadow: inset 0 1px 1px rgba(0,0,0,.05);
     margin: 50px auto;
     min-height: 20px;
     padding: 20px;
     text-align: left;
     width: 70%;
 }

 .Todo ul {
     margin: 20px 0px;
     padding: 0;
     list-style: none;
 }

 .Todo ul li {
     background-color: #fff;
     border: 1px solid #ddd;
     display: flex;
     justify-content: space-between;
     margin-bottom: -1px;
     padding: 10px 15px;
 }

 .Todo ul li .hide {
     visibility: hidden;
 }

 .Todo ul li.completed {
     background-color: #dff0d8;
 }

 .Todo ul li .actions {
     display: flex;
     justify-content: space-between;
     width: 40px;
 }

 .Todo ul li span {
     cursor: pointer;
 }

 .Todo ul li .done {
     color: #79c41d;
     display: block;
 }

 .Todo ul li .trash {
     color: #c41d1d;
     display: block;
 }

 .Todo form input {
     background-color: #fff;
     border-radius: 4px;
     border: 1px solid #ccc;
     box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
     color: #555;
     font-size: 14px;
     height: 34px;
     line-height: 34px;
     padding: 6px 12px;
     width: 40%;
 }

File: src/components/Todo/Todo.css

  1. 不要忘记将Todo组件导入您的App组件。否则,组件将不会渲染:
  import React, { Component } from 'react';
  import Todo from './Todo/Todo';
  import Header from '../shared/components/layout/Header';
  import Content from '../shared/components/layout/Content';
  import Footer from '../shared/components/layout/Footer';
  import './App.css';

  class App extends Component {
    render() {
      return (
        <div className="App">
          <Header title="Todo List" />

          <Content>
            <Todo />
          </Content>

          <Footer />
        </div>
      );
    }
  }

 export default App;

File: src/components/App.js

  1. 如果您正确地遵循了所有说明,您将看到如下的待办事项列表:

  2. 默认任务的初始状态:

  • 添加新任务:

  • 写下任务标题,然后按键进入

  • 将任务标记为已完成:

  • 删除任务:

I challenge you to save the tasks using localStorage instead of defining the default tasks with componentWillMount.

Pomotoro 定时器–实现构造函数和组件安装

为了理解componentDidMount,我们将创建一个 Pomodoro 计时器(如果您不知道它是什么,可以阅读以下内容:https://en.wikipedia.org/wiki/Pomodoro_Technique

我们的 Pomodoro 定时器将如下所示:

*

创建我们的 Pomodoro 计时器:

  1. 我们需要做的第一件事是在我们的components目录中创建一个名为Pomodoro的新文件夹,以及一个名为Timer.js的文件和 CSS 文件Timer.css。这是我们将用于此组件的类组件的框架:
import React, { Component } from 'react';
import './Timer.css';

class Timer extends Component {
  constructor() {
    super();
  }

  componentDidMount() {

  }

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

      </div>
    );
  }
}

export default Timer;

File: src/components/Pomodoro/Timer.js

  1. 对于 Pomotoro 计时器,我们需要在构造函数中使用一些时间值和警报值初始化本地状态(当时间结束时):
 constructor() {
    super();

    // Initial State
    this.state = {
      alert: {
        type: '',
        message: ''
      },
      time: 0
    };

    // Defined times for work, short break and long break...
    this.times = {
      defaultTime: 1500, // 25 min
      shortBreak: 300, // 5 min
      longBreak: 900 // 15 min
    };
  }
  1. 安装组件后会调用componentDidMount方法,并且只执行一次。在这种情况下,一旦安装了我们的组件,我们需要用默认时间(25 分钟)更新我们的时间状态,为此,我们需要创建一个名为setDefaultTime的新方法,然后在componentDidMount方法中执行它:
  componentDidMount() {
    // Set default time when the component mounts
    this.setDefaultTime();
  }

  setDefaultTime = () => {
    // Default time is 25 min
    this.setState({
      time: this.times.defaultTime
    });
  }
  1. 在我们将默认时间定义为时间状态之后,让我们看看需要如何渲染 Pomodoro 计时器。我们的render方法应该是这样的:
 render() {
    const { alert: { message, type }, time } = this.state;

    return (
      <div className="Pomodoro">
        <div className={`alert ${type}`}>
          {message}
        </div>

        <div className="timer">
          {this.displayTimer(time)}
        </div>

        <div className="types">
          <button 
            className="start" 
 onClick={this.setTimeForWork}
          >
            Start Working
          </button>
          <button 
            className="short" 
 onClick={this.setTimeForShortBreak}
          >
            Short Break
          </button>
          <button 
            className="long" 
 onClick={this.setTimeForLongBreak}
          >
            Long Break
          </button>
        </div>
      </div>
    );
  }
  1. 在本例中,我们的 JSX 非常简单。我们从本地状态(messagetypetime获取值,并在用户收到警报消息时显示 div 以显示我们的警报。我们有另一个 div 来显示计时器,这里我们将当前时间(以秒表示)传递给displayTimer方法,该方法将这些秒转换为mm:ss格式。布局的最后一部分是选择计时器类型的按钮(开始工作 25 分钟、短休息 5 分钟或长休息 15 分钟),您可能已经注意到,我们对每种类型的计时器在onClick事件上执行不同的方法。 setTimeForWorksetTimeForShortBreaksetTimeForLongBreak这三个功能的目的是根据计时器的类型更新警报消息,然后调用一个名为setTime的通用函数,将每个选项的具体时间作为参数传递。让我们先看看这三个函数应该是什么样子:***
  setTimeForWork = () => {
    this.setState({
      alert: {
        type: 'work',
        message: 'Working!'
      }
    });

    return this.setTime(this.times.defaultTime);
  }

  setTimeForShortBreak = () => {
    this.setState({
      alert: {
        type: 'shortBreak',
        message: 'Taking a Short Break!'
      }
    });

    return this.setTime(this.times.shortBreak);
  }

  setTimeForLongBreak = () => {
    this.setState({
      alert: {
        type: 'longBreak',
        message: 'Taking a Long Break!'
      }
    });

    return this.setTime(this.times.longBreak);
  }
  1. 正如我们在前面的方法中所学到的,当我们在类中使用箭头函数指定方法时,它们会自动绑定(它们可以访问“this”对象)。这意味着我们不需要在构造函数上绑定它们。现在我们来创建我们的setTime方法:
  setTime = newTime => {
    this.restartInterval();

    this.setState({
      time: newTime
    });
  }
  1. 如您所见,我们执行了一个名为restartInterval()的新方法,并使用newTime变量更新了本地状态,我们将其作为参数传递(它可以是 1500 秒=25 分钟、300 秒=5 分钟或 900 秒=15 分钟)。您可能注意到,从函数的名称来看,我们将使用一个setInterval函数,它用于每隔 X 毫秒调用一个函数。我们的restartInterval功能应该是这样的:
  restartInterval = () => {
    // Clearing the interval
    clearInterval(this.interval);

 // Execute countDown function every second
    this.interval = setInterval(this.countDown, 1000);
  }
  1. 在本例中,我们首先用clearInterval(this.interval)清除了间隔。这是因为用户可以在不同类型的计时器之间切换,所以每次设置新的计时器时需要清除间隔。清除间隔后,我们使用setInterval每秒调用countDown函数。countDown功能如下:
  countDown = () => {
    // If the time reach 0 then we display Buzzzz! alert.
    if (this.state.time === 0) {
      this.setState({
        alert: {
          type: 'buz',
          message: 'Buzzzzzzzz!'
        }
      });
    } else {
      // We decrease the time second by second
      this.setState({
        time: this.state.time - 1
      });
    }
  }
  1. 此拼图的最后一块是displayTimer函数,它将时间转换为mm:ss格式,并显示在我们的组件中:
 displayTimer(seconds) {
    // Formatting the time into mm:ss
    const m = Math.floor(seconds % 3600 / 60);
    const s = Math.floor(seconds % 3600 % 60);

 return `${m < 10 ? '0' : ''}${m}:${s < 10 ? '0' : ''}${s}`;
 }
  1. 让我们把它们放在一起:
 import React, { Component } from 'react';
 import './Timer.css';

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

      // Initial State
      this.state = {
        alert: {
          type: '',
          message: ''
        },
        time: 0
      };

      // Defined times for work, short break and long break...
      this.times = {
        defaultTime: 1500, // 25 min
        shortBreak: 300, // 5 min
        longBreak: 900 // 15 min
      };
    }

    componentDidMount() {
      // Set default time when the component mounts
      this.setDefaultTime();
    }

    setDefaultTime = () => {
      // Default time is 25 min
      this.setState({
        time: this.times.defaultTime
      });
    }

    setTime = newTime => {
      this.restartInterval();

      this.setState({
        time: newTime
      });
    }

    restartInterval = () => {
      // Clearing the interval
      clearInterval(this.interval);

      // Execute countDown every second
      this.interval = setInterval(this.countDown, 1000);
    }

    countDown = () => {
      // If the time reach 0 then we display Buzzzz! alert.
      if (this.state.time === 0) {
        this.setState({
          alert: {
            type: 'buz',
            message: 'Buzzzzzzzz!'
          }
        });
      } else {
        // We decrease the time second by second
        this.setState({
          time: this.state.time - 1
        });
      }
    }

    setTimeForWork = () => {
      this.setState({
        alert: {
          type: 'work',
          message: 'Working!'
        }
      });

      return this.setTime(this.times.defaultTime);
    }

    setTimeForShortBreak = () => {
      this.setState({
        alert: {
          type: 'shortBreak',
          message: 'Taking a Short Break!'
        }
      });

      return this.setTime(this.times.shortBreak);
    }

    setTimeForLongBreak = () => {
      this.setState({
        alert: {
          type: 'longBreak',
          message: 'Taking a Long Break!'
        }
      });

      return this.setTime(this.times.longBreak);
    }

    displayTimer(seconds) {
      // Formatting the time into mm:ss
      const m = Math.floor(seconds % 3600 / 60);
      const s = Math.floor(seconds % 3600 % 60);

      return `${m < 10 ? '0' : ''}${m}:${s < 10 ? '0' : ''}${s}`;
    }

    render() {
      const { alert: { message, type }, time } = this.state;

      return (
        <div className="Pomodoro">
          <div className={`alert ${type}`}>
            {message}
          </div>

          <div className="timer">
            {this.displayTimer(time)}
          </div>

          <div className="types">
            <button 
              className="start" 
 onClick={this.setTimeForWork}
            >
              Start Working
            </button>
            <button 
              className="short" 
 onClick={this.setTimeForShortBreak}
            >
              Short Break
            </button>
            <button 
              className="long" 
 onClick={this.setTimeForLongBreak}
            >
              Long Break
            </button>
          </div>
        </div>
      );
    }
  }

 export default Timer;

File: src/components/Pomodoro/Timer.js

  1. 完成组件后,最后一步是添加样式。这是用于 Pomodoro 计时器的 CSS。当然,如果您愿意,您可以更改它:
.Pomodoro {
    padding: 50px;
}

.Pomodoro .timer {
    font-size: 100px;
    font-weight: bold;
}

.Pomodoro .alert {
    font-size: 20px;
    padding: 50px;
    margin-bottom: 20px;
}

.Pomodoro .alert.work {
    background: #5da423;
}

.Pomodoro .alert.shortBreak {
    background: #f4ad42;
}

.Pomodoro .alert.longBreak {
    background: #2ba6cb;
}

.Pomodoro .alert.buz {
    background: #c60f13;
}

.Pomodoro button {
    background: #2ba6cb;
    border: 1px solid #1e728c;
    box-shadow: 0 1px 0 rgba(255, 255, 255, 0.5) inset;
    color: white;
    cursor: pointer;
    display: inline-block;
    font-size: 14px;
    font-weight: bold;
    line-height: 1;
    margin: 50px 10px 0px 10px;
    padding: 10px 20px 11px;
    position: relative;
    text-align: center;
    text-decoration: none;
}

.Pomodoro button.start {
    background-color: #5da423;
    border: 1px solid #396516;
}

.Pomodoro button.short {
    background-color: #f4ad42;
    border: 1px solid #dd962a;
}

File: src/components/Pomodoro/Timer.css

不要忘记将<Timer />组件导入App.js。如果您按照正确的方式操作,您应该会看到 Pomodoro 定时器的工作方式如下:

** 工作:

  • 稍作休息:

  • 长时间休息:

  • Buzzzz-时间到了!:

I challenge you to add a Play, Pause, and Reset buttons to control the timer.

加密硬币交换器–实现 shouldComponentUpdate

今天,每个人都在谈论比特币、以太坊、Ripple 和其他加密货币。让我们创建我们自己的加密硬币交换器来了解shouldComponentUpdate是如何工作的。

我们的交换机将如下所示:

  1. 我们将出售全部硬币。这意味着我们不会用小数进行交易;所有的东西都应该是整数,每个货币的成本是 10 美元。
import React, { Component } from 'react';
import './Coins.css';

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

    // Initial state...
    this.state = {
      dollars: 0
    };
  }

  shouldComponentUpdate(props, state) {
    // We only update if the dollars are multiples of 10
    return state.dollars % 10 === 0;
  }

  handleOnChange = e => {
    this.setState({
      dollars: Number(e.target.value || 0)
    });
  }

  render() {
    return (
      <div className="Coins">
        <h1>Buy Crypto Coins!</h1>

        <div className="question">
          <p>How much dollars do you have?</p>

          <p>
            <input 
              placeholder="0" 
 onChange={this.handleOnChange} 
              type="text" 
            />
          </p>
        </div>

        <div className="answer">
          <p>Crypto Coin price: $10</p>
          <p>
            You can buy <strong>{this.state.dollars / 10}</strong> 
            coins.
          </p>
        </div>
      </div>
    );
  }
}

export default Coins;

File: src/components/Coins/Coins.js

  1. 每次用户在输入中写入内容并将值转换为数字时,我们都会更新美元状态,但如果您运行此代码,您可能会注意到,当您输入 10 以下的数字时,您可以购买 0 枚硬币的信息在您写入 10、20、30、40 等之前不会改变。
  2. shouldComponentUpdate此方法是提高应用性能的最重要方法之一。每次我们更新一个本地状态时,它都会收到两个参数(props,state),当更新一个 prop 时,就会执行这个方法。返回的值必须是布尔值,这意味着如果您有意编写以下内容,您的组件将永远不会更新,因为此方法将阻止其更新:
shouldComponentUpdate(props, state) {
  return false;
}
  1. 但是,另一方面,如果您返回 true,或者即使您根本没有定义此方法,React 的默认行为始终是更新组件,在某些情况下,在呈现大量视图和处理大量定期更改的数据时,这可能会导致性能问题。

  2. 在我们的示例中,只有当用户输入的美元数是 10 的倍数时,我们才返回 true。这就是为什么在这种情况下您只能看到组件更新:

  1. 但对于不是 10 的倍数的数字来说,这是行不通的:

  1. 现在,如果我们从组件中删除shouldComponentUpdate方法,或者直接返回true值,那么每次我们写入一个数字时,组件都会更新,结果如下:

  1. 如您所见,通过shouldComponentUpdate,我们可以控制组件的更新,这大大提高了应用的性能。我们示例的最后一部分是 CSS:
  .Coins {
    background-color: #f5f5f5;
    border-radius: 4px;
    border: 1px solid #e3e3e3;
    box-shadow: inset 0 1px 1px rgba(0,0,0,.05);
    margin-bottom: 20px;
    margin: 50px auto;
    min-height: 20px;
    padding: 19px;
    text-align: left;
    width: 70%;
  }

  .Coins input {
    background-color: #fff;
    border-radius: 4px;
    border: 1px solid #ccc;
    box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
    color: #555;
    font-size: 14px;
    height: 34px;
    line-height: 34px;
    padding: 6px 12px;
    width: 120px;
  }

File: src/components/Coins/Coins.css

注意–实现组件将接收道具,组件将卸载

在本例中,我们将创建一个简单的注释列表,其中每隔 10 秒,我们将模拟从服务接收到具有新数据的更新,并且使用componentWillReceiveProps,我们将注册上次从服务器获得更新的时间:

  1. 在渲染之前调用componentWillReceiveProps方法。与shouldComponentUpdate类似,只要有新的道具传递给组件,或者状态发生变化,就会调用它。在本例中,我们需要创建假数据,但数据通常需要来自实际服务:
 export const notes1 = [
    {
      title: 'Note 1',
      content: 'Content for Note 1'
    },
    {
      title: 'Note 2',
      content: 'Content for Note 2'
    },
    {
      title: 'Note 3',
      content: 'Content for Note 3'
    }
  ];

 export const notes2 = [
    {
      title: 'Note 4',
      content: 'Content for Note 4'
    },
    {
      title: 'Note 5',
      content: 'Content for Note 5'
    },
    {
      title: 'Note 6',
      content: 'Content for Note 6'
    }
  ];

File: src/components/Notes/data.js

  1. 创建假数据后,让我们创建组件:
 import React, { Component } from 'react';
 import moment from 'moment';
 import './Notes.css';

  const formatTime = 'YYYY-MM-DD HH:mm:ss';

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

      // We save the first date when the data is 
      // rendered at the beginning
      this.state = {
        lastUpdate: moment().format(formatTime).toString()
      }
    }

    componentWillReceiveProps(nextProps) {
      // If the prop notes has changed...
      if (nextProps.notes !== this.props.notes) {
        this.setState({
          lastUpdate: moment().format(formatTime).toString()
        });
      }
    }
    render() {
      const { notes } = this.props;

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

          <ul>
            {notes.map((note, key) => (
              <li key={key}>{note.title} - {note.content}</li>
            ))}
          </ul>

          <p>Last Update: <strong>{this.state.lastUpdate}</strong>
          </p>
        </div>
      );
    }
  }

 export default Notes;

File: src/components/Notes/Notes.js

  1. 在本例中,我们使用的是moment.js库,要安装它,需要运行以下命令:
 npm install moment
  1. 现在,在我们的App.js文件中,我们将模拟在第一次渲染 10 秒后,我们将从服务接收一个新的更新,并渲染新的注释:
 import React, { Component } from 'react';
 import Notes from './Notes/Notes';
 import Header from '../shared/components/layout/Header';
 import Content from '../shared/components/layout/Content';
 import Footer from '../shared/components/layout/Footer';

 // This is our fake data...
 import { notes1, notes2 } from './Notes/data';
 import './App.css';

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

      // The first time we load the notes1...
      this.state = {
        notes: notes1
      };
    }

    componentDidMount() {
      // After 10 seconds (10000 milliseconds) we concatenate our       
      // data with notes2...
      setTimeout(() => {
        this.setState({
          notes: [...this.state.notes, ...notes2]
        });
      }, 10000);
    }

    render() {
      return (
        <div className="App">
          <Header title="Notes" />

          <Content>
            <Notes notes={this.state.notes} />
          </Content>

          <Footer />
        </div>
      );
    }
  }

 export default App;

File: src/components/App.js

  1. 最后一部分是 CSS 文件:
  .Notes {
    background-color: #f5f5f5;
    border-radius: 4px;
    border: 1px solid #e3e3e3;
    box-shadow: inset 0 1px 1px rgba(0,0,0,.05);
    margin-bottom: 20px;
    margin: 50px auto;
    min-height: 20px;
    padding: 19px;
    text-align: left;
    width: 70%;
  }

 .Notes ul {
    margin: 20px 0px;
    padding: 0;
    list-style: none;
  }

 .Notes ul li {
    background-color: #fff;
    border: 1px solid #ddd;
    display: flex;
    justify-content: space-between;
    margin-bottom: -1px;
    padding: 10px 15px;
    position: relative;
  }

File: src/components/Notes/Notes.css

  1. 如果运行该应用,您将看到如下内容:

  1. 10 秒后,您将看到:

  1. 如您所见,上次更新日期已从 2018-02-20 00:07:28 更改为 2018-02-20 00:07:38(10 秒后)。
  2. componentWillUnmount这是在组件从 DOM 中移除之前立即调用的最后一个方法。通常,用于对componentWillMount方法创建的任何 DOM 元素或计时器执行清理。让我们稍微修改一下代码,以便能够调用此方法。在我们的Notes组件中,您可以在render方法后添加此代码:
 componentWillUnmount() {
    console.log('Hasta la vista baby!');
    document.body.style = 'background: black;';
    document.getElementById('unmountMessage').style.color = 'white';
  }
  1. 我们需要修改我们的index.html文件,以手动包含一个按钮,该按钮不属于 React:
  <body>
    <div id="root"></div>

    <div id="unmountMessage">There is no mounted component!</div>

    <button 
      id="unmount" 
 style="margin:0 auto;display:block;background:red;color:white;"
    >
      Unmount
    </button>
  </body>

File: public/index.html

  1. 然后,在我们的index.js文件中,我们正在呈现<App />组件,让我们添加一些额外的代码(我们实际上需要从 DOM 中删除元素):
 import React from 'react';
 import ReactDOM from 'react-dom';
 import './index.css';
 import App from './components/App';
 import registerServiceWorker from './registerServiceWorker';

 const unmountButton = document.getElementById('unmount');

 // Is not very common to remove a Component from the DOM,           
  // but this will be just to understand how  
  // componentWillUnmount works.
 function unmount() {              
 ReactDOM.unmountComponentAtNode(
 document.getElementById('root')
    );

 document.getElementById('unmountMessage')
      .style.display = 'block';

 unmountButton.remove();
  }

  unmountButton.addEventListener('click', unmount);

  document.getElementById('unmountMessage')
    .style.display = 'none';

 ReactDOM.render(<App />, document.getElementById('root'));
  registerServiceWorker();

File: src/index.js

  1. 有了这个,我们将在页面底部有一个可怕的红色按钮,当我们点击它时,我们将卸载我们的组件。背景将变为黑色,我们将显示文本“没有安装组件!”,控制台将显示 Hasta la vista baby!:

  1. 单击按钮后,您将看到:

C3.js 图表–实现组件更新

js 是一个第三方库,通过包装构建整个图表所需的代码,可以轻松生成基于 D3 的图表。这意味着您不再需要编写任何 D3 代码:

  1. componentDidUpdate此 React 方法通常用于管理第三方 UI 元素并与本机 UI 交互。当我们使用 C3.js 等第三方库时,需要使用新数据更新 UI 库。使用 npm 安装 C3.js:
 npm install c3
  1. 安装 C3.js 后,我们需要将 C3 CSS 文件添加到我们的index.html。目前,我们可以使用他们提供的 CDN:
<!-- Add this on the <head> tag -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/c3/0.4.10/c3.min.css" rel="stylesheet" />

File: public/index.html

  1. 现在我们可以创建我们的Chart组件:
 import React, { Component } from 'react';
 import c3 from 'c3';
 import './Chart.css';

 class Chart extends Component {
    componentDidMount() {
      // When the component mounts the first time we update  
      // the chart.
      this.updateChart();
    }

    componentDidUpdate() {
      // When we receive a new prop then we update the chart again.
      this.updateChart();
    }

    updateChart() {
      c3.generate({
        bindto: '#chart',
        data: {
          columns: this.props.columns,
          type: this.props.chartType
        }
      });
    }

    render() {
      return <div id="chart" />;
    }
  }

 export default Chart;

File: src/components/Chart/Chart.js

  1. 如您所见,我们正在componentDidUpdate上执行updateChart方法,每次用户从App.js收到新道具时都会执行该方法。让我们在App.js文件中添加一些我们需要的逻辑:
 import React, { Component } from 'react';
 import Chart from './Chart/Chart';
 import Header from '../shared/components/layout/Header';
 import Content from '../shared/components/layout/Content';
 import Footer from '../shared/components/layout/Footer';
 import './App.css';

 class App extends Component {
    constructor(props) {
      super(props);

      this.state = {
        chartType: 'line'
      };

      this.columns = [
        ['BTC', 3000, 6000, 10000, 15000, 13000, 11000],
        ['ETH', 2000, 3000, 5000, 4000, 3000, 940],
        ['XRP', 100, 200, 300, 500, 400, 300],
      ];
    }

    setBarChart = () => {
      this.setState({
        chartType: 'bar'
      });
    }

    setLineChart = () => {
      this.setState({
        chartType: 'line'
      });
    }

    render() {
      return (
        <div className="App">
          <Header title="Charts" />

          <Content>
            <Chart 
 columns={this.columns} 
 chartType={this.state.chartType} 
            />

            <p>
              Chart Type
              <button onClick={this.setBarChart}>Bar</button>
              <button onClick={this.setLineChart}>Line</button>
            </p>
          </Content>

          <Footer />
        </div>
      );
    }
  }

 export default App;

File: src/components/App.js

  1. 现在,让我们为Chart组件添加一些基本样式:
 p {
    text-align: center;
  }

 button {
    background: #159fff;
    border: none;
    color: white;
    margin-left: 1em;
    padding: 0.5em 2em;
    text-transform: uppercase;
    &:hover {
      background: darken(#159fff, 5%);
    }
  }

 #chart {
    background: #fff;
    width: 90%;
    margin: 1em auto;
  }

File: src/components/Chart.css

  1. 在本例中,我们创建了一些图表来显示当前最重要的加密货币(BTC-比特币、ETH-以太坊和 XRP-Ripple)的信息。它应该是这样的:

This image gives you an idea of how the line charts look like

  1. 我们有两个按钮在图表类型(条形图或线条图)之间切换。如果我们单击工具栏,我们将看到以下图表:

This image gives you an idea of how the bar charts look like.

  1. 如果您从Chart组件中删除componentDidUpdate方法,那么当您按下按钮时,图表将不会更新。这是因为每次我们需要刷新数据时,我们都需要调用c3.generate方法,在这种情况下,React 的componentDidUpdate方法非常有用。

基本动画–实现组件更新

在本例中,我们将学习如何使用componentWillUpdate

  1. componentWillUpdate允许您在组件接收新道具或新状态之前操作组件。它通常用于动画。让我们创建一个基本动画(淡入/淡出),看看如何使用它:
 import React, { Component } from 'react';
 import './Animation.css';

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

      this.state = {
        show: false
      };
    }

    componentWillUpdate(newProps, newState) {
      if (!newState.show) {
        document.getElementById('fade').style = 'opacity: 1;';
      } else {
        document.getElementById('fade').style = 'opacity: 0;';
      }
    }

    toggleCollapse = () => {
      this.setState({
        show: !this.state.show
      });
    }

    render() {
      return (
        <div className="Animation">
          <button onClick={this.toggleCollapse}>
            {this.state.show ? 'Collapse' : 'Expand'}
          </button>

          <div 
 id="fade" 
 className={
              this.state.show ? 'transition show' : 'transition'
            }
          >
            This text will disappear
          </div>
        </div>
      );
    }
  }

 export default Animation;

File: src/components/Animation/Animation.js

  1. 如您所见,我们正在使用newState验证 show 状态,并观察它是否正确。然后我们添加opacity 0,如果为 false,则添加opacity 1。关于componentWillUpdate我想提到的一件重要事情是,你不能在这个方法中更新状态(这意味着你不能使用this.setState,因为它会导致对同一方法的另一次调用,从而创建一个无限循环。让我们添加一些样式:
.Animation {
background: red;
 }
.Animation .transition {
transition: all 3s ease 0s;
color: white;
padding-bottom: 10px;
}
.Animation .transition.show {
padding-bottom: 300px;
background: red;
}

File: src/components/Animation/Animation.css

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

  1. 单击按钮后,您将看到文本淡出的动画,红色 div 将展开,结果如下:

它是如何工作的。。。

正如您在所有这些示例中看到的,React 生命周期方法用于处理应用中的不同场景。在第 5章【掌握 Redux】中,我们将了解如何实现 Redux 以及生命周期方法如何与 Redux 状态协同工作。

理解纯组分

许多人对功能组件和纯组件之间的差异感到困惑。大多数人认为他们是一样的,但事实并非如此。当我们使用纯组分时,我们需要从 React 导入PureComponent

 import React, { PureComponent } from 'react';

如果 React 组件的渲染方法是“纯”(这意味着它渲染相同的结果,给定相同的道具和状态),则可以使用此函数来提高应用的性能。纯组件对 props 和 nextrops 对象以及 state 和 nextState 对象执行简单的比较。纯组件不包括shouldComponentUpdate(nextProps, nextState)方法,如果我们尝试添加它,我们将从 React 获得警告。

在这个配方中,我们将创建一个基本示例来了解纯组件是如何工作的。

准备

对于这个方法,我们需要安装 Chrome extension React Developer 工具,以便在应用中进行简单的调试。在第 12 章测试和调试中,我们将深入探讨这个主题。

您可以从下载 React 开发者工具 https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi

怎么做…

我们将创建一个组件,将输入的所有数字相加。我们可以从最后的一些食谱开始:

  1. 我们要做的第一件事是修改我们的App.js并包括数字组件:
    import React, { Component } from 'react';
    import Numbers from './Numbers/Numbers';

    import Header from '../shared/components/layout/Header';
    import Content from '../shared/components/layout/Content';
    import Footer from '../shared/components/layout/Footer';
    import './App.css';

    class App extends Component {
      render() {
        return (
          <div className="App">
            <Header title="Understanding Pure Components" />

            <Content>
              <Numbers />
            </Content>

            <Footer />
          </div>
        );
      }
    }

 export default App;

File: src/components/App.js

  1. 现在,我们将创建数字组件:
    // Dependencies
    import React, { Component } from 'react';

    // Components
    import Result from './Result';

    // Styles
    import './Numbers.css';

    class Numbers extends Component {
      state = {
        numbers: '', // Here we will save the input value
        results: []  // In this state we will save the results of the sums
      };

      handleNumberChange = e => {
        const { target: { value } } = e;

        // Converting the string value to array
 // "12345" => ["1", "2", "3", "4", "5"]
        const numbers = Array.from(value);

        // Summing all numbers from the array
 // ["1", "2", "3", "4", "5"] => 15
        const result = numbers.reduce((a, b) => Number(a) + Number(b), 0);

        // Updating the local state
        this.setState({
          numbers: value,
          results: [...this.state.results, result]
        });
      }

      render() {
        return (
          <div className="Numbers">
            <input
              type="number"
              value={this.state.numbers}
              onChange={this.handleNumberChange}
            />

            {/* Rendering the results array */}
            <ul>
              {this.state.results.map((result, i) => (
 <Result key={i} result={result} />
              ))}
            </ul>
          </div>
        )
      }
    }

    export default Numbers;

File: src/components/Numbers/Numbers.js

  1. 然后,让我们创建结果组件(作为类组件):
    import React, { Component } from 'react';

    class Result extends Component {
      render() {
        return <li>{this.props.result}</li>;
      }
    }

    export default Result;

File: src/components/Numbers/Result.js

  1. 最后,风格:
    .Numbers {
      padding: 30px;
    }

    .Numbers input[type=number]::-webkit-inner-spin-button,
 .Numbers input[type=number]::-webkit-outer-spin-button {
      -webkit-appearance: none;
      margin: 0;
    }

    .Numbers input {
      width: 500px;
      height: 60px;
      font-size: 20px;
      outline: none;
      border: 1px solid #ccc;
      padding: 10px;
    }

    .Numbers ul {
      margin: 0 auto;
      padding: 0;
      list-style: none;
      width: 522px;
    }

    .Numbers ul li {
      border-top: 1px solid #ccc;
      border-left: 1px solid #ccc;
      border-right: 1px solid #ccc;
      padding: 10px;
    }

    .Numbers ul li:first-child {
      border-top: none;
    }

    .Numbers ul li:last-child {
      border-bottom: 1px solid #ccc;
    }

File: src/components/Numbers/Numbers.css

它是如何工作的…

如果运行应用,您将看到:

如您所见,我们使用的是带有数字类型的输入,这意味着只有在您开始写数字(1、2、3 等等)时,我们才会接受数字,您将看到每行的总和结果(0+1=1、1+2=3、3+3=6

可能这看起来很简单,但是如果让我们使用 React Developer 工具检查应用,我们需要启用 Highlight Updates 选项。

在此之后,开始在输入中写入多个数字(快速),您将看到所有 React 正在执行的渲染。

如您所见,React 正在进行大量渲染。高光为红色时,表示该组件的性能不好。此时纯组件将帮助我们;让我们将结果组件迁移为纯组件:

    import React, { PureComponent } from 'react';

    class Result extends PureComponent {
      render() {
        return <li>{this.props.result}</li>;
      }
    }

    export default Result;

File: src/components/Numbers/Result.js

现在,如果我们尝试对数字做同样的处理,让我们看看区别。

如您所见,与类组件相比,使用纯组件 React 进行的渲染更少。现在您可能认为,如果我们使用无状态组件而不是纯组件,结果将是相同的。不幸的是,这不会发生;如果要验证这一点,让我们再次更改结果组件并将其转换为功能组件:

    import React from 'react';

    const Result = props => <li>{props.result}</li>;

    export default Result;

File: src/components/Numbers/Result.js

即使代码更少,但让我们看看渲染会发生什么。

如您所见,结果与类组件相同,这意味着并非所有必要的时间都使用无状态组件将帮助我们提高应用的性能。如果您考虑的组件是纯的,请考虑将它们转换为纯组件。

防止 React 中的 XSS 漏洞

在本教程中,我们将学习 React 中的跨站点脚本(XSS)漏洞。XSS 攻击在 web 应用中非常普遍,一些开发人员仍然没有意识到这一点。XSS 攻击是注入未受保护的 web 应用的 DOM 中的恶意脚本。每个应用的风险可能不同。它可能只是一个无辜的警报脚本注入,或者更糟糕的是,有人可以访问您的 cookie 并窃取您的私人凭据(密码),例如。

让我们创建一个 XSS 组件,开始使用一些 XSS 攻击。我们将有一个响应变量来模拟来自真实服务器的响应,我们将模拟使用 Redux 的初始状态(我们将在第 5 章中看到 Redux,掌握 Redux

怎么做。。。

现在我们将了解如何创建 XSS 组件:

  1. 创建 XSS 组件:
 import React, { Component } from 'react';

 // Let's suppose this response is coming from a service and have     
  // some XSS attacks in the content...
 const response = [
    {
      id: 1,
      title: 'My blog post 1...',
      content: '<p>This is <strong>HTML</strong> code</p>'
    },
    {
      id: 2,
      title: 'My blog post 2...',
      content: `<p>Alert: <script>alert(1);</script></p>`
    },
    {
      id: 3,
      title: 'My blog post 3...',
      content: `
 <p>
       <img onmouseover="alert('This site is not secure');" 
 src="attack.jpg" />
        </p>
      `
    }
  ];

 // Let's suppose this is our initialState of Redux 
  // which is injected to the DOM...
 const initialState = JSON.stringify(response);

 class Xss extends Component {
    render() {
      // Parsing the JSON string to an actual object...
      const posts = JSON.parse(initialState);

      // Rendering our posts...
      return (
        <div className="Xss">
          {posts.map((post, key) => (
            <div key={key}>
              <h2>{post.title}</h2>

              <p>{post.content}</p>
            </div>
          ))}
        </div>
      );
    }
  }

 export default Xss;

File: src/components/Xss/Xss.js

  1. 如果渲染此组件,将看到类似以下内容:

  1. 如您所见,默认情况下,React 阻止我们将 HTML 代码直接注入组件。它将 HTML 呈现为字符串。这很好,但有时我们需要在组件中插入 HTML 代码。
  2. 实施dangerouslySetInnerHTML这个道具可能会让你有点害怕(可能是因为它明确地说了“危险”这个词!)。我要告诉你,如果我们知道如何安全地使用它,这个道具还不错。*让我们修改之前的代码,我们将添加这个道具,看看 HTML 现在是如何呈现的:***
import React, { Component } from 'react';
 // Let's suppose this response is coming from a service and have       
  // some XSS attacks in the content...
 const response = [
    {
      id: 1,
      title: 'My blog post 1...',
      content: '<p>This is <strong>HTML</strong> code</p>'
    },
    {
      id: 2,
      title: 'My blog post 2...',
      content: `<p>Alert: <script>alert(1);</script></p>`
    },
    {
      id: 3,
      title: 'My blog post 3...',
      content: `
        <p>
          <img onmouseover="alert('This site is not secure');" 
          src="attack.jpg" />
        </p>
      `
    }
  ];

 // Let's suppose this is our initialState of Redux 
  // which is injected to the DOM...
 const initialState = JSON.stringify(response);

 class Xss extends Component {
    render() {
      // Parsing the JSON string to an actual object...
      const posts = JSON.parse(initialState);

      // Rendering our posts...
      return (
        <div className="Xss">
          {posts.map((post, key) => (
            <div key={key}>
              <h2>{post.title}</h2>
              <p><strong>Secure Code:</strong></p>
              <p>{post.content}</p>
              <p><strong>Insecure Code:</strong></p>
              <p 
 dangerouslySetInnerHTML={{ __html: post.content }} 
              />
            </div>
          ))}
        </div>
      );
    }
  }

 export default Xss;

File: src/components/Xss/Xss.js

  1. 我们的网站现在应该是这样的:

  1. 这很有趣,可能你认为“我的博客帖子 2”的内容会在浏览器中触发警报,但不会。如果我们检查代码,警报脚本就在那里。

  1. 即使我们使用dangerouslySetInnerHTMLReact 也能保护我们免受恶意脚本注入,但它的安全性不足以让我们在网站的安全方面放松。现在让我们来看看我的博文 3 内容的问题。代码<img onmouseover="alert('This site is not secure');" src="attack.jpg" />没有直接使用<script>标签注入恶意代码,而是使用img标签与事件(onmouseover一起使用。因此,如果您对 React 的保护感到满意,我们可以看到,如果将鼠标移到图像上,将执行此 XSS 攻击:

**

  1. 移除 XSS 攻击这有点吓人,对吧?但正如我在本食谱开头所说,有一种安全的方法可以使用危险的 LysetinerHTML,而且,是的,正如您现在所想,我们需要在使用危险的 LysetinerHTML 呈现代码之前清除恶意脚本。下一个脚本将负责从标记中删除<script>标记和事件,但当然,您可以根据您想要的安全级别进行修改:****
 import React, { Component } from 'react';

 // Let's suppose this response is coming from a service and have 
  // some XSS attacks in the content...
 const response = [
    {
      id: 1,
      title: 'My blog post 1...',
      content: '<p>This is <strong>HTML</strong> code</p>'
    },
    {
      id: 2,
      title: 'My blog post 2...',
      content: `<p>Alert: <script>alert(1);</script></p>`
    },
    {
      id: 3,
      title: 'My blog post 3...',
      content: `
        <p>
          <img onmouseover="alert('This site is not secure');" 
          src="attack.jpg" />
        </p>
      `
    }
  ];

 // Let's suppose this is our initialState of Redux 
  // which is injected to the DOM...
 const initialState = JSON.stringify(response);

 const removeXSSAttacks = html => {
    const SCRIPT_REGEX = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi;

    // Removing the <script> tags
    while (SCRIPT_REGEX.test(html)) {
      html = html.replace(SCRIPT_REGEX, '');
    }

    // Removing all events from tags...
    html = html.replace(/ on\w+="[^"]*"/g, '');

    return {
      __html: html
    }
  };

 class Xss extends Component {
    render() {
      // Parsing the JSON string to an actual object...
      const posts = JSON.parse(initialState);

      // Rendering our posts...
      return (
        <div className="Xss">
          {posts.map((post, key) => (
            <div key={key}>
              <h2>{post.title}</h2>
              <p><strong>Secure Code:</strong></p>
              <p>{post.content}</p>
              <p><strong>Insecure Code:</strong></p>
              <p 
 dangerouslySetInnerHTML=
              {removeXSSAttacks(post.content)} 
              />
            </div>
          ))}
        </div>
      );
    }
  }

 export default Xss;

File: src/components/Xss/Xss.js

  1. 如果我们现在查看代码,我们将看到现在的渲染更加安全:

  1. JSON.stringify到目前为止,我们已经学会了如何使用dangerouslySetInnerHTML将 HTML 代码注入 React 组件,但是使用 JSON.stringify 还有一个潜在的安全问题。如果我们受到 XSS 攻击(<script>标记在内容中)在我们的响应中,然后我们使用 JSON.stringify 将对象转换为字符串,HTML 标记没有编码。这意味着如果我们将字符串注入 HTML(就像 Redux 对初始状态所做的那样),我们将有一个潜在的安全问题JSON.stringify(response)输出如下:****
  [
      {"id":1,"title":"My blog post 1...","content":"<p>This is <strong>HTML</strong> code</p>"},
      {"id":2,"title":"My blog post 2...","content":"<p>Alert: <script>alert(1);</script></p>"},
      {"id":3,"title":"My blog post 3...","content":"<p><img onmouseover=\"alert('This site is not secure');\" src=\"attack.jpg\" /></p>"}
  ]    
  1. 如您所见,所有 HTML 都是公开的,没有任何编码字符,这是一个问题。但我们如何解决这个问题呢?我们需要安装一个名为serialize-javascript的软件包:
 npm install serialize-javascript
  1. 我们需要像这样序列化代码,而不是使用JSON.stringify
 import serialize from 'serialize-javascript';

 // Let's suppose this response is coming from a service and have    
  // some XSS attacks in the content...
 const response = [
    {
      id: 1,
      title: 'My blog post 1...',
      content: '<p>This is <strong>HTML</strong> code</p>'
    },
    {
      id: 2,
      title: 'My blog post 2...',
      content: `<p>Alert: <script>alert(1);</script></p>`
    },
    {
      id: 3,
      title: 'My blog post 3...',
      content: `<p><img onmouseover="alert('This site is not 
      secure');" src="attack.jpg" /></p>`
    }
  ];

 // Let's suppose this is our initialState of Redux which is 
  // injected to the DOM...
 const initialState = serialize(response);
 console.log(initialState);
  1. 控制台的输出如下所示:
  [
      {"id":1,"title":"My blog post 1...","content":"\u003Cp\u003EThis is \u003Cstrong\u003EHTML\u003C\u002Fstrong\u003E code\u003C\u002Fp\u003E"},
      {"id":2,"title":"My blog post 2...","content":"\u003Cp\u003EAlert: \u003Cscript\u003Ealert(1);\u003C\u002Fscript\u003E\u003C\u002Fp\u003E"},
      {"id":3,"title":"My blog post 3...","content":"\u003Cp\u003E\u003Cimg onmouseover=\"alert('This site is not secure');\" src=\"attack.jpg\" \u002F\u003E\u003C\u002Fp\u003E"}
    ]
  1. 现在,我们的代码使用 HTML 实体(编码),而不是直接使用 HTML 标记,好消息是我们可以使用JSON.parse再次将该字符串转换为原始对象。我们的组件应该如下所示:
 import React, { Component } from 'react';
 import serialize from 'serialize-javascript';

 // Let's suppose this response is coming from a service and have 
  // some XSS attacks in the content...
 const response = [
    {
      id: 1,
      title: 'My blog post 1...',
      content: '<p>This is <strong>HTML</strong> code</p>'
    },
    {
      id: 2,
      title: 'My blog post 2...',
      content: `<p>Alert: <script>alert(1);</script></p>`
    },
    {
      id: 3,
      title: 'My blog post 3...',
      content: `<p><img onmouseover="alert('This site is not secure');" 
      src="attack.jpg" /></p>`
    }    
  ];

  // Let's suppose this is our initialState of Redux which is 
  // injected to the DOM...
 const secureInitialState = serialize(response);
 // const insecureInitialState = JSON.stringify(response);

 console.log(secureInitialState);

 const removeXSSAttacks = html => {
    const SCRIPT_REGEX = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi;

    // Removing the <script> tags
    while (SCRIPT_REGEX.test(html)) {
      html = html.replace(SCRIPT_REGEX, '');
    }

    // Removing all events from tags...
    html = html.replace(/ on\w+="[^"]*"/g, '');

    return {
      __html: html
    }
  };

 class Xss extends Component {
    render() {
      // Parsing the JSON string to an actual object...
      const posts = JSON.parse(secureInitialState);

      // Rendering our posts...
      return (
        <div className="Xss">
          {posts.map((post, key) => (
            <div key={key}>
              <h2>{post.title}</h2>
              <p><strong>Secure Code:</strong></p>
              <p>{post.content}</p>
              <p><strong>Insecure Code:</strong></p>
              <p 
 dangerouslySetInnerHTML={removeXSSAttacks(post.content)} 
              />
            </div>
          ))}
        </div>
      );
    }
  }

 export default Xss;

File: src/components/Xss/Xss.js

它是如何工作的。。。

正如您所看到的,XSS 攻击非常普遍,许多网站在不知情的情况下就遇到了这个问题。如果我们不采取最低限度的安全预防措施,API 中可能会发生其他注入攻击,如 SQL 注入。

还有更多。。。

以下是一些安全建议:

  • 始终清理来自表单的用户内容。
  • 始终使用serialize而不是JSON.stringify
  • 仅在绝对必要时使用dangerouslySetInnerHTML
  • 对您的组件进行单元测试,并尝试覆盖所有可能的 XSS 攻击(我们将在第 12 章、测试和调试中看到单元测试)。
  • 始终使用sha1md5对密码进行加密,不要忘记添加 salt 值(例如,如果密码为abc123,那么您的 salt 可以像这样加密:sha1(md5('$4ltT3xt_abc123'))
  • 如果使用 cookie 存储敏感信息(主要是个人信息和密码),则可以使用 Base64 保存 cookie 以混淆数据。
  • 除非您需要公开,否则请向 API(安全令牌)添加一些保护。第 8 章中有一个关于安全令牌的配方,使用 MongoDB 和 MySQL创建一个带有 Node.js 的 API。********