React-秘籍-全-

React 秘籍(全)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

现在存在大量用于 Web 开发的 JavaScript 库、框架和工具。然而,我们应该评估每种技术是否符合我们项目的要求。这就是为什么我想向你介绍 React,这是一个创建动态 UI 的最强大的库之一。它是目前由 Facebook 制作的最流行的库(不是框架)。我在不同的项目中使用过其他 JS 框架,比如 AngularJS(还有新版本的 Angular 2、4、5)、Backbone.js、Ember 和 Vue.js,但我可以告诉你,使用 React 我更喜欢开发新的 Web 应用程序。

React 改变了 Web 应用程序的开发方式,结合 Redux,我们得到了一个强大的前端架构,这不仅对有经验的开发人员有意义,也对刚开始前端之旅的人有意义。

本书介绍了 React 的所有工具和最佳实践,以简单易行的食谱形式呈现,本书中的所有食谱都是 100%实用的,每一个都有必要的代码来理解所有重要的内容。

欢迎来到一个更美好的未来,愿你在阅读和学习本书时玩得开心。

本书适合对象

本书可以被任何具有构建 Web 应用程序基础知识的开发人员使用。主要是 JavaScript 开发人员,但不限于任何其他类型的开发人员。

本书内容

[第一章],使用 React 工作,React 是一个由 Facebook 制作的 JavaScript 库(MIT 许可证),用于创建交互式 UI。它用于创建动态和可重用的组件。React 最强大的地方在于它可以在客户端、服务器、移动应用甚至 VR 应用中使用。

[第二章],征服组件和 JSX,本章包含了有关如何在 React 中创建组件的技巧。我们将学习如何创建 React 组件(类组件、纯组件和函数组件)并组织我们的项目结构。

[第三章],处理事件、绑定和有用的 React 包,本章包含与处理事件、在 React 中绑定方法以及实现一些最有用的 React 包相关的技巧。

[第四章],使用 React Router 为我们的应用程序添加路由,本章中,我们将学习如何使用 React Router v4 在我们的项目中添加动态路由。

第五章,《精通 Redux》,Redux 是 JavaScript 应用程序的可预测状态容器。这意味着 Redux 可以与原生 JavaScript 或诸如 Angular 和 jQuery 之类的框架/库一起使用。Redux 主要是一个负责发出状态更新和对操作做出响应的库。

第六章,《使用 Redux Form 创建表单》,表单是任何 Web 应用程序的基本部分,在接下来的教程中,我们将学习如何使用带有或不带有 Redux Form 的表单。

第七章,《使用 React 进行动画》,动画在任何 Web 应用程序中都很常见。自 CSS3 以来,动画已经变得普遍且易于实现。动画最常见的用途是过渡,其中您可以更改 CSS 属性并定义持续时间或延迟。

第八章,《使用 Node.js 和 MongoDB 以及 MySQL 创建 API》,Node.js 广泛用作 Web 应用程序的后端,因为它易于创建 API,并且其性能优于诸如 Java、PHP 和 Ruby 等技术。通常,使用 Node.js 的最流行方式是使用一个名为 Express 的框架。

第九章,《Apollo 和 GraphQL》,GraphQL 是一种应用层查询语言,可以与任何数据库一起使用,也是一个由 Facebook 创建的开源项目(MIT 许可证)。与 REST 的主要区别在于 GraphQL 不使用端点而是使用查询,并且受大多数服务器语言的支持,如 JavaScript(Node.js)、Go、Ruby、PHP、Java、Python 等。

第十章,《精通 Webpack 4.x》,Webpack 4 默认情况下不需要配置文件。在旧版本中,你必须有一个配置文件,但是当然,如果你需要根据项目需求自定义 Webpack 4,你仍然可以创建一个配置文件,这样会更容易配置。

[第十一章](884074f8-0a8a-4e3f-b9dc-b817e0f3f7c3.xhtml),实现服务器端渲染,如果你不太关心 SEO,可能就不需要担心服务器端渲染(SSR)。目前,Googlebot 支持客户端渲染(CSR),它可以在 Google 中索引我们的网站,但如果你关心 SEO,并且担心在其他搜索引擎(如 Yahoo、Bing 或 DuckDuckGo)上改善 SEO,那么使用服务器端渲染(SSR)是一个好方法。

[第十二章](1daa68c4-442d-479f-b1b6-3efa80592901.xhtml),测试和调试,测试和调试对于任何希望拥有最佳质量的项目都非常重要。不幸的是,许多开发人员不关心测试(单元测试),因为他们认为这会减慢开发速度,有些人把它留到项目结束时。根据我的个人经验,我可以说,从项目开始就进行测试会节省时间,因为最后你需要修复的错误会更少。

[第十三章](863541f6-a9a0-4a1b-8d1b-e448571b24e1.xhtml),部署到生产环境,现在是时候将我们的应用部署到生产环境并展示给世界。在这一章中,你将学习如何使用最好的云服务之一:Digital Ocean 来部署我们的 React 应用。

[第十四章](2b2d55bd-c734-4907-9688-0a9e9a2ff0cb.xhtml),使用 React Native 工作,React Native 是使用 JavaScript 和 React 构建移动应用的框架。许多人认为使用 React Native 可以制作一些“移动 Web 应用”或“混合应用”(如 Ionic、PhoneGap 或 Sencha),但实际上你构建的是原生应用,因为 React Native 会将你的 React 代码转换为 Android 的 Java 和 iOS 应用的 Objective-C。

要充分利用这本书

要掌握 React,你需要对 JavaScript 和 Node.js 有基本的了解。这本书主要针对 Web 开发人员,写作时做出了以下假设:

  • 读者知道如何安装最新版本的 Node.js。

  • 能理解 JavaScript ES6 语法的中级开发人员。

  • 对 CLI 工具和 Node.js 语法有一些经验。

这本书也有一些内容适用于使用 React Native 的移动开发人员(iOS 和 Android),如果你是初学者,你应该学习如何安装 Android SDK 或使用 Xcode 安装 iOS 模拟器。

下载示例代码文件

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

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

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

  2. 选择“支持”选项卡。

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

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

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

  • Windows 用户可使用 WinRAR/7-Zip

  • Mac 用户可使用 Zipeg/iZip/UnRarX

  • Linux 用户可使用 7-Zip/PeaZip

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

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

警告或重要说明会显示为这样。提示和技巧会显示为这样。

第一章:使用 React

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

  • 介绍

  • 在 React 中使用最新的 JS 功能

  • React 中有什么新功能?

  • 在 Windows 上使用 React

介绍

React 是一个由 Facebook 制作的 JavaScript 库(MIT 许可证),用于创建交互式 UI。它用于创建动态和可重用的组件。React 最强大的地方在于它可以在客户端、服务器、移动应用程序甚至 VR 应用程序中使用。

在现代网络中,我们需要不断地操作 DOM;问题是这样做可能会严重影响我们应用程序的性能。React 使用虚拟 DOM,这意味着所有更新都发生在内存中(这比直接操作真实 DOM 更快)。与其他 JavaScript 框架(如 Angular、Vue 或 Backbone)相比,React 的学习曲线较短,主要是因为 React 代码大多是用现代 JavaScript(类、箭头函数、字符串模板等)编写的,而没有太多用于编写代码的模式,比如依赖注入,或者像 Angular 中的模板系统。

像 Airbnb、Microsoft、Netflix、Disney、Dropbox、Twitter、PayPal、Salesforce、Tesla 和 Uber 这样的公司在他们的项目中广泛使用 React。在这本书中,你将学习如何像他们一样使用最佳实践来开发你的 React 应用程序。

在 React 中使用最新的 JS 功能

正如我在介绍中所说,React 主要是用现代 JavaScript(ES6、ES7 和 ES8)编写的。如果你想利用 React,有一些现代 JS 功能是你应该掌握的,以获得你的 React 应用程序的最佳结果。在这个第一个示例中,我们将涵盖基本的 JS 功能,这样你就可以准备好开始在你的第一个 React 应用程序上工作了。

如何做到…

在本节中,我们将看到如何在 React 中使用最重要的 JS 功能:

  1. letconst:在 JavaScript 中声明变量的新方法是使用letconst。你可以使用let来声明可以在块作用域中改变其值的变量。letvar的区别在于,let是一个块作用域变量,不能是全局的,而用var可以声明一个全局变量,例如:
 var name = 'Carlos Santana';
    let age = 30;

    console.log(window.name); // Carlos Santana
    console.log(window.age);  // undefined
  1. 理解“块作用域”的最好方法是通过使用varlet声明for循环。首先,让我们使用var并看看它的行为:
 for (var i = 1 ; i <= 10; i++) {
      console.log(i); // 1, 2, 3, 4... 10
    }

    console.log(i); // Will print the last value of i: 10
  1. 如果我们用let写相同的代码,会发生这种情况:
 for (let i = 1 ; i <= 10; i++) {
      console.log(i); // 1, 2, 3, 4... 10
    }

    console.log(i); // Uncaught ReferenceError: i is not defined
  1. 使用const,我们可以声明常量,这意味着值不能被改变(除了数组和对象):
 const pi = 3.1416;
    pi = 5; // Uncaught TypeError: Assignment to constant variable.
  1. 如果我们用const声明一个数组,我们可以操作数组元素(添加、删除或修改元素):
 const cryptoCurrencies = ['BTC', 'ETH', 'XRP'];

    // Adding ERT: ['BTC', 'ETH', 'XRP', 'ERT'];
    cryptoCurrencies.push('ERT'); 

 // Will remove the first element: ['ETH', 'XRP', 'ERT']; cryptoCurrencies.**shift**();

    **// Modifying an element**
    cryptoCurrencies[1] = 'LTC'; **// ['ETH', 'LTC', 'ERT'];**
  1. 此外,使用对象,我们可以添加、删除或修改节点:
 const person = {
      name: 'Carlos Santana',
      age: 30,
      email: 'carlos@milkzoft.com'
    };

    // Adding a new node...
    person.website = 'https://www.codejobs.com';

    // Removing a node...
    delete person.email;

    // Updating a node...
    person.age = 29;
  1. 扩展运算符:扩展运算符(...)将可迭代对象分割成单独的值。在 React 中,它可以用于将值推入另一个数组,例如当我们想要通过利用setState向待办事项列表添加新项目时(这将在下一章中解释):
    this.setState({
      items: [
        ...this.state.items, // Here we are spreading the current items
        {
      task: 'My new task', // This will be a new task in our Todo list.
        }
      ]
    });
  1. 此外,扩展运算符可以在 React 中用于在 JSX 中扩展属性(props):
    render() {
      const props = {};

      props.name = 'Carlos Santana';
      props.age = 30;
      props.email = 'carlos@milkzoft.com';

      return <Person {...props} />;
    }
  1. 剩余参数:rest参数也用...表示。在函数中以...为前缀的最后一个参数称为剩余参数。剩余参数是一个数组,当参数的数量超过命名参数的数量时,它将包含函数的其余参数:
 function setNumbers(param1, param2, ...args) {
      // param1 = 1
      // param2 = 2
      // args = [3, 4, 5, 6];
      console.log(param1, param2, ...args); // Log: 1, 2, 3, 4, 5, 6
    }

    setNumbers(1, 2, 3, 4, 5, 6);
  1. 解构赋值:解构赋值是 React 中最常用的功能。它是一个表达式,允许我们将可迭代对象的值或属性分配给变量。通常,我们可以将组件的 props 转换为变量(或常量):
 // Imagine we are on our <Person> component and we are 
 // receiving the props (in this.props): name, age and email. render() { // Our props are: 
      // { name: 'Carlos Santana', age: 30, email: 
       'carlos@milkzoft.com' } console.log(this.props); 
      const { name, age, email } = this.props;

      // Now we can use the nodes as constants... console.log(name, age, email);

      return (
        <ul>
          <li>Name: {name}</li>
          <li>Age: {age}</li>
          <li>Email: {email}</li>
        </ul> ); }

    // Also the destructuring can be used on function parameters
    const Person = ({ name, age, email }) => (
      <ul>
        <li>Name: {name}</li>
        <li>Age: {age}</li>
        <li>Email: {email}</li>
      </ul>
    );
  1. 箭头函数:ES6 提供了一种使用=>运算符创建函数的新方法。这些函数称为箭头函数。这种新方法有更短的语法,箭头函数是匿名函数。在 React 中,箭头函数用作绑定方法中的this对象的一种方式,而不是在构造函数中绑定它。
 class Person extends Component {
      showProps = () => {
        console.log(this.props); // { name, age, email... }
      }

      render() {
        return (
          <div>
            Consoling props: {this.showProps()}
          </div>
        );
      }
    }
  1. 模板文字:模板文字是使用反引号( )而不是单引号('')或双引号("")创建字符串的新方法。React 使用模板文字来连接类名或使用三元运算符呈现字符串:
 render() {
      const { theme } = this.props;

      return (
        <div 
 className={`base ${theme === 'dark' ? 'darkMode' : 
         'lightMode'}`}        >
          Some content here...
        </div>
      );
    }
  1. 映射:map()方法返回一个新数组,其中包含对调用数组中每个元素的提供函数的结果。在 React 中,映射的使用非常广泛,主要用于在 React 组件内呈现多个元素;例如,它可以用于呈现任务列表:
 render() {
      const tasks = [
        { task: 'Task 1' },
        { task: 'Task 2' },
        { task: 'Task 3' }
      ];

      return (
        <ul>
          {tasks.map((item, key) => <li key={key}>{item.task}</li>}
        </ul>
      );
    }
  1. Object.assign()Object.assign() 方法用于将一个或多个源对象的所有可枚举自有属性的值复制到目标对象中。它将返回目标对象。这个方法主要用于 Redux,用于创建不可变对象并将新状态返回给 reducers(Redux 将在第五章,精通 Redux中介绍):
    export default function coinsReducer(state = initialState, action) {
      switch (action.type) {
        case FETCH_COINS_SUCCESS: {
          const { payload: coins } = action;

          return Object.assign({}, state, {
            coins
          });
        }

        default:
         return state;
      }
    };
  1. :JavaScript 类是 ES6 中引入的,主要是现有基于原型的继承的新语法。类是函数,不会被提升。React 使用类来创建类 组件
    import React, { Component } from 'react';

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

    export default Home;
  1. 静态方法:静态方法不是在类的实例上调用的。相反,它们是在类本身上调用的。这些通常是实用函数,比如用来创建或克隆对象的函数。在 React 中,它们可以用来定义组件中的 PropTypes
 import React, { Component } from 'react';
    import PropTypes from 'prop-types';
    import logo from '../../images/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;
  1. PromisesPromise 对象表示异步操作的最终完成(或失败)及其结果值。我们将在 React 中使用 promises 来处理请求,例如使用 axios 或 fetch;此外,我们将使用 Promises 来实现服务器端渲染(这将在第十一章,实现服务器端渲染中介绍)。

  2. async/await:async 函数声明定义了一个异步函数,返回一个 AsyncFunction 对象。这也可以用来执行服务器请求,例如使用 axios:

    Index.getInitialProps = async () => {
      const url = 'https://api.coinmarketcap.com/v1/ticker/';
      const res = await axios.get(url);

      return {
        coins: res.data
      };
    };

React 有什么新功能?

这段文字是在 2018 年 8 月 14 日写的,当时 React 的最新版本是 16.4.2。React 16 版本有一个名为 Fiber 的新核心架构。

在这个配方中,我们将看到这个版本中最重要的更新,你应该了解这些更新,以充分利用 React。

如何做...

让我们看看新的更新:

  1. 组件现在可以从渲染中返回数组和字符串:以前,React 强制你返回一个包裹在
    或其他标签中的元素;现在可以直接返回数组或字符串:
    // Example 1: Returning an array of elements.
 render() {
      // Now you don't need to wrap list items in an extra element
      return [
        <li key="1">First item</li>,
        <li key="2">Second item</li>,
        <li key="3">Third item</li>,
      ];
    }

    // Example 2: Returning a string
    render() {
      return 'Hello World!';
    }
  1. 此外,React 现在有一个名为 Fragment 的新功能,它也可以作为元素的特殊包装器。它可以用空标签(<></>)指定,也可以直接使用 React.Fragment
    // Example 1: Using empty tags <></>
 render() {
      return (
        <>
          <ComponentA />
          <ComponentB />
          <ComponentC />
        </>
      );
    }

    // Example 2: Using React.Fragment
    render() {
      return (
        <React.Fragment>
          <h1>An h1 heading</h1>
          Some text here.
          <h2>An h2 heading</h2>
          More text here.
          Even more text here.
        </React.Fragment>
      );
    }

 // Example 3: Importing Fragment
    import React, { Fragment } from 'react';
    ...
    render() {
      return (
        <Fragment>
          <h1>An h1 heading</h1>
          Some text here.
          <h2>An h2 heading</h2>
          More text here.
          Even more text here.
        </Fragment>
      );
    }
  1. 来自官方网站的错误边界

UI 的一部分出现 JavaScript 错误不应该导致整个应用程序崩溃。为了解决 React 用户的这个问题,React 16 引入了一个新概念,即"错误边界"。错误边界是 React 组件,它们捕获其子组件树中任何位置的 JavaScript 错误,记录这些错误,并显示一个替代 UI,而不是崩溃的组件树。错误边界在渲染期间、生命周期方法中以及它们下面整个树的构造函数中捕获错误。如果一个类组件定义了一个名为 componentDidCatch(error, info)的新生命周期方法,它就成为了一个错误边界。

    class ErrorBoundary extends React.Component {
      constructor(props) {
        super(props);

        this.state = { 
          hasError: false 
        };
      }

      componentDidCatch(error, info) {
        // Display fallback UI
        this.setState({ 
          hasError: true 
        });

        // You can also log the error to an error reporting service
        logErrorToMyService(error, info);
      }

      render() {
        if (this.state.hasError) {
          // You can render any custom fallback UI
          return <h1>Something went wrong.</h1>;
        }

        return this.props.children;
      }
    }

    // **Then you can use it as a regular component:
**    **render**() {
   **   <ErrorBoundary>**
        <MyComponent />
      **</ErrorBoundary>
**    }
  1. 更好的服务器端渲染 来自官方网站:

React 16 包括一个完全重写的服务器渲染器。它非常快。它支持流式传输,因此您可以更快地开始向客户端发送字节。并且由于一种新的打包策略,它会编译掉 process.env 检查(信不信由你,在 Node 中读取 process.env 真的很慢!),您不再需要捆绑 React 以获得良好的服务器渲染性能。

  1. 从官方网站上减小文件大小:"尽管增加了所有这些内容,React 16 实际上比 15.6.1 要小。
  • react 为 5.3 kb(经过 gzip 压缩为 2.2 kb),而之前为 20.7 kb(经过 gzip 压缩为 6.9 kb)

  • react-dom 为 103.7 kb(经过 gzip 压缩为 32.6 kb),而之前为 141 kb(经过 gzip 压缩为 42.9 kb)

  • react + react-dom 为 109 kb(经过 gzip 压缩为 34.8 kb),而之前为 161.7 kb(经过 gzip 压缩为 49.8 kb)

与上一个版本相比,这相当于总体减小了 32%的大小(gzip 后减小了 30%)。

如果您想查看 React 的最新更新,可以访问官方 React 博客:reactjs.org/blog

在 Windows 上使用 React

我不是 Windows 的开发大粉丝,因为有时配置起来有点问题。我总是更喜欢 Linux 或 Mac,但我知道很多读者会使用 Windows。在这个配方中,我将向您展示在使用 Windows 时可能遇到的最常见问题。

如何做...

我们现在将看到在开发中使用 Windows 时最常见的问题:

  1. 终端:你将面临的第一个问题是使用 Windows 终端(CMD),因为它不支持 Unix 命令(如 Linux 或 Mac)。解决方法是安装一个 Unix 终端;最受推荐的是使用Git Bash终端,它在你安装Git时就包含在内(git-scm.com),第二个选择是安装Cygwin,它是 Windows 中的 Linux 终端(www.cygwin.com)。

  2. 环境变量:在 Windows 上使用的另一个常见问题是设置环境变量。通常,当我们编写 npm 脚本时,我们设置诸如NODE_ENV=productionBABEL_ENV=development之类的环境变量,但在 Windows 中设置这些变量时,你需要使用SET命令,这意味着你需要执行SET NODE_ENV=productionSET BABEL_ENV=development。这样做的问题是,如果你与其他使用 Linux 或 Mac 的人一起工作,他们将会遇到SET命令的问题,而且你可能需要忽略这个文件,只为你的本地环境修改它。这可能会很繁琐。解决这个问题的方法是使用一个叫做 cross-env 的包;你可以通过执行npm install cross-env来安装它,这将在 Windows、Mac 和 Linux 上都能工作:

   "scripts": {
      "start": "cross-env NODE_ENV=development webpack-dev-server --
       mode development --open",
      "start-production": "cross-env NODE_ENV=production webpack-dev-
       server --mode production"
    }
  1. 区分大小写的文件或目录:实际上,在 Linux 上也会发生这种情况,但有时很难识别这个问题,例如,如果你在components/**home**/Home.jsx目录中创建一个组件,但在你的代码中尝试像这样导入组件:
 import Home from './components/Home/Home';

通常,这在 Mac 上不会引起任何问题,但可能会在 Linux 或 Windows 上生成错误,因为我们试图将一个文件以不同的名称(因为它是区分大小写的)导入到目录中。

  1. 路径:Windows 使用反斜杠(\)来定义路径,而在 Mac 或 Linux 中使用正斜杠(/)。这是有问题的,因为有时我们需要定义一个路径(在 Node.js 中大多数情况下),我们需要做类似这样的事情:
 // In Mac or Linux
    app.use(
      stylus.middleware({
        src: __dirname + '/stylus',
        dest: __dirname + '/public/css',
        compile: (str, path) => {
          return stylus(str)
            .set('filename', path)
            .set('compress', true);
        }
      })
    );

    // In Windows
    app.use(
      stylus.middleware({
        src: __dirname + '\stylus',
        dest: __dirname + '\public\css',
        compile: (str, path) => {
          return stylus(str)
            .set('filename', path)
            .set('compress', true);
        }
      })
    );

    // This can be fixed by using path
    import path from 'path';

   // path.join will generate a valid path for Windows or Linux and Mac
    app.use(
      stylus.middleware({
        src: path.join(__dirname, 'stylus'),
        dest: path.join(__dirname, 'public', 'css'),
        compile: (str, path) => {
          return stylus(str)
            .set('filename', path)
            .set('compress', config().html.css.compress);
        }
      })
    );

第二章:征服组件和 JSX

在本章中,将涵盖以下内容:

  • 创建我们的第一个 React 组件

  • 组织我们的 React 应用程序

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

  • 将 props 传递给组件并使用 PropTypes 进行验证

  • 在组件中使用本地状态

  • 创建一个函数式或无状态组件

  • 理解 React 生命周期方法

  • 理解 React 纯组件

  • 在 React 中防止 XSS 漏洞

介绍

本章包含与在 React 中创建组件相关的内容。我们将学习如何创建 React 组件(类组件、纯组件和函数组件)并组织我们的项目结构。我们还将学习如何使用 React 本地状态,实现所有 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;

文件:src/Home.js

  1. 您可能已经注意到我们在文件末尾导出了我们的类组件,但直接在类声明上导出也是可以的,就像这样:
 import React, { Component } from 'react';
 export default class Home extends Component {
      render() {
        return <h1>I'm Home Component</h1>;
      }
    }

文件:src/Home.js

我更喜欢在文件末尾导出它,但有些人喜欢以这种方式做,所以这取决于您的偏好。

  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;

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

文件:src/App.js

它是如何工作的...

正如你所看到的,我们从 React 库中导入了ReactComponent。你可能注意到我们并没有直接使用React对象。要在 JSX 中编写代码,你需要导入React。JSX 类似于 HTML,但有一些不同之处。在接下来的示例中,你将学习更多关于 JSX 的知识。

这个组件被称为class组件(React.Component),有不同的类型:纯组件(React.PureComponent)和功能组件,也被称为无状态组件,我们将在接下来的示例中介绍。

如果你运行应用程序,你应该会看到类似这样的东西:

还有更多...

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

所有 React 组件的名称都应该以文件和类名的首字母大写开头。一开始,你可能会感到不舒服,但这是 React 中的最佳实践。

JSX 和 HTML 之间的一些主要区别是属性名称。你可能已经注意到,我们使用className而不是class。这是唯一的特殊属性名称。其他由破折号分隔的两个单词需要转换为驼峰命名法,例如,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. 现在你的目录树应该是这样的:

我强烈建议你为共享组件创建另一个目录,src/shared/components。我将在下一个教程中详细解释这一点。

  1. App.js文件中,更改logoHome的引入:
 import logo from '../shared/images/logo.svg';
 import Home from './Home/Home';

文件:src/components/App.js

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

文件:src/index.js

工作原理...

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

在下一个教程中,我们将看到如何在我们的应用程序中共享组件。

使用 CSS 类和内联样式来为组件添加样式

在上一个教程中,我们学会了如何创建一个类组件。现在让我们为Home组件添加一些 CSS。

在 React 中,最佳实践之一是将样式文件放在与组件相同的目录中。如果你曾经使用过 PHP、Node 或任何其他服务器语言,你可能会将样式写在一个style.css文件中,并在模板中使用link标签进行引入。React 使用的是目前最流行的模块打包工具 Webpack。通过 Webpack,我们可以配置处理样式的方式(直接使用 CSS 或使用 CSS 预处理器如 Sass、Stylus 或 Less CSS),并且我们可以实现 CSS 模块化。这是一个强大的方式来避免 CSS 的三个主要问题:

  • 不再有冲突(意外的 CSS 覆盖)

  • 显式依赖(每个组件的样式)

  • 没有全局作用域

在第十章,《掌握 Webpack 4.x》,我们将介绍 Webpack,并且我们将能够在项目中使用 Sass 或 Stylus 来实现 CSS 模块。

操作步骤...

现在我们将为Home组件添加 CSS:

  1. 创建一个新的应用程序,或者使用之前的一个(my-first-react-app)。

  2. 然后为我们的Home组件创建一个新的 CSS 文件。让我们重用上一个示例中创建的Home组件。现在,您需要在与您的Home.js文件相同级别的位置(在components文件夹内)创建一个Home.css文件。在创建此文件之前,让我们稍微修改我们的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;

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

  1. 现在让我们给我们的Home.css添加样式。基本上,我们将我们的组件包装在一个className为 Home 的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;
  }

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

  1. 现在假设您需要添加内联样式。我们可以使用 style 属性来实现这一点,CSS 属性需要以驼峰命名法写在{{ }}之间,就像这样:
 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;

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

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

它是如何工作的...

如您所见,将 CSS 文件连接到我们的组件非常简单,如果您正确地按照所有步骤进行操作,您的网站应该看起来像这样:

还有更多...

您可能好奇 CSS 代码是如何添加到浏览器中的,因为我们没有直接向项目导入 CSS 文件(例如使用<link>标签)。好吧,您会惊讶地发现,CSS 代码是通过使用<style>标签将其注入到我们的<head>标签中的每个导入样式表中的。如果您使用 Chrome DevTools 检查您的项目,您会看到类似于这样的内容:

这是因为在我们使用create-react-app创建应用程序时,默认使用了名为style-loader的 Webpack 加载器:

当我们使用create-react-app时,没有直接修改 Webpack 配置的方法,因为它使用一个名为react-scripts的包,但是在第十章,掌握 Webpack中,我们将看到如何配置我们的 Webpack,而不使用create-react-app这样的起始工具包。

还有更多的 Webpack 加载程序可以执行不同的操作,例如css-loader用于 CSS 模块,sass-loader用于实现 Sass,stylus-loader用于实现 Stylus,以及extract-text-plugin用于将 CSS 代码移动到.css文件中,而不是将其注入到 DOM 中(通常,这仅在生产中使用)。

将 props 传递给组件并使用 PropTypes 进行验证

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

如何做...

以前创建的 React 应用程序的名称相同,让我们首先创建我们的Header组件。

  1. 此时,我们当前的header放置在App.js中。
 import React, { Component } from 'react';
 import logo from '../shared/images/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;

文件:src/components/App.js

  1. 让我们将该标题移动到我们的新Header组件中,然后将其导入到App组件中。因为layout components是全局或共享的,所以我们需要在我们的shared components目录(src/shared/components/layout)中创建一个layout directory

  2. 在继续之前,您必须安装一个名为prop-types的软件包,以使用PropTypes验证:

npm install prop-types
  1. PropTypes最初作为 React 核心模块的一部分发布,并且通常与 React 组件一起使用。PropTypes用于记录传递给组件的属性的预期类型。React 将检查传递给组件的 props 与这些定义是否匹配,并且如果不匹配,它将在开发中发送警告:
 import React, { Component } from 'react';
 import PropTypes from 'prop-types';
 import logo from '../../images/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;

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

  1. staticPropTypes属性基本上是一个对象,在其中您需要定义将传递的 prop 的类型。arrayboolfuncnumberobjectstringsymbol是原始类型,但也有特定的类型,例如nodeelementinstanceOfoneOfoneOfTypearrayOfobjectOfshapeany。还有一个可选属性叫做isRequired,如果 prop 是必需的,将其添加到任何类型中,如果未定义,将产生 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;

文件:src/components/App.js 不要与<Header/>组件混淆,它与 HTML5 中的<header>标签不同,这就是为什么在 React 中建议在类名中使用大写字母。

  1. 传递给我们组件的所有属性都包含在这个 props 中。您可能已经注意到,我们只发送了title属性,因为它是唯一必需的属性。url属性是可选的,并且在解构中也有一个默认值(http://localhost:3000)。如果我们不传递 title 属性,即使在解构中有一个默认值 Welcome to React,我们也会收到警告,如下所示:

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

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

 export default Footer;

文件:src/shared/components/layout/Footer.js

  1. 到目前为止,我们只将 props 作为属性传递(使用自关闭组件<Component />),但还有另一种将 props 作为子元素传递的方式(<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;

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

文件:src/components/App.js

它是如何工作的...

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

如果您正确地按照所有步骤进行了操作,您应该会看到类似于这样的东西:

还有更多...

如您所见,有许多将 props 发送到组件的方法。还有更多接收 props 的方法,例如使用 Redux(通过容器)或 React Router,但这些是我们将在下一章中涵盖的主题。

在组件中使用本地状态

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

本地状态对于切换、处理表单非常有帮助,并且用于在同一组件内管理信息。如果我们需要在不同组件之间共享数据,不建议使用本地状态。在这种情况下,我们需要实现 Redux 状态,我们将在第五章精通 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;

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

  1. 在这个例子中,我们在构造函数中将本地状态定义为一个对象,并在渲染时直接打印值。我们在构造函数的开头使用super()。这是用来调用父构造函数(React.Component)的。如果我们不包括它,我们会得到这样的错误:

  1. 在我们添加了super()之后,我们需要将我们的初始状态定义为一个普通对象:
  this.state = {
    name: 'Carlos'
  };
  1. 使用this.setState()更新我们的本地状态:现在,这只是一个没有被更新的状态。这意味着组件将永远不会再次重新渲染。要更新状态,我们需要使用this.setState()方法并传递状态的新值。我们可以添加一个setTimeout来在 1 秒后(1,000 毫秒)更新名称状态,所以我们需要修改我们的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方法每秒都会被调用,导致无限循环。这将影响我们应用的性能,这就是为什么在更新状态时需要小心。正如您所看到的,在 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;

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

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

它是如何工作的...

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

还有更多...

本地状态也用于处理表单,但我们将在第六章使用 Redux Form 创建表单中进行介绍。

创建一个函数式或无状态组件

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

将 props 传递给组件并使用 PropTypes 进行验证示例中,我们创建了一些布局组件(HeaderContentFooter)。这些组件通常不是动态的(除非您想在标题中添加切换菜单或一些用户信息),所以在这种情况下,我们可以将它们转换为函数组件。

如何做...

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

  1. 首先,让我们看看当前的Header组件是什么样子的:
 import React, { Component } from 'react';
 import PropTypes from 'prop-types';
 import logo from '../../images/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;

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

  1. 第一件事是将我们的类组件转换为箭头函数,通过这个改变,我们不再需要导入React.Component。迁移的第二部分是将 props 作为函数的参数传递,而不是从this.props获取它们,最后一步是将我们的静态propTypes移动为函数的一个节点。在这些改变之后,我们的代码应该是这样的:
 import React from 'react';
 import PropTypes from 'prop-types';
 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>
      </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.js 功能组件相当于只有渲染方法。这就是为什么我们只需要直接返回 JSX。

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

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

 export default Footer;

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

文件:src/shared/components/layout/Footer.js 在这种情况下,正如您所看到的,我们需要创建一个没有参数的箭头函数(因为我们没有任何 props),并直接返回我们需要渲染的 JSX。

  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;

文件:src/shared/components/layout/Content.js

  1. 这个组件类似于我们的Header组件。我们需要将 props 作为参数传递并保留我们的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;

文件: src/shared/components/layout/Content.js

它是如何工作的...

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

还有更多...

一个功能组件不仅没有状态,而且也没有 React 生命周期方法。

理解 React 生命周期方法

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

如何做...

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

待办事项列表 - 实现 ComponentWillMount

在这个示例中,您将学习有关 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;

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

  1. 构造函数:构造函数是一个在对象初始化之前执行的独特方法。构造函数可以使用super关键字调用父类(超类)的构造函数。这个方法用于初始化我们的本地状态或绑定我们的方法。对于待办事项列表,我们需要在构造函数中用一些值初始化本地状态或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)。第二部分是组件列表,我们将在其中显示我们的待办事项列表(或任务列表),传递items数组和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 数组中找到特定的任务,将节点修改为已完成,然后更新本地状态:
 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;

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

文件:src/components/Todo/List.js

  1. 每当我们使用.map函数从数组中渲染多个 React 元素时,我们必须为我们创建的每个项目添加 key 属性。否则,我们将收到类似于此的 React 警告:

  1. 您可能已经注意到,我们还包含了一些 Font Awesome 图标,并且为了使其工作,我们需要将 Font Awesome 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>

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

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

文件:src/components/App.js

  1. 如果您正确地遵循了所有说明,您应该看到 Todo 列表如下所示:
  • 具有默认任务的初始状态:

  • 添加新任务:

  • 写下任务标题,然后按Enter

  • 标记任务为完成:

  • 删除任务:

我挑战您使用localStorage保存任务,而不是使用componentWillMount定义默认任务。

番茄工作法计时器 - 实现构造函数和 componentDidMount

要理解componentDidMount,我们将创建一个番茄工作法计时器(如果您不知道它是什么,可以阅读这篇文章:en.wikipedia.org/wiki/Pomodoro_Technique)。

我们的番茄工作法计时器将如下所示:

创建我们的番茄工作法计时器:

  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;

文件:src/components/Pomodoro/Timer.js

  1. 对于我们的番茄工作法计时器,我们需要在构造函数中使用一些值初始化我们的本地状态,用于时间和警报(当时间结束时):
 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. 在将默认时间定义为我们的时间状态之后,让我们看看我们需要如何渲染番茄钟计时器。我们的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事件上执行不同的方法。

  2. 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变量更新了我们的本地状态,我们将其作为参数传递(可以是 1,500 秒=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;

文件:src/components/Pomodoro/Timer.js

  1. 在我们完成了组件之后,最后一步是添加我们的样式。这是用于番茄钟计时器的 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;
}

文件:src/components/Pomodoro/Timer.css

不要忘记将<Timer />组件导入到App.js中。如果你按照一切正确的步骤进行,你应该会看到番茄钟计时器的工作方式如下:

  • 工作中:

  • 短时间休息:

  • 长时间休息:

  • 嗡嗡声 - 时间到了!:

我向你挑战,添加一个播放、暂停和重置按钮来控制计时器。

加密货币兑换器 - 实现 shouldComponentUpdate

今天,每个人都在谈论比特币、以太坊、瑞波和其他加密货币。让我们创建我们自己的加密货币兑换器,来学习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;

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

  1. 每当用户在输入框中输入内容并将值转换为数字时,我们都会更新我们的 dollars 状态,但是如果你运行这段代码,你可能会注意到,当你输入小于 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;
  }

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

笔记 - 实现 componentWillReceiveProps 和 componentWillUnmount

在这个例子中,我们将创建一个简单的笔记列表,在这个列表中,每 10 秒,我们将模拟从服务接收到新数据的更新,并且通过componentWillReceiveProps,我们将记录我们上次从服务器接收到更新的时间:

  1. componentWillReceiveProps方法在渲染之前被调用。像shouldComponentUpdate一样,每当新的 props 被传递给组件,或者状态发生改变时,它都会被调用。在这个例子中,我们需要创建假数据,但数据通常需要来自实际的服务:
 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'
    }
  ];

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

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

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

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

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

文件:src/index.js

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

  1. 点击按钮后,您将看到这个:

C3.js 图表 - 实现 componentDidUpdate

C3.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" />

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

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

  1. 正如您所看到的,我们在componentDidUpdate上执行updateChart方法,每当用户从App.js接收新的 props 时都会执行。让我们在我们的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;

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

文件:src/components/Chart.css

  1. 在这种情况下,我们创建了一些图表来显示有关当今最重要的加密货币(BTC - 比特币,ETH - 以太坊和 XRP - 瑞波)的信息。它应该是这样的:

这张图片让你了解折线图是什么样子的

  1. 我们有两个按钮可以在图表类型之间切换(柱状图或折线图)。如果我们点击 BAR,我们应该看到这个图表:

这张图片让你了解柱状图是什么样子的。

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

基本动画 - 实现 componentWillUpdate

在这个例子中,我们将学习如何使用componentWillUpdate

  1. componentWillUpdate允许您在组件接收新的 props 或新的状态之前对组件进行操作。它通常用于动画。让我们创建一个基本的动画(淡入/淡出)来看如何使用它:
 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;

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

  1. 正如您所看到的,我们正在用newState验证show state,并观察它是 true。然后我们添加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;
}

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

  1. 如果您运行应用程序,您将看到这个视图:

  1. 在您点击按钮之后,您将看到一个文本淡出的动画,红色的 div 将被扩展,给您这个结果:

它是如何工作的...

正如你可以看到所有这些例子,React 生命周期方法被用来处理我们应用程序中的不同场景。在第五章,精通 Redux中,我们将看到如何实现 Redux 以及生命周期方法如何与 Redux 状态一起工作。

理解 React Pure Components

许多人对函数组件和纯组件之间的区别感到困惑。他们中的大多数人认为它们是相同的,但这是不正确的。当我们使用纯组件时,我们需要从 React 中导入 PureComponent

 import React, { PureComponent } from 'react';

如果你的 React 组件的 render 方法是“纯”的(这意味着它根据相同的 props 和 state 渲染相同的结果),你可以使用这个函数来提高你的应用程序的性能。纯组件对 props 和 nextProps 对象以及 state 和 nextState 对象进行浅比较。纯组件不包括 shouldComponentUpdate(nextProps, nextState) 方法,如果我们尝试添加它,我们将收到来自 React 的警告。

在这个示例中,我们将创建一个基本的例子来理解 Pure Components 是如何工作的。

准备工作

对于这个示例,我们需要安装 Chrome 扩展 React Developer Tools 来在我们的应用程序中进行简单的调试。在第十二章,测试和调试中,我们将深入探讨这个主题。

你可以从chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi下载 React Developer Tools。

如何做…

我们将创建一个组件,其中我们将对输入的所有数字进行求和。我们可以从最后的一些示例中开始:

  1. 首先,我们要做的是修改我们的 App.js 并包括 Numbers 组件:
    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;

文件:src/components/App.js

  1. 现在我们将创建 Numbers 组件:
    // 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;

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

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

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

    export default Result;

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

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

它是如何工作的…

如果你运行应用程序,你会看到这个:

正如你所看到的,我们使用的是类型为数字的输入,这意味着如果你开始输入数字(1,然后 2,然后 3,等等),你将看到每一行的求和结果(0 + 1 = 1,1 + 2 = 3,3 + 3 = 6)。

对你来说,这可能看起来很简单,但是如果我们使用 React 开发者工具来检查应用程序,我们需要启用“高亮更新”选项。

之后,快速在输入框中输入多个数字,你将看到 React 正在执行的所有渲染。

正如你所看到的,React 正在进行大量的渲染。当高亮显示为红色时,意味着该组件的性能不佳。这就是 Pure Components 将帮助我们的地方;让我们将我们的 Result 组件迁移到成为 Pure Component:

    import React, { PureComponent } from 'react';

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

    export default Result;

文件:src/components/Numbers/Result.js

现在,如果我们尝试对数字做同样的事情,让我们看看有什么不同。

正如你所看到的,使用 Pure Component React 与 Class Component 相比,渲染次数较少。也许现在你认为,如果我们使用无状态组件而不是 Pure Component,结果将是相同的。不幸的是,这不会发生;如果你想验证这一点,让我们再次更改 Result 组件,并将其转换为 Functional Component。:

    import React from 'react';

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

    export default Result;

文件:src/components/Numbers/Result.js

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

正如你所看到的,结果与 Class Component 相同,这意味着并非始终使用无状态组件必然会帮助我们改善应用程序的性能。如果你认为某些组件是纯粹的,请考虑将它们转换为 Pure components。

在 React 中防止 XSS 漏洞

在这个示例中,我们将学习有关 React 中跨站脚本(XSS)漏洞的知识。XSS 攻击在 Web 应用程序中很常见,一些开发人员仍然不知道这一点。XSS 攻击是恶意脚本,被注入到未受保护的 Web 应用程序的 DOM 中。每个应用程序的风险都可能不同。它可能只是一个无害的警报脚本注入,或者更糟糕的是,有人可以访问你的 Cookie 并窃取你的私人凭据(密码)。

让我们创建一个 XSS 组件,开始玩一下 XSS 攻击。我们将有一个模拟来自真实服务器的响应的变量,并且我们将模拟使用 Redux 的初始状态(我们将在第五章中看到 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;

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

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

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

  1. 这很有趣,也许您认为“我的博客文章 2”的内容会在浏览器中触发警报,但实际上并没有。如果我们检查代码,警报脚本是存在的。

  1. 即使我们使用dangerouslySetInnerHTML,React 也会保护我们免受恶意脚本注入的影响,但这对我们来说还不够安全,我们不能放松对网站安全性的警惕。现在让我们看看“我的博客文章 3”的内容存在的问题。代码<img onmouseover="alert('This site is not secure');" src="attack.jpg" />并不直接使用<script>标签来注入恶意代码,而是使用了一个带有事件(onmouseover)的img标签。因此,如果您对 React 的保护感到满意,我们可以看到,如果我们将鼠标移到图像上,这个 XSS 攻击将被执行:

  1. 删除 XSS 攻击:这有点可怕,对吧?但正如我在本教程开头所说的,有一种安全的方法来使用dangerouslySetInnerHTML,是的,正如你现在可能在想的那样,我们需要在使用dangerouslySetInnerHTML渲染之前清理我们的恶意脚本。下一个脚本将负责删除<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;

文件: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. 我们需要使用serialize而不是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;

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

它是如何工作的...

正如你所看到的,XSS 攻击是很普遍的,许多网站都在不知情的情况下遭受这种问题。还有其他的注入攻击,比如 SQL 注入,在 API 中如果我们不采取最低限度的安全预防措施,可能会发生。

还有更多...

以下是一些安全建议:

  • 始终对来自表单的用户内容进行清理。

  • 始终使用serialize而不是JSON.stringify

  • 只在绝对必要时使用dangerouslySetInnerHTML

  • 为你的组件编写单元测试,并尝试覆盖所有可能的 XSS 攻击(我们将在第十二章中看到单元测试,测试和调试)。

  • 始终使用sha1md5加密密码,并不要忘记添加盐值(例如,如果密码是abc123,那么你的盐可以像这样加密:sha1(md5('$4ltT3xt_abc123'))

  • 如果你使用 cookie 来存储敏感信息(主要是个人信息和密码),你可以使用 Base64 保存 cookie 以混淆数据。

  • 将 API 添加一些保护(安全令牌),除非你需要公开。在第八章《使用 Node.js 和 MongoDB 以及 MySQL 创建 API》中有关安全令牌的方法。

第三章:处理事件,绑定和有用的 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 开发(https://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 太棒了!

第六章:使用 Redux Form 创建表单

在本章中,将涵盖以下配方:

  • 使用本地状态创建受控表单

  • 使用 Redux Form 构建表单

  • 在表单中实现验证

介绍

表单是任何 Web 应用程序的基本部分,在接下来的配方中,我们将学习如何使用带有或不带有 Redux Form 的表单。

使用本地状态创建受控表单

对于这个配方,我们将创建一个简单的待办事项列表,使用我们的本地状态来使用表单。

准备工作

对于这个配方,我们需要安装uuid包来生成随机 ID,如下面的代码所示:

npm install uuid

如何做…

让我们按照以下步骤创建我们的受控表单:

  1. 首先,对于待办事项列表,我们将在src/components/Todo/index.jsx中创建一个名为Todo的新组件。我们将使用的框架如下所示:
import React, { Component } from 'react';
import uuidv4 from 'uuid/v4';
import './Todo.css';

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

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

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

        <form onSubmit={this.handleOnSubmit}>
          <input value={this.state.task} />
        </form>
      </div>
    );
  }
}

export default Todo;

文件:src/components/Todo/index.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 Todo from './components/Todo';
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 path="/todo" component={Todo} exact />
      <Route component={Error404} />
    </Switch>
  </App>
);

export default AppRoutes;

文件:src/routes.jsx

  1. 如果你去/todo,你会看到输入框,但你可能会注意到无法在上面写任何东西,这是因为我们将我们的本地状态(this.state.task)连接到我们的输入值,但我们需要一个onChange函数来更新我们的状态,如下面的代码所示:
import React, { Component } from 'react';
import uuidv4 from 'uuid/v4';
import './Todo.css';

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

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

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

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

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

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

export default Todo;

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

  1. 现在我们可以在输入框中写任何东西,如下截图所示:

  1. 为了保存输入的项目,我们需要在form标签中添加一个onSubmit函数,在这里我们需要更新我们的本地状态以将项目推送到items数组中。此外,我们需要包括我们的List组件,我们将在其中显示所有的项目。完整的代码如下:
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: []
    };
  }

  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 
    // task to the items array.
    this.setState({
      task: '',
      items: [
        ...this.state.items,
        {
          id: uuidv4(),
          task: this.state.task,
          complete: false
        }
      ]
    });
  }

  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} />
      </div>
    );
  }
}

export default Todo;

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

  1. 我们的List组件将是一个功能性组件,我们将在其中渲染一个项目列表,如下面的代码所示:
import React from 'react';

const List = props => (
  <ul>
    {props.items.map((item, key) => (
      <li key={key}>
        {item.task}
      </li>
    ))}
  </ul>
);

export default List;

文件:src/components/Todo/List.jsx

  1. 最后,我们需要添加我们的 CSS 文件,如下面的代码所示:
.Todo {
    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%;
}

.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;
    position: relative;
}

.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%;
}

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

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

  1. 我们的待办事项列表将如下截图所示:

它是如何工作的…

正如您所看到的,从输入表单中检索值的唯一方法是使用本地状态和onChange函数来更新输入的值。如果您将状态连接到输入值,但没有添加onChange回调来更新它,您将无法写入任何内容,因为虚拟 DOM 没有被更新,唯一的方法是通过更新我们的本地状态。

使用 Redux Form 构建表单

Redux Form通常用于大型表单或步骤表单,因为它具有 Redux 状态来保持整个表单的值。此外,Redux Form 很方便地验证数据并有效地处理提交。

准备工作

对于这个示例,我们需要安装 Redux Form 如下:

 npm install redux-form

如何做到...

对于这个示例,我们将使用 Redux Form 来创建相同的 Todo 列表:

  1. 安装了 Redux Form 之后,我们需要对上一个示例的代码进行一些修改以实现 Redux Form。我们需要做的第一件事是为我们的表单添加一个 reducer。为此,我们需要从redux-form中导入一个 reducer,并且我们可以将变量的名称更改为formReducer以更明确,然后将 reducer 作为一个表单添加到我们的combineReducers中,如下面的代码所示:
// Dependencies
import { combineReducers } from 'redux';
import { reducer as formReducer } from 'redux-form';

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

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

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

export default rootReducer;

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

  1. 通常,我们使用 Redux Form 创建的所有表单都需要它们的组件,这意味着我们需要创建一个组件来处理我们的 Todo 表单。由于我们需要在Todo文件夹中创建一个名为TodoForm.jsx的文件,我们组件的代码如下:
import React, { Component } from 'react';
import { Field, reduxForm } from 'redux-form';

class TodoForm extends Component {
  // Functional component to render an input...
  renderInput = ({ input }) => <input {...input} type="text" />;

 // This function is useful to handle our 
  onSubmit = values => {
    const { addTask, dispatch, reset } = this.props;

    // Resetting our form, this will clear our input...
    dispatch(reset('todo'));  

    // Executing our addTask method and
    // passing the form values.
    addTask(values);
  }

  render() {
    // handleSubmit is part of Redux Form 
    // to handle the onSubmit event
    const { handleSubmit } = this.props;

    return (
      <form onSubmit={handleSubmit(this.onSubmit)}>
        {/* Field is a Redux Form Component, we need to pass the 
         name of the input and the component we are using to 
         render it */}
        <Field 
          name="task" 
          component={this.renderInput} 
        />
      </form>
    )
  }
}

// With this we named our form reducer for this specific form (todo).  
export default reduxForm({
  form: 'todo'
})(TodoForm);

文件:src/components/Todo/TodoForm.jsx

  1. Redux Form 包含许多有用的 props 来处理我们表单中的数据。我标记了我们将在此示例中使用的 props(addTask是从另一个组件传递的 prop,因此它不是 Redux Form 的一部分),您可以通过在渲染方法中添加控制台来查看所有的 props console.log(this.props);,如下面的截图所示:

  1. 现在让我们修改我们的Todo组件,包括TodoForm组件,并通过我们的addTask方法接收数据,如下面的代码所示:
import React, { Component } from 'react';
import uuidv4 from 'uuid/v4';
import List from './List';
import TodoForm from './TodoForm';
import './Todo.css';
class Todo extends Component {
  constructor() {
    super();

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

  addTask = values => {
    // This values are coming from our 
    // onSubmit method in our TodoForm.
    const { task } = values;

    this.setState({
      items: [
        ...this.state.items,
        {
          id: uuidv4(),
          task,
          complete: false
        }
      ]
    });
  }

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

        <TodoForm addTask={this.addTask} />
        <List items={this.state.items} />
      </div>
    );
  }
}
export default Todo;

它是如何工作的...

如您所见,Redux Form 很容易实现:

  1. 在第一步中,我们将 Redux Form reducer 连接到了我们的 store

  2. 在第二步中,我们创建了TodoForm组件,在这里我们渲染了表单字段,将表单 reducer 连接到 store,并将值发送回addTask回调函数

  3. 在最后一步中,我们呈现了我们的TodoForm并发送了addTask回调,该回调处理任务值并将其插入到本地状态中

最后,我们将看到与上一个示例相同的结果,但现在我们使用 Redux Form,如下面的屏幕截图所示:

在表单中实现验证

我们 Redux Form 实现的最后部分是验证。使用上一个示例,让我们添加对输入任务的验证。

如何做...

在任何表单中都需要验证,因此让我们为我们的字段添加一些验证:

  1. 首先,我们需要修改我们的TodoForm.jsx,并且我们需要创建一个validate函数,在这个函数中,我们需要验证我们的任务是否不为空。然后,我们需要创建一个renderError方法,以便在尝试添加空任务时呈现我们的错误消息,如下面的代码所示:
import React, { Component } from 'react';
import { Field, reduxForm } from 'redux-form';
import './TodoForm.css';

class TodoForm extends Component {
  renderInput = ({ input }) => <input {...input} type="text" />;

  onSubmit = values => {
    const { addTask, dispatch, reset } = this.props;

    // Resetting our form...
    dispatch(reset('todo'));

    addTask(values);
  }

  renderError(field) {
    const { meta: { submitFailed, error } } = field;

    if (submitFailed && error) {
      return (
        <div className="error">
          {error}
        </div>
      );
    }

    return null;
  }

  render() {
    const { handleSubmit, submitting } = this.props;

    return (
      <form onSubmit={handleSubmit(this.onSubmit)}>
        <Field name="task" component={this.renderInput} />
        <Field name="task" component={this.renderError} />
      </form>
    );
  }
}

const validate = values => {
  const errors = {};

  if (!values.task) {
    errors.task = 'Task cannot be empty!';
  }

  return errors;
}

export default reduxForm({
  validate,
  form: 'todo'
})(TodoForm);

文件:src/components/Todo/TodoForm.jsx

  1. 接下来,我们需要创建一个TodoForm.css来为我们的错误消息添加一些样式,如下面的代码所示:
.error {
  color: red;
  font-size: small;
  margin-top: 10px;
}

文件:src/components/Todo/TodoForm.css

它是如何工作的...

如果我们尝试添加一个没有任何值的新任务,并按Enter提交表单,我们将看到以下屏幕截图中显示的视图:

第七章:使用 React 进行动画

在本章中,将涵盖以下食谱:

  • 使用 ReactCSSTransitionGroup 对待事项列表进行动画处理

  • 使用 react-animations 库

  • 使用 React pose 创建我们的第一个动画

介绍

动画在任何 Web 应用程序中都很常见。自 CSS3 以来,动画已经变得普遍且易于实现。动画最常见的用途是过渡,您可以在其中更改 CSS 属性并定义持续时间或延迟。React 可以使用名为ReactCSSTransitionGroup的动画插件来处理动画。在接下来的食谱中,我们将使用ReactCSSTransitionGroup来创建一些动画。ReactCSSTransitionGroup是一个用于平滑实现基本 CSS 动画和过渡的附加组件。

使用 ReactCSSTransitionGroup 对待事项列表进行动画处理

在这个食谱中,我们将使用ReactCSSTransitionGroup来对待事项列表进行动画处理。

准备就绪

对于这个食谱,我们需要安装react-addons-css-transition-group包:

npm install react-addons-css-transition-group

如何做到这一点...

我们将制作一个带有一些动画效果的待办事项列表:

  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: 'Default Task 1',
          completed: false
        },
        {
          id: uuidv4(),
          task: 'Default Task 2',
          completed: true
        },
        {
          id: uuidv4(),
          task: 'Default Task 3',
          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 submited we reset the task value and we push the 
    // new task to the items array.
    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;

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

  1. 现在,在我们的List组件中,我们需要包括ReactCSSTransitionGroup并将其用作列表元素的包装器。我们需要使用transitionName属性指定我们的过渡名称,transitionAppear在第一次动画挂载时添加过渡。默认情况下为false
import React from 'react';
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
import './List.css';

const List = props => (
  <ul>
    <ReactCSSTransitionGroup 
 transitionName="todo" 
 transitionAppear={true}
    >
      {props.items.map((item, key) => (
        <li 
          key={key} 
          className={`${item.completed ? 'completed' : 'pending'}`}
        >
          {item.task}

          <div className="actions">
            <span 
              className={item.completed ? 'hide' : 'done'} 
              onClick={() => props.markAsCompleted(item.id)}
            >
              <i className="fa fa-check"></i>
            </span>

            <span 
              className="trash" 
              onClick={() => props.removeTask(item.id)}
            >
              <i className="fa fa-trash"></i>
            </span>
          </div>
        </li>
      ))}
    </ReactCSSTransitionGroup>
  </ul>
);

export default List;

文件:src/components/Todo/List.jsx

  1. 现在,使用transitionName,我们将使用由ReactCSSTransitionGroup创建的特殊类添加一些样式:
.todo-enter {
    opacity: 0.01;
}

.todo-enter.todo-enter-active {
    opacity: 1;
    transition: opacity 0.5s ease;
}

.todo-leave {
    opacity: 1;
}

.todo-leave.todo-leave-active {
    opacity: 0.01;
    transition: opacity .5s ease-in;
}

.todo-appear {
    opacity: 0.01;
    transition: opacity .5s ease-in;
}

.todo-appear.todo-appear-active {
    opacity: 1;
}

文件:src/components/Todo/List.css

它是如何工作的...

我们需要在ReactCSSTransitionGroup组件内包含我们想要动画的元素。每当我们向待办事项列表中添加一个项目时,我们可以看到我们的特殊类(.todo-enter.todo-enter-active)被注入一秒钟,以启动我们的动画:

如果我们删除一个项目,我们将看到.todo-leave.todo-leave-active类持续一秒钟:

正如你所看到的,使用ReactCSSTransitionGroup帮助我们处理动画的状态。您可以使用它来在您的 React 应用程序中创建更好的动画。

使用 react-animations 库

在这个食谱中,我们将学习如何使用库 react-animations。

准备就绪

对于这个食谱,我们需要安装以下软件包:

npm install react-animations radium

如何做...

让我们做一些动画:

  1. 我们需要使用Radium来创建内联样式,以便使用react-animations包中的动画。首先,让我们创建我们的组件:
import React, { Component } from 'react';
import { fadeIn } from 'react-animations';
import Radium, { StyleRoot } from 'radium';

const styles = {
  fadeIn: {
    animation: 'x 1s',
    animationName: Radium.keyframes(fadeIn, 'fadeIn')
  }
};

class Animations extends Component {
  render() {
    return (
      <StyleRoot>
        <div className="Animations" style={styles.fadeIn}>
          <h1>This text will be animated</h1>
        </div>
      </StyleRoot>
    );
  }
}

export default Animations;

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

  1. 在这个例子中,我们使用了fadeIn动画。我们需要从react-animations中导入我们想要使用的动画,将动画添加到我们的Radium样式中,然后使用<StyleRoot>作为我们动画的包装器,最后指定内联样式fadeIn

  2. 如果你想使用另一个动画,例如bounce,那么你需要添加弹跳动画并为其创建一个样式:

import React, { Component } from 'react';
import { fadeIn, bounce } from 'react-animations';
import Radium, { StyleRoot } from 'radium';

const styles = {
  fadeIn: {
    animation: 'x 1s',
    animationName: Radium.keyframes(fadeIn, 'fadeIn')
  },
  bounce: {
    animation: 'x 1s',
    animationName: Radium.keyframes(bounce, 'bounce')
  }
};

class Animations extends Component {
  render() {
    return (
      <StyleRoot>
        <div className="Animations" style={styles.bounce}>
          <h1>This text will be animated</h1>
        </div>
      </StyleRoot>
    );
  }
}

export default Animations;

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

还有更多...

正如你所看到的,使用react-animations中的动画非常容易。还有很多其他动画:

  • 弹跳

  • 淡入

  • 淡出

  • 闪光

  • 翻转

  • 滚入

  • 滚出

  • 淡入旋转

  • 旋转出

  • 橡皮筋舞蹈

  • 摇动

  • 摇摆

  • 放大

  • 缩小

要查看所有可用的动画,请访问官方存储库github.com/FormidableLabs/react-animations

使用 React Pose 创建我们的第一个动画

React Pose 是用于 HTML、SVG 和 React 的声明式动作系统。这是一个非常酷的库,你可以用它在 React 中做出惊人的动画。

准备工作

对于这个示例,我们需要安装以下软件包并更新我们的reactreact-dom16.4.2或更高版本:

 npm install react react-dom react-pose styled-components

如何做...

按照以下步骤创建 React pose 动画:

  1. 首先,让我们创建我们的组件结构:
import React, { Component } from 'react';
import posed from 'react-pose';
import styled from 'styled-components';
import './Animations.css';

class Animations extends Component {
  render() {
    return (
      <div class="Animations">

      </div>
    );
  }
}

export default Animations;

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

  1. 我们需要做的第二件事是创建我们的第一个 posed div,并创建一个使用styled-components的样式化div,并具有我们动画的状态(normalhover):
import React, { Component } from 'react';
import posed from 'react-pose';
import styled from 'styled-components';
import './Animations.css';

// Creating our posed div
const Circle = posed.div({
  normal: {
    scale: 1 // Normal state
  },
  hover: {
    scale: 3 // Hover state
  }
});

// Creating styled component
const StyledCircle = styled(Circle)`
  color: white;
  cursor: pointer;
  background: blue;
  line-height: 80px;
  border-radius: 50%;
  height: 80px;
  width: 80px;
`;

class Animations extends Component {
  render() {
    return (
      <div class="Animations">

      </div>
    );
  }
}

export default Animations;

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

  1. 现在我们需要将我们的StyledCircle组件添加到我们的render方法中:
      render() {
        return (
          <div class="Animations">
            <StyledCircle
              pose={this.state.hover ? 'hover' : 'normal'}
              onMouseEnter={this.handleMouseEnter}
              onMouseLeave={this.handleMouseLeave}
              onClick={this.handleClick}
              style={{ background: this.state.bg }}
            >
              Click me!
            </StyledCircle>
          </div>
        );
      }

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

  1. 正如你所看到的,我们需要创建一些事件方法,并且当用户点击时,我们将使用本地状态来改变圆圈的大小和颜色:
import React, { Component } from 'react';
import posed from 'react-pose';
import styled from 'styled-components';
import './Animations.css';

const Circle = posed.div({
  normal: {
    scale: 1 // Normal state
  },
  hover: {
    scale: 3 // Hover state
  }
});

// Creating styled component
const StyledCircle = styled(Circle)`
  color: white;
  cursor: pointer;
  background: blue;
  line-height: 80px;
  border-radius: 50%;
  height: 80px;
  width: 80px;
`;

class Animations extends Component {
  state = {
    bg: 'blue',
    hover: false
  };

  handleMouseEnter = () => {
    this.setState({
      hover: true
    });
  }

  handleMouseLeave = () => {
    this.setState({
      hover: false
    });
  }

  handleClick = () => {
    // Choosing a random color...
    const colors = ['red', 'green', 'gray', 'orange', 'black', 'pink'];

    this.setState({
      bg: colors[Math.floor(Math.random() * colors.length)]
    });
  }

  render() {
    return (
      <div class="Animations">
        <StyledCircle
          pose={this.state.hover ? 'hover' : 'normal'}
          onMouseEnter={this.handleMouseEnter}
          onMouseLeave={this.handleMouseLeave}
          onClick={this.handleClick}
          style={{ background: this.state.bg }}
        >
          Click me!
        </StyledCircle>
      </div>
    );
  }
}

export default Animations;

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

它是如何工作的...

我们的第一个视图将是一个带有标签“点击我!”的蓝色圆圈:

如果我们悬停在圆圈上,我们将看到 pose 动画,它会增加圆圈的比例:

最后,如果我们点击圆圈,我们会看到我们的圆圈会随机改变背景颜色:

还有更多...

我们甚至可以结合react-animations库中的动画。例如,如果我们想要在用户点击圆圈时翻转它,那么我们可以这样做:

import React, { Component } from 'react';
import posed from 'react-pose';
import styled, { keyframes } from 'styled-components';
import { flip } from 'react-animations';
import './Animations.css';

const flipAnimation = keyframes`${flip}`;

const Circle = posed.div({
  normal: {
    scale: 1 // Normal state
  },
  hover: {
    scale: 3 // Hover state
  }
});

// Creating styled component
const StyledCircle = styled(Circle)`
  color: white;
  cursor: pointer;
  background: blue;
  line-height: 80px;
  border-radius: 50%;
  height: 80px;
  width: 80px;
`;

class Animations extends Component {
  state = {
    style: {
      background: 'blue'
    },
    hover: false
  };

  handleMouseEnter = () => {
    this.setState({
      hover: true
    });
  }

  handleMouseLeave = () => {
    this.setState({
      hover: false
    });
  }

  handleClick = () => {
    // Choosing a random color...
    const colors = ['red', 'green', 'gray', 'orange', 'black', 'pink'];

    this.setState({
      style: {
        animation: `1s ${flipAnimation}`,
        background: colors[Math.floor(Math.random() * colors.length)]
      }
    });
  }

  render() {
    return (
      <div className="Animations">
        <StyledCircle
          pose={this.state.hover ? 'hover' : 'normal'}
          onMouseEnter={this.handleMouseEnter}
          onMouseLeave={this.handleMouseLeave}
          onClick={this.handleClick}
          style={this.state.style}
         >
           Click me!
         </StyledCircle>
       </div>
    );
  }
}

export default Animations;

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

第八章:使用 Node.js 和 MongoDB、MySQL 创建 API

在本章中,将涵盖以下配方:

  • 使用 Express 创建一个基本的 API

  • 用 MongoDB 构建数据库

  • 用 MySQL 构建数据库

  • 添加访问令牌以保护我们的 API

介绍

从 Node.js 官方网站(nodejs.org):

Node.js 是建立在 Chrome 的 V8 JavaScript 引擎上的 JavaScript 运行时。Node.js 使用事件驱动的、非阻塞的 I/O 模型,使其轻量高效。Node.js 的包生态系统 npm 是世界上最大的开源库生态系统。

Node.js 广泛用作 Web 应用程序的后端,因为它很容易创建 API,并且其性能优于 Java、PHP 或 Ruby 等技术。通常,使用 Node.js 最流行的方式是使用一个叫做 Express 的框架。

来自 Express 官方网站(expressjs.com):

Express 是一个最小化和灵活的 Node.js Web 应用程序框架,为 Web 和移动应用程序提供了强大的功能集。

使用 Express 创建一个基本的 API

Express 是最流行的 Node.js 框架,安装和使用都很容易。在这个配方中,我们将使用 Express 创建、配置和安装一个基本的 API。

准备工作

首先,我们需要安装 Node。你需要去官方网站www.nodejs.org,然后下载 Node.js。有两个版本:LTS(长期支持)版本和当前版本,具有最新功能。在我看来,选择 LTS 版本总是更好,但这取决于你。

一旦你安装了 Node,你可以在终端中运行这个命令来检查你的版本:

node -v
v10.8.0

此外,Node 默认包含 Node Package Manager (npm)。你可以用这个命令检查你的版本:

npm -v
6.3.0

现在我们需要安装 Express。为了做到这一点,有一个叫做express-generator的包,它将允许我们用一个简单的命令创建一个 Express 应用程序。我们需要全局安装它:

npm install -g express-generator

在我们安装了express-generator之后,我们可以创建一个 Express 应用程序。我通常喜欢在我的 Mac 的主文件夹内创建一个名为projects的目录,或者如果你使用 Windows,你可以在C:\projects下创建它:

express my-first-express-app

一旦你运行了这个命令,你会看到类似这样的东西:

如果你按照运行应用程序的说明,你会看到 Express 应用程序在http://localhost:3000上运行:

 cd my-first-express-app
 npm install
npm start 

你会看到这个视图:

如何做到这一点...

默认情况下由express-generator生成的代码是 ES5 代码,使用varrequiremodule.exports等:

  1. 我们需要做的第一件事是将这段代码转换为 ES6。为了做到这一点,让我们首先修改我们的app.js文件。这是该文件的原始代码:
  var createError = require('http-errors');
  var express = require('express');
  var path = require('path');
  var cookieParser = require('cookie-parser');
  var logger = require('morgan');

  var indexRouter = require('./routes/index');
  var usersRouter = require('./routes/users');

  var app = express();

  // view engine setup
  app.set('views', path.join(__dirname, 'views'));
  app.set('view engine', 'jade');

  app.use(logger('dev'));
  app.use(express.json());
  app.use(express.urlencoded({ extended: false }));
  app.use(cookieParser());
  app.use(express.static(path.join(__dirname, 'public')));

  app.use('/', indexRouter);
  app.use('/users', usersRouter);

  // catch 404 and forward to error handler
  app.use(function(req, res, next) {
    next(createError(404));
  });

  // error handler
  app.use(function(err, req, res, next) {
    // set locals, only providing error in development
    res.locals.message = err.message;
    res.locals.error = req.app.get('env') === 'development' ? err : {};

    // render the error page
    res.status(err.status || 500);
    res.render('error');
  });

 module.exports = app;

文件:app.js

  1. 迁移到 ES6,我们应该有这段代码:
  import createError from 'http-errors';
  import express from 'express';
  import path from 'path';
  import cookieParser from 'cookie-parser';
  import logger from 'morgan';

  import indexRouter from './routes/index';
  import usersRouter from './routes/users';

  const app = express();

  // view engine setup
  app.set('views', path.join(__dirname, 'views'));
  app.set('view engine', 'jade');

  app.use(logger('dev'));
  app.use(express.json());
  app.use(express.urlencoded({ extended: false }));
  app.use(cookieParser());
  app.use(express.static(path.join(__dirname, 'public')));

  app.use('/', indexRouter);
  app.use('/users', usersRouter);

  // catch 404 and forward to error handler
  app.use((req, res, next) => {
    next(createError(404));
  });

  // error handler
  app.use((err, req, res, next) => {
    // set locals, only providing error in development
    res.locals.message = err.message;
    res.locals.error = req.app.get('env') === 'development' ? err : {};

    // render the error page
    res.status(err.status || 500);
    res.render('error');
  });

  // Listening port
  app.listen(3000);

文件:app.js

  1. 现在让我们删除我们的bin/www目录,因为我们在文件末尾添加了app.listen(3000);,然后你需要修改package.json中的start脚本:
  "scripts": {
    "start": "node app.js"
  }

文件:package.json

  1. 如果你尝试用npm start运行你的应用程序,你会得到这个错误:

  1. 这个错误是因为我们的 ES6 代码不能直接与 Node 一起工作。我们需要使用 Babel 来编译我们的文件,以便能够编写 ES6 代码。为此,我们需要全局安装babel-cli,还需要安装babel-preset-es2015包:
    npm install -g babel-cli
 npm install babel-preset-es2015
  1. 为了使它工作,我们需要创建一个名为.babelrc的新文件,并添加我们的es2015预设:
    {
      "presets": ["es2015"]
    }

文件:.babelrc

  1. 现在你需要再次修改你的start脚本,并将node切换为babel-node
  "scripts": {
    "start": "babel-node app.js"
  }

文件:package.json

  1. 如果你在终端中运行npm start,你现在应该能够运行应用程序了。

  2. 当我们将代码更改为 ES6 后,我们遇到了另一个问题。如果你修改一个文件并在应用程序中保存它,它不会刷新。而且,如果由于某种原因我们的应用程序崩溃,那么我们的服务器将停止工作。解决这个问题的方法是使用一个 Node 监视器。最流行的是nodemon

    npm install nodemon
  1. 你需要修改你的start脚本:
  "scripts": {
    "start": "nodemon app.js --exec babel-node"
  }

文件:package.json

  1. 现在,如果你对应用程序进行任何更改(例如,在routes/index.js文件中,你可以更改第 6 行的Express文本为任何其他内容),你会看到服务器如何重新启动并刷新网站:

  1. 正如你所看到的,绿色的第一条消息说starting babel-node app.js,然后当它检测到变化时,它会说restarting due to changes...现在我们可以看到我们网站上的变化:

  1. 因为我们的 Express 应用程序是为了成为 API 而不是常规网站而创建的,所以我们需要删除许多多余的东西,比如views文件夹和模板引擎,我们需要进行一些结构上的改变,以便更容易处理。让我们看看我们的app.js文件现在是什么样子的:
  // Dependencies
  import express from 'express';
  import path from 'path';

  // Controllers
  import apiController from './controllers/api';

  // Express Application
  const app = express();

  // Middlewares
  app.use(express.json());
  app.use(express.urlencoded({ extended: false }));

  // Routes
  app.use('/api', apiController);

  // Listening port
  app.listen(3000);

文件:app.js

  1. 如您所见,我将routes目录重命名为controllers,并且我还删除了该目录中的users.js文件,并将index.js重命名为api.js。让我们创建一个处理博客的 API:
  import express from 'express';

  const router = express.Router();

  // Mock data, this should come from a database....
  const posts = [
    {
      id: 1,
      title: 'My blog post 1',
      content: '<p>Content</p>',
      author: 'Carlos Santana'
    },
    {
      id: 2,
      title: 'My blog post 2',
      content: '<p>Content</p>',
      author: 'Cristina Rojas'
    },
    {
      id: 3,
      title: 'My blog post 3',
      content: '<p>Content</p>',
      author: 'Carlos Santana'
    }
  ];

  router.get('/', (req, res, next) => {
    res.send(`
      <p>API Endpoints:</p>
      <ul>
        <li>/api/posts</li>
        <li>/api/post/:id</li>
      </ul>
    `);
  });

  router.get('/posts', (req, res, next) => {
    res.json({
      response: posts
    });
  });

  router.get('/post/:id', (req, res, next) => {
    const { params: { id } } = req;

    const singlePost = posts.find(post => post.id === Number(id));

    if (!singlePost) {
      res.send({
        error: true,
        message: 'Post not found'
      });
    }

    res.json({
      response: [singlePost]
    });
  });

 export default router;

文件:controllers/api.js

它是如何工作的...

现在让我们测试我们的新 API:

  1. 如果我们转到http://localhost:3000/api,我们将显示一个端点列表。这是可选的,但对开发人员来说是一个有用的参考:

  1. 如果您转到http://localhost:3000/api/posts,您将看到所有的帖子:

  1. 另外,如果您访问http://localhost:3000/api/post/1,您将得到列表中的第一篇帖子:

  1. 最后,如果您尝试获取我们数据中不存在的帖子(http://localhost:3000/api/post/99),那么我们将返回一个错误:

使用 MongoDB 构建数据库

MongoDB 是最流行的 NoSQL 数据库。它是免费的(开源)和面向文档的。在这个教程中,我们将安装 MongoDB,创建一个数据库,创建一个文档,并插入一些数据,以便使用 Node.js 和 Mongoose 库显示信息。

准备就绪

首先,我们需要安装 MongoDB。在这个教程中,我将向您展示使用 Mac 安装它的最简单的方法,并且我会给您一些链接,以便在 Linux 或 Windows 上安装它。

来自 MongoDB 官方文档docs.mongodb.com/manual/tutorial/install-mongodb-on-os-x“从 3.0 版本开始,MongoDB 只支持 MacOS 版本 10.7(狮子)及更高版本的 Intel x86-64。”

手动安装 MongoDB 社区版(困难的方法)

这个安装适用于 Mac 和 Linux:

  1. www.mongodb.com/download-center#community下载您想要的 MongoDB 版本的二进制文件。

  2. 从下载的文件中提取文件;您可以使用终端并使用以下命令:

    tar -zxvf mongodb-osx-ssl-x86_64-3.6.3.tgz
  1. 将提取的文件夹复制到 MongoDB 将运行的位置:
    mkdir -p mongodb
 cp -R -n mongodb-osx-ssl-x86_64-3.6.3/ mongodb
  1. 确保二进制文件的位置在PATH变量中。您可以在您的 shell 的rc文件中添加以下行,比如~/.bashrc~/.bash_profile
    export PATH=<your-mongodb-install-directory>/bin:$PATH 

使用 Homebrew 安装 MongoDB 社区版(简单方法)

Homebrew 是 Mac 的软件包管理器(也被称为 macOS 的缺失软件包管理器),并且很容易安装。前往官方网站(https://brew.sh),在那里您会找到一个您应该运行以安装它的命令,如下所示:

 /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
  1. 如果您已经安装了 Homebrew,或者刚刚安装了它,那么您需要做的第一件事就是使用以下命令更新软件包数据库:
    brew update
  1. 现在我们需要使用以下命令安装 MongoDB:
 brew install mongodb
  1. 如果您想要安装 MongoDB 的最新开发版本,那么您应该运行此命令(我不建议这样做,因为它可能有一些尚未修复的错误,但这取决于您):
    brew install mongodb --devel

运行 MongoDB

在我们第一次启动 MongoDB 之前,我们需要创建一个目录,mongod进程将在其中写入数据:

  1. 默认情况下,mongod 进程使用/data/db目录。要创建此文件夹,您可以使用以下命令:
    mkdir -p /data/db
  1. 现在我们需要为数据目录设置权限:
    chmod -R 777 /data
  1. 在一个新的终端(或选项卡)中,您需要运行以下命令:
    mongod
  1. 如果您没有遇到错误,您可以在与mongod相同的主机机器上启动 Mongo shell(在新的终端或选项卡中):
    mongo --host 127.0.0.1:127017

如果您遇到这样的错误:Error: Port number 127017 out of range parsing HostAndPort from "127.0.0.1:127017",那么只需运行mongo而不带--host标志。

  1. 最后,如果您想要停止 MongoDB,在运行mongod的终端中按下Ctrl + C

  2. 如果一切正常,您应该在终端中看到这个:

如何做...

首先,我们需要创建一个新的数据库:

  1. 要创建一个新的数据库或切换到现有的数据库,您需要运行:use <name of the database>。让我们创建一个博客数据库:
    use blog
  1. 现在我们需要创建一个名为posts的集合,并且您需要直接以 JSON 格式保存数据,使用db.<your-collection-name>.save({})命令:
   db.posts.save({ title: 'Post 1', slug: 'post-1', content: '<p>Content</p>' })
  1. 如您所见,我没有添加任何id值,这是因为 MongoDB 会自动为每一行创建一个名为_id的唯一 ID,这是一个随机哈希。如果您想要查看刚刚保存的数据,您需要使用不带任何参数的find()方法:
   db.posts.find()
  1. 您应该看到您的数据如下所示:

  1. 现在假设您添加了一个新的帖子 2 行,并且您想通过指定 slug(post-2)找到该特定行。您可以这样做:
   db.posts.find({ slug: 'post-2' })
  1. 您应该看到这个:

  1. 现在让我们将帖子 2 的标题更改为 My Updated Post 2。为此,我们需要按照以下方式更新我们的行:
   db.posts.update({ slug: "post-2" }, { $set: { title: "My Updated Post 2" }})
  1. 第一个参数是查询要更新的行,第二个参数使用$set修改字段。

  2. 最后,如果我们想要删除特定的行,可以按照以下步骤进行:

   db.posts.remove({ "_id": ObjectId("5ad2e6ed4fa0d047639da616") })
  1. 删除行的推荐方法是直接指定_id以避免错误删除其他行,但也可以通过任何其他字段删除行。例如,假设您想使用 slug 删除帖子 1。您可以这样做:
   db.posts.remove({ "slug": "post-1" })
  1. 现在您已经学会了如何使用 MongoDB 进行基本操作,让我们使用 Mongoose 库将 MongoDB 实现到 Node.js 中,这是一个对象文档映射器ODM)。我们需要为此示例安装一些额外的包:
 npm install mongoose body-parser slug
  1. 使用与上一个示例相同的代码(Repository: Chapter08/Recipe1/my-first-express-app),我们将连接 Mongoose 到 Node.js。我们需要做的第一件事是修改app.js
  // Dependencies
  import express from 'express';
  import path from 'path';
  import mongoose from 'mongoose';
  import bodyParser from 'body-parser';

  // Controllers
  import apiController from './controllers/api';

  // Express Application
  const app = express();

  // Middlewares
  app.use(bodyParser.json());
  app.use(bodyParser.urlencoded({ extended: false }));

  // Mongoose Connection (blog is our database)
  mongoose.connect('mongodb://localhost/blog');

  // Routes
  app.use('/api', apiController);

  // Listening port
  app.listen(3000);

文件:app.js

  1. 现在我们已经将 Mongoose 连接到我们的数据库,我们需要创建一个模型来处理我们的博客帖子。为此,您需要创建一个src/models/blog.js文件:
// Dependencies
import mongoose, { Schema } from 'mongoose';
import slug from 'slug';

// Defining the post schema...
const postSchema = new Schema({
  title: String,
  slug: { type: String, unique: true },
  content: { type: String, required: true },
  author: String,
  createdAt: Date
});

// Adding a custom method...
postSchema.methods.addAuthor = function(author) {
 /**
 * NOTE: Probably you are thinking, why I'm using function 
   * and not an arrow function?
 * Is because arrow functions does not bind their own context
 * that means this actually refers to the originating context
 */
  this.author = author;

  return this.author;
};
//Before save we create the slug and we add the current date...
postSchema.pre('save', function(next) {
  this.slug = slug(this.title, { lower: 'on' });
  this.createdAt = Date.now();

  next();
});

// Creating our Model...
const Post = mongoose.model('Post', postSchema);

export default Post;

文件:src/models/blog.js

  1. 现在,为了处理我们的模型,我们需要创建一个新的控制器(src/controllers/blog.js),在那里我们将添加保存、更新、删除、查找所有帖子或查找单个帖子的方法:
  // Dependencies
  import slugFn from 'slug';
  import Post from '../models/blog';

 export function createPost(title, content, callback) {
    // Creating a new post...
    const newPost = new Post({
      title,
      content
    });

    // Adding the post author...
    newPost.addAuthor('Carlos Santana');

    // Saving the post into the database...
    newPost.save(error => {
      if (error) {
        console.log(error);
        callback(error, true);
      }

      console.log('Post saved correctly!');
      callback(newPost);
    });
  }

  // Updating a post...
  export function updatePost(slug, title, content, callback) {
    const updatedPost = {
      title,
      content,
      slug: slugFn(title, { lower: 'on' })
    };

    Post.update({ slug }, updatedPost, (error, affected) => {
      if (error) {
        console.log(error);
        callback(error, true);
      }

      console.log('Post updated correctly!');
      callback(affected);
    });
  }

  // Removing a post by slug...
  export function removePost(slug, callback) {
    Post.remove({ slug }, error => {
      if (error) {
        console.log(error);
        callback(error, true);
      }

      console.log('Post removed correctly!');
      callback(true);
    });
  }

  // Find all posts...
  export function findAllPosts(callback) {
    Post.find({}, (error, posts) => {
      if (error) {
        console.log(error);

        return false;
      }

      console.log(posts);
      callback(posts);
    });
  }

  // Find a single post by slug...
  export function findBySlug(slug, callback) {
    Post.find({ slug }, (error, post) => {
      if (error) {
        console.log(error);

        return false;
      }

      console.log(post);
      callback(post);
    });
  }

文件:src/controllers/blog.js

  1. 最后,我们将修改我们的 API 控制器(src/controllers/api.js)以删除我们在上一个示例中创建的虚假数据,并从实际的 MongoDB 数据库获取数据:
  import express from 'express';
  import {
    createPost,
    findAllPosts,
    findBySlug,
    removePost,
    updatePost
  } from './blog';

  const router = express.Router();

  // GET Endpoints
  router.get('/', (req, res, next) => {
    res.send(`
      <p>API Endpoints:</p>
      <ul>
        <li><a href="/api/posts">/api/posts</a></li>
        <li><a href="/api/post/1">/api/post/:id</a></li>
      </ul>
    `);
  });

  router.get('/posts', (req, res, next) => {
    findAllPosts(posts => {
      res.json({
        response: posts
      });
    });
  });

  router.get('/post/:slug', (req, res, next) => {
    const { params: { slug } } = req;

    findBySlug(slug, singlePost => {
      console.log('single', singlePost);
      if (!singlePost || singlePost.length === 0) {
        res.send({
          error: true,
          message: 'Post not found'
        });
      } else {
        res.json({
          response: [singlePost]
        });
      }
    });
  });

  // POST Endpoints
  router.post('/post', (req, res, next) => {
    const { title, content } = req.body;

    createPost(title, content, (data, error = false) => {
      if (error) {
        res.json({
          error: true,
          message: data
        });
      } else {
        res.json({
          response: {
            saved: true,
            post: data
          }
        });
      }
    });
  });

  // DELETE Endpoints
  router.delete('/post/:slug', (req, res, next) => {
    const { params: { slug } } = req;

    removePost(slug, (removed, error) => {
      if (error) {
        res.json({
          error: true,
          message: 'There was an error trying to remove this 
          post...'
        });
      } else {
        res.json({
          response: {
            removed: true
          }
        })
      }
    });
  });

  // PUT Endpoints
  router.put('/post/:slug', (req, res, next) => {
    const { params: { slug }, body: { title, content } } = req;

    updatePost(slug, title, content, (affected, error) => {
      if (error) {
        res.json({
          error: true,
          message: 'There was an error trying to update the post'
        });
      } else {
        res.json({
          response: {
            updated: true,
            affected
          }
        })
      }
    });
  });

  export default router;

文件:src/controllers/api.js

它是如何工作的...

您需要安装 Postman(www.getpostman.com)或任何其他 REST 客户端来测试 API。主要用于POSTPUTDELETE方法,GET方法可以在任何浏览器上轻松验证。

GET 方法端点

GET /posts。此端点可以在浏览器中测试。转到http://localhost:3000/api/posts。我手动插入了三行:

如果您想在 Postman 上进行测试,那么请写入相同的 URL(http://localhost:3000/api/posts),选择GET方法,并点击发送按钮:

GET /post/:slug。此端点也是GET,您需要在 URL 上传递 slug(友好的 URL)。例如,第一行的 slug,My blog post 1,是 my-blog-post-1。slug 是一个友好的 URL,其值与标题相同,但是小写,没有特殊字符,并且空格用破折号(-)替换。在我们的模型中,我们将 slug 定义为唯一字段。这意味着不能有多个具有相同 slug 的帖子。

让我们在浏览器中转到http://localhost:3000/api/post/my-blog-post-1。如果数据库中存在 slug,您将看到信息:

但是,如果您尝试查找数据库中不存在的 slug,您将收到此错误:

POST 方法端点

POST方法通常用于在数据库中插入新数据。

POST /post。对于此端点,我们需要使用 Postman 能够通过 body 发送数据。为此,您需要在 Postman 中选择 POST 方法。使用 URL http://localhost:3000/api/post,然后点击 Headers,并且您需要添加带有值application/x-www-form-urlencoded的标头Content-Type

设置标头后,转到 Body 选项卡并选择原始选项,然后可以发送以下信息:

现在,您可以点击发送按钮并查看服务返回的响应:

如果您做的一切正确,您应该收到一个响应,其中保存的节点设置为 true,post节点包含有关保存的帖子的信息。现在,如果您尝试再次使用相同的数据(相同的标题)点击发送按钮,它将导致错误,因为您记得,我们的 slug 必须是唯一的:

您可能想知道__v是什么,如果我们没有直接添加该节点。那就是versionKey,它是 Mongoose 在每个文档第一次创建时设置的属性。该键的值包含文档的内部修订版本。您可以更改或删除此文档属性的名称。默认值为__v

如果要更改它,可以在定义新模式时执行以下操作:

    // If you want to change the name of the versionKey
    new Schema({...}, { versionKey: '_myVersion' });

或者,如果您想要删除它,可以将versionKey传递为false,但我不建议这样做,因为每次更新文档时,您都无法控制版本更改:

    // If you want to remove it you can do:
    new Schema({...}, { versionKey: false });

DELETE 方法端点

DELETE方法,顾名思义,是用于删除数据库中的行。

DELETE /post/:slug。在 Postman 中,我们需要选择DELETE方法,并在 URL 中传递要删除的帖子的 slug。例如,让我们删除帖子 my-blog-post-2。如果您正确删除它,您应该会收到一个将删除的节点设置为 true 的响应:

如果您想要验证帖子是否已删除,可以再次转到/posts端点,您将看到它不再在 JSON 中:

PUT 方法端点

最后一个方法是PUT,通常用于更新数据库中的行。

PUT /post/:slug。在 Postman 中,您需要选择 PUT 方法,然后选择要编辑的帖子的 URL。让我们编辑 my-blog-post-3;URL 将是http://localhost:3000/api/post/my-blog-post-3。在标题选项卡中,就像在POST方法中一样,您需要添加一个值为 application/x-www-form-urlencoded 的Content-Type标题。在 Body 选项卡中,您发送要替换的新数据,例如新标题和新内容:

如果一切正常,您应该会收到这个响应:

同样,如果您想要验证帖子是否已正确更新,请转到浏览器中的/posts端点:

如您所见,帖子标题、内容和 slug 已正确更新。

使用 MySQL 构建数据库

MySQL 是最受欢迎的数据库。它是一个开源的关系数据库管理系统(RDBMS)。MySQL 通常是 LAMP(Linux,Apache,MySQL,PHP/Python/Perl)堆栈的核心组件;许多捆绑包都包括 MySQL:

其他开发人员更喜欢单独安装。如果你想这样做,你可以直接从官方网站下载 MySQL:dev.mysql.com/downloads/mysql/

在这个教程中,我将使用 MySQL Workbench 来执行 SQL 查询。你可以从www.mysql.com/products/workbench/下载它。随意使用任何其他 MySQL 管理员,或者如果你喜欢终端,你可以直接使用 MySQL 命令。

这里有更多的 MySQL GUI 工具:

准备工作

要在 Node 上使用 MySQL,我们需要安装 sequelize 和 mysql2 包:

    npm install sequelize **mysql2 slug**

如何做到...

  1. 我们需要做的第一件事是创建一个名为 blog 的数据库,并使用它:
CREATE DATABASE blog;
 USE blog;
  1. 既然我们的数据库准备好了,让我们来使用 Node.js 进行 MySQL 实现。有许多使用 MySQL 与 Node 的方法,但在这个教程中,我们将使用一个名为Sequelize的包,它是一个强大的 MySQL 和其他数据库(如 SQLite,Postgres 和 MsSQL)的 ORM。

  2. 我们需要做的第一件事是创建一个配置文件来添加我们的数据库配置(主机,数据库,用户,密码等)。为此,你需要创建一个名为config/index.js的文件:

  export default {
    db: {
      dialect: 'mysql', // 'mysql'|'sqlite'|'postgres'|'mssql'
      host: 'localhost', // Your host, by default is localhost
      database: 'blog', // Your database name
      user: 'root', // Your MySQL user, by default is root
      password: '123456' // Your Db password, sometimes by default                  
                         //is empty.
    }
  };

文件:config/index.js

  1. 我们可以重复使用在 MongoDB 教程中使用的相同 API 控制器:
  import express from 'express';
  import {
    createPost,
    findAllPosts,
    findBySlug,
    removePost,
    updatePost
  } from './blog';

  const router = express.Router();

 // GET Methods
  router.get('/', (req, res, next) => {
    res.send(`
      <p>API Endpoints:</p>
      <ul>
        <li><a href="/api/posts">/api/posts</a></li>
        <li><a href="/api/post/1">/api/post/:id</a></li>
      </ul>
    `);
  });

  router.get('/posts', (req, res, next) => {
    findAllPosts(posts => {
      res.json({
        response: posts
      });
    });
  });

  router.get('/post/:slug', (req, res, next) => {
    const { params: { slug } } = req;

    findBySlug(slug, singlePost => {
      console.log('single', singlePost);
      if (!singlePost || singlePost.length === 0) {
        res.send({
          error: true,
          message: 'Post not found'
        });
      } else {
        res.json({
          response: [singlePost]
        });
      }
    });
  });

  // POST Methods
  router.post('/post', (req, res, next) => {
    const { title, content } = req.body;

    createPost(title, content, (data, error = false) => {
      if (error) {
        res.json({
          error: true,
          details: error
        });
      } else {
        res.json({
          response: {
            saved: true,
            post: data
          }
        });
      }
    });
  });

  // DELETE Methods
  router.delete('/post/:slug', (req, res, next) => {
    const { params: { slug } } = req;

    removePost(slug, (removed, error) => {
      if (error) {
        res.json({
          error: true,
          message: 'There was an error trying to remove this post...'
        });
      } else {
        res.json({
          response: {
            removed: true
          }
        })
      }
    });
  });

  // PUT Methods
  router.put('/post/:slug', (req, res, next) => {
    const { params: { slug }, body: { title, content } } = req;

    updatePost(slug, title, content, (affected, error) => {
      if (error) {
        res.json({
          error: true,
          message: 'There was an error trying to update the post'
        });
      } else {
        res.json({
          response: {
            updated: true,
            affected
          }
        })
      }
    });
  });

  export default router;

文件:controllers/api.js

  1. 现在我们需要创建我们的 blog 模型(models/blog.js)。让我们分部分构建它;首先是连接到我们的数据库:
  // Dependencies
  import Sequelize from 'sequelize';
  import slug from 'slug';

  // Configuration
  import config from '../config';

  // Connecting to the database
  const db = new Sequelize(config.db.database, config.db.user, 
  config.db.password, {
    host: config.db.host,
    dialect: config.db.dialect,
    operatorsAliases: false
  });

文件:models/blog.js

  1. 在我们创建了数据库连接之后,让我们创建我们的 Post 模型。我们将创建一个名为 posts 的表,其中包含以下字段:idtitleslugcontentauthorcreatedAt,但是 Sequelize 默认情况下会在添加DATE字段时自动创建一个名为updatedAt的额外字段,每次更新行时都会更改:
  // This will remove the extra response
  const queryType = {
    type: Sequelize.QueryTypes.SELECT
  };

  // Defining our Post model...
  const Post = db.define('posts', {
    id: {
      type: Sequelize.INTEGER,
      autoIncrement: true,
      primaryKey: true
    },
    title: {
      type: Sequelize.STRING,
      allowNull: false,
      validate: {
        notEmpty: {
          msg: 'The title is empty',
        }
      }
    },
    slug: {
      type: Sequelize.STRING,
      allowNull: false,
      unique: true,
      validate: {
        notEmpty: {
          msg: 'The slug is empty',
        }
      }
    },
    content: {
      type: Sequelize.TEXT,
      allowNull: false,
      validate: {
        notEmpty: {
          msg: 'The content is empty'
        }
      }
    },
    author: {
      type: Sequelize.STRING,
      allowNull: false,
      validate: {
        notEmpty: {
          msg: 'Who is the author?',
        }
      }
    },
    createdAt: {
      type: Sequelize.DATE,
      defaultValue: Sequelize.NOW
    },
  });

文件:models/blog.js

  1. Sequelize 最酷的事情之一是,我们可以在字段为空时添加一个自定义消息的验证(notEmpty)。现在我们将添加一个方法来创建一个新的 post:
  // Creating new post...
 export function createPost(title, content, callback) {
    // .sync({ force: true }), if you pass force this will     
    // drop the table every time.
 db
      .sync()
      .then(() => {
        Post.create({
          title,
          slug: title ? slug(title, { lower: 'on' }) : '',
          content,
          author: 'Carlos Santana'
        }).then(insertedPost => {
          console.log(insertedPost);
          callback(insertedPost.dataValues);
        }).catch(error => {
          console.log(error);
          callback(false, error);
        });
      });
  }

文件:models/blog.js

  1. 现在我们需要一个方法来更新一个 post:
  // Updating a post...
  export function updatePost(slg, title, content, callback) {
    Post.update(
      {
        title,
        slug: slug(title, { lower: 'on' }),
        content
      },
      {
        where: { slug: slg }
      }
    ).then(rowsUpdated => {
      console.log('UPDATED', rowsUpdated);
      callback(rowsUpdated);
    }).catch(error => {
      console.log(error);
      callback(false, error);
    });
  }

文件:models/blog.js

  1. 此外,我们需要一种方法来通过其 slug 删除一个 post:
  // Removing a post by slug...
  export function removePost(slug, callback) {
    Post.destroy({
      where: {
        slug
      }
    }).then(rowDeleted => {
      console.log('DELETED', rowDeleted);
      callback(rowDeleted);
    }).catch(error => {
      console.log(error);
      callback(false, error);
    });
  }

文件:models/blog.js

  1. Sequelize 还支持直接的 SQL 查询。让我们创建两种方法,一种是通过 SQL 查询找到所有帖子,另一种是通过 slug 找到帖子:
 // Find all posts...
  export function findAllPosts(callback) {
    db.query('SELECT * FROM posts', queryType).then(data => {
      callback(data);
    });
  }

  // Find a single post by slug...
  export function findBySlug(slug, callback) {
    db.query(`SELECT * FROM posts WHERE slug = '${slug}'`, queryType).then(data => {
      callback(data);
    });
  }

文件:models/blog.js

  1. 我们在文件开头定义的queryType变量是为了避免从 Sequelize 获得第二个响应。默认情况下,如果您不传递此queryType,Sequelize 将以多维数组的形式返回结果(第一个对象是结果,第二个对象是元数据对象)。让我们把所有的部分放在一起:
  // Dependencies
  import Sequelize from 'sequelize';
  import slug from 'slug';

  // Configuration
  import config from '../config';

  // Connecting to the database
  const db = new Sequelize(config.db.database, config.db.user, 
  config.db.password, {
    host: config.db.host,
    dialect: config.db.dialect,
    operatorsAliases: false // This is to avoid the warning:       
   //sequelize 
   //deprecated String based operators are now deprecated.
  });

  // This will remove the extra metadata object
  const queryType = {
    type: Sequelize.QueryTypes.SELECT
  };

  // Defining our Post model...
  const Post = db.define('posts', {
    id: {
      type: Sequelize.INTEGER,
      autoIncrement: true,
      primaryKey: true
    },
    title: {
      type: Sequelize.STRING,
      allowNull: false,
      validate: {
        notEmpty: {
          msg: 'The title is empty',
        }
      }
    },
    slug: {
      type: Sequelize.STRING,
      allowNull: false,
      unique: true,
      validate: {
        notEmpty: {
          msg: 'The slug is empty',
        }
      }
    },
    content: {
      type: Sequelize.TEXT,
      allowNull: false,
      validate: {
        notEmpty: {
          msg: 'The content is empty'
        }
      }
    },
    author: {
      type: Sequelize.STRING,
      allowNull: false,
      validate: {
        notEmpty: {
          msg: 'Who is the author?',
        }
      }
    },
    createdAt: {
      type: Sequelize.DATE,
      defaultValue: Sequelize.NOW
    },
  });

  // Creating new post...
  export function createPost(title, content, callback) {
    db
      .sync()
      .then(() => {
        Post.create({
          title,
          slug: title ? slug(title, { lower: 'on' }) : '',
          content,
          author: 'Carlos Santana'
        }).then(insertedPost => {
          console.log(insertedPost);
          callback(insertedPost.dataValues);
        }).catch((error) => {
          console.log(error);
          callback(false, error);
        });
      });
  }

  // Updating a post...
 export function updatePost(slg, title, content, callback) {
    Post.update(
      {
        title,
        slug: slug(title, { lower: 'on' }),
        content
      },
      {
        where: { slug: slg }
      }
    ).then(rowsUpdated => {
      console.log('UPDATED', rowsUpdated);
      callback(rowsUpdated);
    }).catch(error => {
      console.log(error);
      callback(false, error);
    });
  }

  // Removing a post by slug...
  export function removePost(slug, callback) {
    Post.destroy({
      where: {
        slug
      }
    }).then(rowDeleted => {
      console.log('DELETED', rowDeleted);
      callback(rowDeleted);
    }).catch(error => {
      console.log(error);
      callback(false, error);
    });
  }

  // Find all posts...
  export function findAllPosts(callback) {
    db.query('SELECT * FROM posts', queryType).then(data => {
      callback(data);
    });
  }

  // Find a single post by slug...
  export function findBySlug(slug, callback) {
    db.query(`SELECT * FROM posts WHERE slug = '${slug}'`, queryType).then(data => {
      callback(data);
    });
  }

文件:models/blog.js

工作原理...

它将以与 MongoDB 配方相同的方式工作,只是结果中存在细微差异。要测试 API,您需要安装 Postman(www.getpostman.com)。

POST 方法端点

POST 方法通常用于在数据库中插入新数据。

POST /post. 对于此端点,我们需要使用 Postman 通过请求正文发送数据。为此,您需要在 Postman 中选择 POST 方法。输入 URL http://localhost:3000/api/post,然后点击 Headers,您需要添加一个值为application/x-www-form-urlencodedContent-Type头:

设置头之后,转到Body选项卡并选择raw选项,您可以像这样发送信息:

现在您可以点击发送按钮并查看服务返回的响应:

如果您做的一切正确,您应该会收到一个响应,其中保存的node设置为true,并且帖子的node中包含有关保存的帖子的信息。如果您尝试再次点击发送按钮并使用相同的数据(相同的标题),它将会导致错误,因为您记得,我们的 slug 必须是唯一的:

此图像中的文本与本文无关。图像的目的是让您一睹错误的样子。在您的 Postman 中尝试,您将看到与图像相同的错误。

GET 方法端点

GET /posts。此端点可以在浏览器中进行测试。转到http://localhost:3000/api/posts。我已经手动使用createPost方法插入了三行:

如果您想在 Postman 上进行测试,请写入相同的 URL(http://localhost:3000/api/posts),选择GET方法,然后点击发送按钮:

GET /post/:slug

此端点也是一个 GET,您需要在 URL 中传递 slug(友好的 URL)。例如,第一行的 slug,我的博客文章 1,是 my-blog-post-1。slug 是一个友好的 URL,其值与标题相同,但是小写,没有特殊字符,并且空格用破折号(-)替换。在我们的模型中,我们将 slug 定义为唯一字段,这意味着不能有多个具有相同 slug 的帖子。

让我们在浏览器中转到http://localhost:3000/api/post/my-blog-post-1。如果数据库中存在 slug,您将看到以下信息:

但是,如果尝试查看数据库中不存在的 slug,您将收到此错误:

DELETE 方法端点

DELETE方法,顾名思义,用于删除数据库中的行。

DELETE /post/:slug。在 Postman 中,我们需要选择DELETE方法,并在 URL 中传递要删除的帖子的 slug。例如,让我们删除 my-blog-post-2。如果删除成功删除,您应该收到一个带有已删除节点值为 true 的响应:

如果要验证帖子是否已删除,可以再次转到/posts端点,您将看到它不再在 JSON 中:

PUT 方法端点

最后一种方法是PUT,通常用于更新数据库中的一行。

PUT /post/:slug

在 Postman 中,您首先需要选择 PUT 方法,然后选择要编辑的帖子的 URL。让我们编辑 my-blog-post-3;因此 URL 将是http://localhost:3000/api/post/my-blog-post-3。在标题选项卡中,您需要添加与 POST 方法相同的Content-Type标题,值为application/x-www-form-urlencoded。最后一部分是 Body 选项卡,您可以在其中发送要替换的新数据,例如新标题和新内容:

如果一切正常,您应该收到此响应:

同样,如果要验证帖子是否已正确更新,请在浏览器中转到/posts端点:

如您所见,帖子标题、内容和 slug 已正确更新。

添加访问令牌以保护我们的 API

我们在最后两个示例中创建的 API 是公开的。这意味着每个人都可以访问并从我们的服务器获取信息,但如果您想在 API 上添加安全层并获取平台上注册用户的信息,该怎么办?我们需要添加访问令牌验证来保护我们的 API,为此,我们必须使用JSON Web TokensJWT)。

准备工作

对于这个示例,您需要为 Node.js 安装 JWT:

    npm install jsonwebtoken

操作步骤...

我们将主要使用我们为 MySQL 示例创建的相同代码,并添加一个安全层来验证我们的访问令牌:

  1. 我们需要做的第一件事是修改我们的配置文件(config/index.js),添加一个安全节点,其中包含我们将用来创建令牌的secretKey,并添加令牌的过期时间:
 export default {
    db: {
      dialect: 'mysql', // The database engine you want to use
      host: 'localhost', // Your host, by default is localhost
      database: 'blog', // Your database name
      user: 'root', // Your MySQL user, by default is root
      password: '123456' // Your MySQL password
    },
    security: {
      secretKey: 'C0d3j0bs', // Secret key
      expiresIn: '1h' // Expiration can be: 30s, 30m, 1h, 7d, etc.
    }
  };

文件:config/index.js

  1. 下一步是在我们模型文件夹中创建一个db.js文件,以分离我们的数据库连接并在我们的模型之间共享它。以前,我们只有博客模型,但现在我们也要创建一个用户模型文件:
  // Configuration
  import config from '../config';
  import Sequelize from 'sequelize';

  export const db = new Sequelize(
    config.db.database, 
    config.db.user,
    config.db.password, 
    {
      host: config.db.host,
      dialect: config.db.dialect,
      operatorsAliases: false
    }
  );

文件:models/db.js

  1. 现在我们需要为用户创建一个表,并保存用户的记录:
    CREATE TABLE users (
      id int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
      username varchar(255) NOT NULL,
      password varchar(255) NOT NULL,
      email varchar(255) NOT NULL,
      fullName varchar(255) NOT NULL,
      PRIMARY KEY (`id`)
    );
  1. 我们可以使用这个命令插入一个用户,更改用户名和密码。在这个示例中,我们将使用 SHA1 算法来加密我们的密码:
 INSERT INTO users (id, username, password, email, fullName) 
    VALUES (
      NULL, 
      'czantany', 
 SHA1('123456'), 
      'carlos@milkzoft.com', 
      'Carlos Santana'
    );

    // The SHA1 hash generated for the 123456 password is      
 // 7c4a8d09ca3762af61e59520943dc26494f8941b
  1. 在我们创建了用户表并有了注册用户之后,让我们创建我们的用户模型,其中包含一个login方法:
  // Dependencies
  import Sequelize from 'sequelize';

  // Db Connection
  import { db } from './db';

  // This will remove the extra response
  const queryType = {
    type: Sequelize.QueryTypes.SELECT
  };

  // Login
  export function login(username, password, callback) {
    db.query(`
      SELECT id, username, email, fullName
      FROM users
      WHERE username = '${username}' AND password = '${password}'
    `, queryType).then(data => callback(data));
  }

文件:models/user.js

  1. 下一步是修改我们的 API 控制器,添加一个login端点来生成我们的令牌,并添加一个验证令牌的函数。然后我们将保护我们的一个端点(/api/posts):
  // Dependencies
  import express from 'express';
  import jwt from 'jsonwebtoken';

  // Models
  import {
    createPost,
    findAllPosts,
    findBySlug,
    removePost,
    updatePost
  } from '../models/blog';
  import { login } from '../models/user';

  // Configuration
  import config from '../config';

  // Extracting the secretKey and the expiresIn
  const { security: { secretKey, expiresIn } } = config;

  const router = express.Router();

  // Token Validation
  const validateToken = (req, res, next) => {
    if (req.headers['access-token']) {
      // The token should come as 'Bearer <access-token>'
      req.accessToken = req.headers['access-token'].split(' ')[1];

 // We just need the token that's why we split the string by       
     //space 
      // and we got the token in the position 1 of the array 
      //generated 
      // by the split method.
      return next();
    } else {
      res.status(403).send({ 
 error: 'You must send an access-token header...'
      });
    }
  }

  // POST login - This will generate a new token
  router.post('/login', (req, res) => {
    const { username, password } = req.body;

    login(username, password, data => {
      if (Object.keys(data).length === 0) {
        res.status(403).send({ error: 'Invalid login' });
      }

      // Creating the token with the 
      // user data + secretKey + expiration time
      jwt.sign({ data }, secretKey, { expiresIn }, (error, 
 accessToken) => {
        res.json({
          accessToken
        });
      });
    });
  });

  // We pass validateToken as middleware and then we verify with   
  //  req.accessToken
    router.get('/posts', validateToken, (req, res, next) => {
      jwt.verify(req.accessToken, secretKey, (error, userData) => {
        if (error) {
          console.log(error);
          res.status(403).send({ error: 'Invalid token' });
        } else {
          findAllPosts(posts => {
            res.json({
              response: posts,
              user: userData
            });
          });
        }
      });
    });

    // From here all the others endpoints are public...
    router.get('/post/:slug', (req, res, next) => {
      const { params: { slug } } = req;

      findBySlug(slug, singlePost => {
        console.log('single', singlePost);
        if (!singlePost || singlePost.length === 0) {
          res.send({
            error: true,
            message: 'Post not found'
          });
        } else {
          res.json({
            response: [singlePost]
          });
        }
      });
    });

 // POST Methods
    router.post('/post', (req, res, next) => {
      const { title, content } = req.body;

      createPost(title, content, (data, error = false) => {
        if (error) {
          res.json({
            error: true,
            details: error
          });
        } else {
          res.json({
            response: {
              saved: true,
              post: data
            }
          });
        }
      });
    });

    // DELETE Methods
    router.delete('/post/:slug', (req, res, next) => {
      const { params: { slug } } = req;

      removePost(slug, (removed, error) => {
        if (error) {
          res.json({
            error: true,
            message: 'There was an error trying to remove this 
            post...'
          });
        } else {
          res.json({
            response: {
              removed: true
            }
          });
        }
      });
    });

    // PUT Methods
    router.put('/post/:slug', (req, res, next) => {
      const { params: { slug }, body: { title, content } } = req;

      updatePost(slug, title, content, (affected, error) => {
        if (error) {
          res.json({
            error: true,
            message: 'There was an error trying to update the post'
          });
        } else {
          res.json({
            response: {
              updated: true,
              affected
            }
          });
        }
      });
    });

 export default router;

文件:controllers/api.js

工作原理...

如果你想测试 API 的安全性,首先需要执行POST /api/login方法来获取一个新的令牌。和以前一样,我们可以使用 Postman 来做这个。

您需要选择 POST 方法,然后编写 URL http://localhost:3000/api/login,并添加一个Content-Type头,值为application/x-www-form-urlencoded,以便通过请求体发送数据:

然后,在Body选项卡上,我们需要发送我们的数据(用户名和密码),以及我们在数据库中拥有的用户信息。在这里,我们手动进行这个过程,但最终,这些信息应该来自您网站上的登录表单:

如果您为用户传递了正确的信息,您应该会得到accessToken,但如果由于某种原因登录失败或用户或密码不正确,您将收到类似于此的错误:

一旦您获得新的accessToken(请记住,此令牌仅在 1 小时内有效;在过期时间后,您将需要创建一个新的),您需要复制该令牌,然后将其作为标头(作为访问令牌的格式Bearer <access-token>)发送到我们的受保护端点(/api/posts)。

非常重要的是,您发送正确的格式,Bearer[空格]。请记住,我们使用空格来获取令牌。如果您一切都做对了,您应该会收到来自服务的响应,其中包含博客的帖子和用户信息(这可能在不同的端点,但在此示例中,我只是在此处添加了用户数据)。

如您所见,在用户数据中,我们从数据库获取信息,还有两个新字段:iat(发行时间)和exp(令牌过期)。但如果我们的令牌过期或用户发送了不正确的访问令牌会发生什么呢?在这些情况下,我们将返回一个错误:

还有更多...

如您所见,令牌验证易于实现,并在处理私人数据时为我们的 API 增加了安全层。您可能会问最佳保存生成的访问令牌的位置在哪里。有些人将访问令牌保存在 cookie 或会话中,但我不建议这样做,因为存在一些相关的安全问题。我的建议是只在用户连接到网站时使用本地存储来保存它,然后在用户关闭浏览器后将其删除,但这将取决于您想要为平台添加的安全类型。

第九章:Apollo 和 GraphQL

在本章中,将涵盖以下配方:

  • 创建我们的第一个 GraphQL 服务器

  • 使用 Apollo 和 GraphQL 创建 Twitter 时间线

介绍

GraphQL 是一种可以与任何数据库一起使用的应用层查询语言。它也是开源的(MIT 许可证),由 Facebook 创建。它与 REST 的主要区别在于 GraphQL 不使用端点,而是使用查询,并且受大多数服务器语言支持,如 JavaScript(Node.js),Go,Ruby,PHP,Java 和 Python。

现在让我们来看看 GraphQL 和 REST 之间的主要区别。

GraphQL:

  • 查询可读

  • 您可以在不使用版本的情况下演变 API

  • 类型系统

  • 您可以避免进行多次往返以获取相关数据

  • 很容易限制我们需要的数据集

REST:

  • 在 REST 中,一切都是资源

  • REST 是无模式的

  • 您需要版本来演变 API

  • 很难限制我们需要的数据集

  • 如果您需要来自不同资源的数据,您需要进行多个请求

创建我们的第一个 GraphQL 服务器

对于这个配方,我们将创建一个联系人列表,其中我们将保存我们朋友的姓名,电话和电子邮件地址。

准备工作

我们需要做的第一件事是为我们的项目创建一个目录并初始化一个新的package.json文件,安装expressgraphqlexpress-graphql

 mkdir contacts-graphql
 cd contacts-graphql
 npm init --yes
 npm install express graphql express-graphql babel-preset-env
 npm install -g babel-cli

我们需要安装babel-preset-envbabel-cli以在 Node 中使用 ES6 语法。此外,我们需要创建一个.babelrc文件:

  {
    "presets": ["env"]
  }

文件:.babelrc

如何做...

让我们创建我们的第一个 GraphQL 服务器:

  1. 首先,我们需要为我们的 Express 服务器创建一个index.js文件:
  import express from 'express';

  const app = express();

  app.listen(3000, () => console.log('Running server on port 3000'));

文件:index.js

  1. 如果您在终端中运行babel-node index.js,您应该能够看到运行在端口 3000 上的节点服务器:

  1. 现在我们需要包含我们的express-graphql库,并从graphql导入buildSchema方法:
    import express from 'express';
    import expressGraphQL from 'express-graphql';
    import { buildSchema } from 'graphql';

    const app = express();

    app.listen(3000, () => console.log('Running server on port 3000'));

文件:index.js

  1. 一旦我们有了expressGraphQLbuildSchema,让我们用我们的第一个查询创建我们的第一个 GraphQL 服务器:
  // Dependencies
  import express from 'express';
  import expressGraphQL from 'express-graphql';
  import { buildSchema } from 'graphql';

  // Express Application
  const app = express();

  // Creating our GraphQL Schema
  const schema = buildSchema(`
    type Query {
      message: String
    }
  `);

  // Root has the methods we will execute to get the data
  const root = {
    message: () => 'First message'
  };

  // GraphQL middleware
  app.use('/graphql', expressGraphQL({
    schema,
    rootValue: root,
    graphiql: true // This enables the GraphQL browser's IDE
  }));

  // Running our server
  app.listen(3000, () => console.log('Running server on port 3000'));

文件:index.js

  1. 现在让我们为我们的联系人列表创建数据文件。我们可以创建一个 data 目录和一个contacts.json文件:
    {
      "contacts": [
        {
          "id": 1,
          "name": "Carlos Santana",
          "phone": "281-323-4146",
          "email": "carlos@milkzoft.com"
        },
        {
          "id": 2,
          "name": "Cristina",
          "phone": "331-251-5673",
          "email": "cristina@gmail.com"
        },
        {
          "id": 3,
          "name": "John Smith",
          "phone": "415-307-4382",
          "email": "john.smith@gmail.com"
        },
        {
          "id": 4,
          "name": "John Brown",
          "phone": "281-323-4146",
          "email": "john.brown@gmail.com"
        }
      ]
    }

文件:data/contacts.json

  1. 现在我们需要添加获取数据的方法(getContactgetContacts):
      // Dependencies
      import express from 'express';
      import expressGraphQL from 'express-graphql';
      import { buildSchema } from 'graphql';

      // Contacts Data
      import { contacts } from './data/contacts';

      // Express Application
      const app = express();

      // Creating our GraphQL Schema
      const schema = buildSchema(`
        type Query {
          contact(id: Int!): Contact
          contacts(name: String): [Contact]
        }

        type Contact {
          id: Int
          name: String
          phone: String
          email: String
        }
      `);

      // Data methods
      const methods = {
        getContact: args => {
          const { id } = args;

          return contacts.filter(contact => contact.id === id)[0];
        },
        getContacts: args => {
          const { name = false } = args;

          // If we don't get a name we return all contacts
          if (!name) {
            return contacts;
          }

          // Returning contacts with same name...
          return contacts.filter(
            contact => contact.name.includes(name)
          );
        }
      };

      // Root has the methods we will execute to get the data
      const root = {
        contact: methods.getContact,
        contacts: methods.getContacts
      };

      // GraphQL middleware
      app.use('/graphql', expressGraphQL({
        schema,
        rootValue: root,
        graphiql: true // This enables the GraphQL GUI
      }));

      // Runnign our server
      app.listen(3000, () => console.log('Running server on port 3000'));

文件:index.js

它是如何工作的...

如果你运行服务器并转到 URL http://localhost:3000/graphql,你将看到 GraphiQL IDE,并且默认情况下会有一个 message 查询,如果你点击播放按钮,你将观察到带有消息“First message”的数据:

现在在 GraphiQL IDE 中,我们需要创建一个查询,并为我们的contactId添加一个查询变量以获取单个联系人:

现在对于我们的getContacts查询,我们需要传递contactName变量:

正如你所看到的,如果我们发送John作为contactName,查询将返回我们拥有的两行名称为John SmithJohn Brown的联系人。此外,如果我们发送一个空值,我们将得到所有的联系人:

此外,我们可以开始使用 fragments,它们用于在queriesmutationssubscriptions之间共享字段:

正如你所看到的,我们用我们想要获取的字段定义了我们的 fragment,然后在两个查询(contact1contact2)中,我们重复使用了相同的 fragment(contactFields)。在查询变量中,我们传递了我们想要获取数据的联系人的值。

还有更多...

Mutation 也是必不可少的,因为它们帮助我们修改数据。让我们实现一个 mutation,并通过传递 ID 和我们想要更改的字段来更新一个联系人。

我们需要添加我们的 mutation 定义并创建更新联系人的函数;我们的代码应该如下所示:

  // Dependencies
  import express from 'express';
  import expressGraphQL from 'express-graphql';
  import { buildSchema } from 'graphql';

  // Contacts Data
  import { contacts } from './data/contacts';

  // Express Application
  const app = express();

  // Creating our GraphQL Schema
  const schema = buildSchema(`
    type Query {
      contact(id: Int!): Contact
      contacts(name: String): [Contact]
    }

    type Mutation {
      updateContact(
 id: Int!, 
 name: String!, 
 phone: String!, 
 email: String!
      ): Contact
    }

    type Contact {
      id: Int
      name: String
      phone: String
      email: String
    }
  `);

  // Data methods
  const methods = {
    getContact: args => {
      const { id } = args;

      return contacts.filter(contact => contact.id === id)[0];
    },
    getContacts: args => {
      const { name = false } = args;

      // If we don't get a name we return all contacts
      if (!name) {
        return contacts;
      }

      // Returning contacts with same name...
      return contacts.filter(contact => contact.name.includes(name));
    },
    updateContact: ({ id, name, phone, email }) => {
      contacts.forEach(contact => {
        if (contact.id === id) {
          // Updating only the fields that has new values...
          contact.name = name || contact.name;
          contact.phone = phone || contact.phone;
          contact.email = email || contact.email;
        }
      });

      return contacts.filter(contact => contact.id === id)[0];
    }
  };

  // Root has the methods we will execute to get the data
  const root = {
    contact: methods.getContact,
    contacts: methods.getContacts,
    updateContact: methods.updateContact
  };

  // GraphQL middleware
  app.use('/graphql', expressGraphQL({
    schema,
    rootValue: root,
    graphiql: true // This enables the GraphQL GUI
  }));

  // Running our server
  app.listen(3000, () => console.log('Running server on port 3000'));

文件:index.js

现在让我们在 GraphiQL 中创建我们的 mutation 并更新一个联系人:

使用 Apollo 和 GraphQL 创建 Twitter 时间线

Apollo 是一个用于 GraphQL 的开源基础设施。还有其他处理 GraphQL 的库,比如 Relay 和 Universal React Query Library (URQL)。这些库的主要问题是它们主要用于 React 应用程序,而 Apollo 可以与任何其他技术或框架一起使用。

准备工作

对于这个示例,我们将使用create-react-app创建一个新的 React 应用程序:

 create-react-app apollo

我们需要通过执行以下命令来弹出配置:

 npm run eject

eject命令将把react-scripts的所有配置带到你的本地项目中(Webpack 配置)。

现在我们需要安装以下包:

 npm install apollo-boost graphql graphql-tag moment mongoose react-
  apollo

我们还需要安装这些开发包:

 npm install --save-dev babel-preset-react babel-preset-stage-0

最后,我们需要创建我们的 tweet 模型:

 "resolutions": {
   "graphql": "0.13.2"
 }

此外,我们需要在我们的package.json中删除babel节点。

 "babel": {
   "presets": [
     "react-app"
   ]
 }

文件:package.json

然后,最后,我们需要创建一个.babelrc文件,其中包含以下内容:

 {
   "presets": ["react", "stage-0"]
 }

现在是真相的时刻!如果您正确地按照所有步骤进行了操作,您应该看到 GraphiQL IDE 在http://localhost:5000/graphiql上运行,但可能会出现以下错误:

您需要安装并运行 MongoDB 才能使用此项目。如果您不知道如何做到这一点,您可以查看第八章,使用 MongoDB 和 MySQL 创建 Node.js API

文件:.babelrc

让我们开始后端服务器:

  1. 首先,在apollo项目内(我们使用create-react-app创建的项目),我们需要创建一个名为backend的新目录,初始化一个package.json文件,并在src文件夹内创建:
 cd apollo
 mkdir backend
 cd backend
 npm init -y
 mkdir src
  1. 现在我们需要安装这些依赖项:
 npm install cors express express-graphql graphql graphql-tools 
      mongoose nodemon babel-preset-es2015

 npm install -g babel-cli
  1. 在我们的package.json文件中,我们需要修改我们的启动脚本以使用nodemon
      "scripts": {
        "start": "nodemon src/app.js --watch src --exec babel-node 
        --presets es2015"
      }

然后我们需要添加一个resolutions节点来指定我们将要使用的 GraphQL 的确切版本。这是为了避免版本冲突。当前版本的graphql0.13.2。当然,您需要在阅读本文时指定最新版本的 GraphqQL:

  1. 然后我们需要创建我们的app.js文件,在其中我们将创建我们的 GraphQL 中间件:
  // Dependencies
  import express from 'express';
  import expressGraphQL from 'express-graphql';
  import cors from 'cors';
  import graphQLExpress from 'express-graphql';
  import { makeExecutableSchema } from 'graphql-tools';

  // Query
  import { typeDefs } from './types/Query';
  import { resolvers } from './types/Resolvers';

  // Defining our schema with our typeDefs and resolvers
  const schema = makeExecutableSchema({
    typeDefs,
    resolvers
  });

  // Intializing our express app
  const app = express();

  // Using cors
  app.use(cors());

  // GraphQL Middleware
  app.use('/graphiql', graphQLExpress({
    schema,
    pretty: true,
    graphiql: true
  }));

  // Listening port 5000
  app.listen(5000);

  console.log('Server started on port 5000');

文件:src/app.js

  1. 正如你所看到的,我们已经从types文件夹中包含了我们的 typeDefs 和 resolvers,所以让我们创建这个目录并创建我们的 Query 文件:
 export const typeDefs = [`
    # Scalar Types (custom type)
    scalar DateTime

    # Tweet Type (should match our Mongo schema)
    type Tweet {
      _id: String
      tweet: String
      author: String
      createdAt: DateTime
    }

    # Query
    type Query {
      # This query will return a single Tweet
      getTweet(_id: String): Tweet 

      # This query will return an array of Tweets
      getTweets: [Tweet]
    }

    # Mutations
    type Mutation {
      # DateTime is a custom Type
      createTweet(
        tweet: String,
        author: String,
        createdAt: DateTime 
      ): Tweet

      # Mutation to delete a Tweet
      deleteTweet(_id: String): Tweet

      # Mutation to update a Tweet (! means mandatory).
      updateTweet(
        _id: String!,
        tweet: String!
      ): Tweet
    }

    # Schema
    schema {
      query: Query
      mutation: Mutation
    }
  `];

创建我们的 GraphQL 后端服务器

  1. 文件:src/types/Query.js
 // Dependencies
 import { GraphQLScalarType } from 'graphql';
 // TweetModel (Mongo Schema)
 import TweetModel from '../model/Tweet';
 // Resolvers
 export const resolvers = {
    Query: {
      // Receives an _id and returns a single Tweet.
      getTweet: _id => TweetModel.getTweet(_id),
      // Gets an array of Tweets.
      getTweets: () => TweetModel.getTweets()
    },
    Mutation: {
      // Creating a Tweet passing the args (Tweet object), the _ is    
      // the root normally is undefined
      createTweet: (_, args) => TweetModel.createTweet(args),
      // Deleting a Tweet passing in the args the _id of the Tweet 
      // we want to remove
      deleteTweet: (_, args) => TweetModel.deleteTweet(args),
      // Updating a Tweet passing the new values of the Tweet we 
      // want to update
      updateTweet: (_, args) => TweetModel.updateTweet(args)
    },
    // This DateTime will return the current date.
    DateTime: new GraphQLScalarType({
      name: 'DateTime',
      description: 'Date custom scalar type',
      parseValue: () => new Date(),
      serialize: value => value,
      parseLiteral: ast => ast.value
    })
  };

在我们跳到实际的配方之前,我们需要首先创建我们的 GraphQL 后端服务器,以创建我们完成这个项目所需的所有查询和变异。我们将在接下来的章节中看到如何做到这一点。

  1. 文件:src/types/Resolvers.js
  // Dependencies
  import mongoose from 'mongoose';

  // Connecting to Mongo
  mongoose.Promise = global.Promise;
  mongoose.connect('mongodb://localhost:27017/twitter', {
    useNewUrlParser: true
  });

  // Getting Mongoose Schema
  const Schema = mongoose.Schema;

  // Defining our Tweet schema
  const tweetSchema = new Schema({
    tweet: String,
    author: String,
    createdAt: Date,
  });

  // Creating our Model
  const TweetModel = mongoose.model('Tweet', tweetSchema);

  export default {
    // Getting all the tweets and sorting descending
    getTweets: () => TweetModel.find().sort({ _id: -1 }),
    // Getting a single Tweet using the _id
    getTweet: _id => TweetModel.findOne({ _id }),
    // Saving a Tweet
    createTweet: args => TweetModel(args).save(),
    // Removing a Tweet by _id
    deleteTweet: args => {
      const { _id } = args;

      TweetModel.remove({ _id }, error => {
        if (error) {
          console.log('Error Removing:', error);
        }
      });

      // Even when we removed a tweet we need to return the object 
      // of the tweet
      return args;
    },
    // Updating a Tweet (just the field tweet will be updated)
    updateTweet: args => {
      const { _id, tweet } = args;

      // Searching by _id and then update tweet field.
      TweetModel.update({ _id }, {
        $set: {
          tweet
        }
      },
      { upsert: true }, error => {
        if (error) {
          console.log('Error Updating:', error);
        }
      });

      // This is hard coded for now
      args.author = 'codejobs';
      args.createdAt = new Date();

      // Returning the updated Tweet 
      return args;
    }
  };

文件:src/model/Tweet.js

  1. 文件:package.json

在我们创建了 Query 文件之后,我们需要添加我们的 resolvers。这些是为每个查询和变异执行的函数。我们还将使用GraphQLScalarType定义我们的自定义DateTime类型:

  1. 通常,这个错误意味着我们在两个项目(前端和后端)中都在使用graphql,npm 不知道将使用哪个版本。这是一个棘手的错误,但我会告诉你如何修复它。首先,我们从我们的两个项目(前端和后端)中删除node_modules文件夹。然后我们需要在两个package.json文件中添加一个resolutions节点:
  "resolutions": {
     "graphql": "0.13.2"
   }
  1. 同时,我们还需要从两个package.json文件中的graphql版本中删除插入符(^)。

  2. 现在我们必须删除package-lock.jsonyarn.lock文件(如果有的话)。

  3. 在我们再次安装依赖之前,最好将 npm 更新到最新版本:

     npm install -g npm
  1. 之后,为了安全起见,让我们清除 npm 缓存:
    npm cache clean --force
  1. 然后再次运行npm install(首先在后端),然后在npm start中运行项目,如果一切正常,您应该看到 GraphiQL IDE 正常工作:

如何做到这一点...

现在我们的后端准备好了,让我们开始在前端工作:

  1. 我们需要修改的第一个文件是index.js文件:
    // Dependencies
    import React from 'react';
    import { render } from 'react-dom';
    import ApolloClient from 'apollo-boost';
    import { ApolloProvider } from 'react-apollo';

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

    // Styles
    import './index.css';

    // Apollo Client
    const client = new ApolloClient({
      uri: 'http://localhost:5000/graphiql' // Backend endpoint
    });

    // Wrapping the App with ApolloProvider
    const AppContainer = () => (
      <ApolloProvider client={client}>
        <App />
      </ApolloProvider>
    );

    // Root
    const root = document.getElementById('root');

    // Rendering the AppContainer
    render(<AppContainer />, root);

文件:src/index.js

  1. 我们将后端端点连接到ApolloClient,并将我们的<App />组件包装在<ApolloProvider>中(是的,这类似于 Redux Provider)。现在让我们修改我们的App.js文件,包括我们的主要组件(Tweets):
  // Dependencies
  import React, { Component } from 'react';

  // Components
  import Tweets from './components/Tweets';

  // Styles
  import './App.css';

  class App extends Component {
    render() {
      return (
        <div className="App">
          <Tweets />
        </div>
      );
    }
  }

 export default App;

文件:src/App.js

  1. 我们需要做的第一件事是创建我们的 GraphQL 查询和 mutations。为此,我们需要创建一个名为graphql的新目录,并在其中创建另外两个目录,一个用于mutations,另一个用于queries
 // Dependencies
 import gql from 'graphql-tag';

 // getTweets query
 export const QUERY_GET_TWEETS = gql`
    query getTweets {
      getTweets {
        _id
        tweet
        author
        createdAt
      }
    }
  `;

文件:src/graphql/queries/index.js

  1. 是的,你看得没错,这不是打字错误!该函数在没有括号的情况下被调用,并且只使用反引号(gqlYOUR QUERY HERE``)。getTweets查询已经在我们的后端中定义。我们正在执行getTweets查询,并将获得字段(_idtweetauthorcreatedAt)。现在让我们创建我们的 mutations:
  // Dependencies
  import gql from 'graphql-tag';

  // createTweet Mutation
  export const MUTATION_CREATE_TWEET = gql`
    mutation createTweet(
      $tweet: String,
      $author: String,
      $createdAt: DateTime
    ) {
      createTweet(
        tweet: $tweet,
        author: $author,
        createdAt: $createdAt
      ) {
        _id
        tweet
        author
        createdAt
      }
    }
  `;

  // deleteTweet Mutation
  export const MUTATION_DELETE_TWEET = gql`
    # ! means mandatory
    mutation deleteTweet($_id: String!) {
      deleteTweet(
        _id: $_id
      ) {
        _id
        tweet
        author
        createdAt
      }
    }
  `;

  // updateTweet Mutation
 export const MUTATION_UPDATE_TWEET = gql`
    mutation updateTweet(
      $_id: String!,
      $tweet: String!
    ) {
      updateTweet(
        _id: $_id,
        tweet: $tweet
      ) {
        _id
        tweet
        author
        createdAt
      }
    }
  `;

文件:src/graphql/mutations/index.js

  1. 我总是喜欢重构和改进事物,这就是为什么我为react-apolloQueryMutation组件创建了两个帮助程序。首先,让我们创建两个目录,sharedshared/components。首先,这是我们的 Query 组件:
  // Dependencies
  import React, { Component } from 'react';
  import { Query as ApolloQuery } from 'react-apollo';

  class Query extends Component {
    render() {
      const {
        query,
        render: Component
      } = this.props;

      return (
        <ApolloQuery query={query}>
          {({ loading, error, data }) => {
            if (loading) {
              return <p>Loading...</p>;
            }

            if (error) {
              return <p>Query Error: {error}</p>
            }

            return <Component data={data || false} />;
          }}
        </ApolloQuery>
      );
    }
  }

  export default Query;

文件:src/shared/components/Query.js

  1. 我们的 Mutation 组件应该是这样的:
  // Dependencies
  import React, { Component } from 'react';
  import { Mutation as ApolloMutation } from 'react-apollo';

  class Mutation extends Component {
    render() {
      const {
        mutation,
        query,
        children,
        onCompleted
      } = this.props;

      return (
        <ApolloMutation
          mutation={mutation}
          update={(cache, { data }) => {
            // Getting the mutation and query name
            const { 
              definitions: [{ name: { value: mutationName } }] 
            } = mutation;
            const { 
              definitions: [{ name: { value: queryName } }] 
            } = query;

            // Getting cachedData from previous query
            const cachedData = cache.readQuery({ query });

            // Getting current data (result of the mutation)
            const current = data[mutationName];

            // Initializing our updatedData
            let updatedData = [];

            // Lower case mutation name
            const mutationNameLC = mutationName.toLowerCase();

            // If the mutation includes "delete" or "remove"
            if (mutationNameLC.includes('delete') 
              || mutationNameLC.includes('remove')) {
              // Removing the current tweet by filtering 
              // from the cachedData
              updatedData = cachedData[queryName].filter(
                row => row._id !== current._id
              );
            } else if (mutationNameLC.includes('create') 
 || mutationNameLC.includes('add')) {
              // Create or add action injects the current 
              // value in the array
              updatedData = [current, ...cachedData[queryName]];
            } else if (mutationNameLC.includes('edit') 
 || mutationNameLC.includes('update')) {
              // Edit or update actions will replace the old values 
              // with the new ones
              const index = cachedData[queryName].findIndex(
                row => row._id === current._id
              );
              cachedData[queryName][index] = current;
              updatedData = cachedData[queryName];
            }

            // Updating our data to refresh the tweets list
            cache.writeQuery({
              query,
              data: {
                [queryName]: updatedData
              }
            });
          }}
          onCompleted={onCompleted} 
        >
          {/** 
            * Here we render the content of the 
            * component (children) 
            */}
          {children}
        </ApolloMutation>
      );
    }
  }

  export default Mutation;

文件:src/shared/components/Mutation.js

  1. 一旦我们的助手准备好了,让我们创建我们的 Tweets、Tweet 和 CreateTweet 组件。这是我们的Tweets组件:
 // Dependencies
  import React, { Component } from 'react';

  // Components
  import Tweet from './Tweet';
  import CreateTweet from './CreateTweet';
  import Query from '../shared/components/Query';

  // Queries
  import { QUERY_GET_TWEETS } from '../graphql/queries';

  // Styles
  import './Tweets.css';

  class Tweets extends Component {
    render() {
      return (
        <div className="tweets">
          {/* Rendering CreateTweet component */}
          <CreateTweet />

          {/** 
            * Executing QUERY_GET_TWEETS query and render our Tweet 
            * component 
            */}
          <Query query={QUERY_GET_TWEETS} render={Tweet} />
        </div>
      );
    }
  }

  export default Tweets;

文件:src/components/Tweets.js

  1. 这是我们的Tweet组件:
    // Dependencies
    import React, { Component } from 'react';
    import moment from 'moment';

    // Components
    import Mutation from '../shared/components/Mutation';

 // Queries
    import { 
      MUTATION_DELETE_TWEET, 
      MUTATION_UPDATE_TWEET 
    } from '../graphql/mutations';

    import { QUERY_GET_TWEETS } from '../graphql/queries';

    // Images (those are temporary images and exists on the repository)
    import TwitterLogo from './twitter.svg';
    import CodejobsAvatar from './codejobs.png';

    class Tweet extends Component {
      // Local State
      state = {
        currentTweet: false
      };

      // Enabling a textarea for edit a Tweet
      handleEditTweet = _id => {
        const { data: { getTweets: tweets } } = this.props;

        const selectedTweet = tweets.find(tweet => tweet._id === _id);

        const currentTweet = {
          [_id]: selectedTweet.tweet
        };

        this.setState({
          currentTweet
        });
      }

      // Handle Change for textarea
      handleChange = (value, _id) => {
        const { currentTweet } = this.state;

        currentTweet[_id] = value;

        this.setState({
          currentTweet
        });
      }

      // Delete tweet mutation
      handleDeleteTweet = (mutation, _id) => {
        // Sending variables
        mutation({
          variables: {
            _id
          }
        });
      }

      // Update tweet mutation
      handleUpdateTweet = (mutation, value, _id) => {
        // Sending variables
        mutation({
          variables: {
            _id,
            tweet: value
          }
        });
      }

      render() {
        // Getting the data from getTweets query
        const { data: { getTweets: tweets } } = this.props;

        // currentTweet state
        const { currentTweet } = this.state;

        // Mapping the tweets
        return tweets.map(({
          _id,
          tweet,
          author,
          createdAt
        }) => (
          <div className="tweet" key={`tweet-${_id}`}>
            <div className="author">
              {/* Rendering our Twitter Avatar (this is hardcoded) */}
              <img src={CodejobsAvatar} alt="Codejobs" />

              {/* Rendering the author */}
              <strong>{author}</strong>
            </div>

            <div className="content">
              <div className="twitter-logo">
                {/* Rendering the Twitter Logo */}
                <img src={TwitterLogo} alt="Twitter" />
              </div>

              {/**
 * If there is no currentTweet being edited then  
                * we display the tweet as a text otherwise we 
                * render a textarea with the tweet to be edited
 */}
              {!currentTweet[_id]
                ? tweet
                : (
                  <Mutation
                    mutation={MUTATION_UPDATE_TWEET}
                    query={QUERY_GET_TWEETS}
                    onCompleted={() => {
                      // Once the mutation is completed we clear our 
                      // currentTweet state
                      this.setState({
                        currentTweet: false
                      });
                    }}
                  >
                    {(updateTweet) => (
                      <textarea
                        autoFocus
                        className="editTextarea"
                        value={currentTweet[_id]}
                        onChange={(e) => {
                          this.handleChange(
                            e.target.value, 
 _id                          ); 
                        }}
                        onBlur={(e) => {
                          this.handleUpdateTweet(
 updateTweet, 
                            e.target.value, 
 _id                          ); 
                        }}
                      />
                    )}
                  </Mutation>
                )
              }
            </div>

            <div className="date">
              {/* Rendering the createdAt date (MMM DD, YYYY) */}
              {moment(createdAt).format('MMM DD, YYYY')}
            </div>

            {/* Rendering edit icon */}
            <div 
              className="edit" 
 onClick={() => { 
                this.handleEditTweet(_id); 
              }}
            >
              <i className="fa fa-pencil" aria-hidden="true" />
            </div>

            {/* Mutation for delete a tweet */}
            <Mutation
              mutation={MUTATION_DELETE_TWEET}
              query={QUERY_GET_TWEETS}
            >
              {(deleteTweet) => (
                <div 
                  className="delete" 
 onClick={() => {
                    this.handleDeleteTweet(deleteTweet, _id); 
                  }}
                >
                  <i className="fa fa-trash" aria-hidden="true" />
                </div>
              )}
            </Mutation>
          </div>
        ));
      }
    }

 export default Tweet;

文件:src/components/Tweet.js

  1. 我们的CreateTweet组件如下:
  // Dependencies
  import React, { Component } from 'react';
  import Mutation from '../shared/components/Mutation';

  // Images (this image is on the repository)
  import CodejobsAvatar from './codejobs.png';

  // Queries
  import { MUTATION_CREATE_TWEET } from '../graphql/mutations';
  import { QUERY_GET_TWEETS } from '../graphql/queries';

  class CreateTweet extends Component {
    // Local state
    state = {
      tweet: ''
    };

    // Handle change for textarea
    handleChange = e => {
      const { target: { value } } = e;

      this.setState({
        tweet: value
      })
    }

    // Executing createTweet mutation to add a new Tweet
    handleSubmit = mutation => {
      const tweet = this.state.tweet;
      const author = '@codejobs';
      const createdAt = new Date();

      mutation({
        variables: {
          tweet,
          author,
          createdAt
        }
      });
    }

    render() {
      return (
        <Mutation
          mutation={MUTATION_CREATE_TWEET}
          query={QUERY_GET_TWEETS}
          onCompleted={() => {
            // On mutation completed we clean the tweet state 
            this.setState({
              tweet: ''
            });
          }}
        >
          {(createTweet) => (
            <div className="createTweet">
              <header>
                Write a new Tweet
              </header>

              <section>
                <img src={CodejobsAvatar} alt="Codejobs" />

                <textarea
                  placeholder="Write your tweet here..."
                  value={this.state.tweet}
                  onChange={this.handleChange}
                />
              </section>

              <div className="publish">
                <button
                  onClick={() => {
                    this.handleSubmit(createTweet);
                  }}
                >
                  Tweet it!
                </button>
              </div>
            </div>
          )}
        </Mutation>
      );
    }
  }

 export default CreateTweet;

文件:src/components/CreateTweet.js

  1. 最后,但同样重要的是,这是样式文件:
  .tweet {
    margin: 20px auto;
    padding: 20px;
    border: 1px solid #ccc;
    height: 200px;
    width: 80%;
    position: relative;
  }

  .author {
    text-align: left;
    margin-bottom: 20px;
  }

  .author strong {
    position: absolute;
    top: 40px;
    margin-left: 10px;
  }

  .author img {
    width: 50px;
    border-radius: 50%;
  }

  .content {
    text-align: left;
    color: #222;
    text-align: justify;
    line-height: 25px;
  }

  .date {
    color: #aaa;
    font-size: 12px;
    position: absolute;
    bottom: 10px;
  }

  .twitter-logo img {
    position: absolute;
    right: 10px;
    top: 10px;
    width: 20px;
  }

  .createTweet {
    margin: 20px auto;
    background-color: #F5F5F5;
    width: 86%;
    height: 225px;
    border: 1px solid #AAA;
  }

  .createTweet header {
    color: white;
    font-weight: bold;
    background-color: #2AA3EF;
    border-bottom: 1px solid #AAA;
    padding: 20px;
  }

  .createTweet section {
    padding: 20px;
    display: flex;
  }

  .createTweet section img {
    border-radius: 50%;
    margin: 10px;
    height: 50px;
  }

  textarea {
    border: 1px solid #ddd;
    height: 80px;
    width: 100%;
  }

  .publish {
    margin-bottom: 20px;
  }

  .publish button {
    cursor: pointer;
    border: 1px solid #2AA3EF;
    background-color: #2AA3EF;
    padding: 10px 20px;
    color: white;
    border-radius: 20px;
    float: right;
    margin-right: 20px;
  }

  .delete {
    position: absolute;
    right: 10px;
    bottom: 10px;
    cursor: pointer;
  }

  .edit {
    position: absolute;
    right: 30px;
    bottom: 10px;
    cursor: pointer;
  }

文件:src/components/Tweets.css

它是如何工作的...

如果你做了一切正确,并且你在前端和后端分别运行(在不同的终端上),那么你可以在http://localhost:3000上运行项目,你应该会看到这个视图:

现在我们可以通过在文本区域中编写推文并点击“Tweet it!”按钮来创建新的推文:

正如你所看到的,推文的顺序是降序的。这意味着最新的推文会被发布在顶部。如果你想编辑一条推文,你可以点击编辑图标(铅笔):

保存更改的方法是通过移除文本区域的焦点(onBlur),现在我们可以看到更新后的推文:

最后,如果你想删除一条推文,那么点击垃圾桶图标(我已经删除了第二条推文):

正如你所看到的,变异非常容易实现,并且通过助手,我们简化了这个过程。

你可能会认为有一些方法可以将 Redux 与 GraphQL 一起使用,但让我告诉你,GraphQL 有可能会取代 Redux,因为我们可以通过 ApolloProvider 访问数据。

第十章:Webpack 4.x 的掌握

在本章中,将涵盖以下配方:

  • Webpack 4 零配置

  • 将 React 添加到 Webpack 4

  • 添加 Webpack Dev Server 和 Sass、Stylus 或 LessCSS 与 React

  • Webpack 4 优化 - 拆分捆绑包

  • 使用 React/Redux 和 Webpack 4 实现 Node.js

介绍

来自 Webpack 4 官方网站(webpack.js.org):

"Webpack 是现代 JavaScript 应用程序的静态模块打包工具。当 webpack 处理你的应用程序时,它在内部构建一个依赖图,映射了你的项目需要的每个模块,并生成一个或多个捆绑包。自从 4 版本以来,webpack 不需要配置文件来捆绑你的项目。尽管如此,它可以非常灵活地配置以更好地满足你的需求。"

Webpack 4 零配置

Webpack 4 默认情况下不需要配置文件。在旧版本中,你必须有一个配置文件。如果你需要根据项目的需要自定义 Webpack 4,你仍然可以创建一个配置文件,这将更容易配置。

准备就绪

为此,你需要创建一个新的文件夹并安装以下包:

mkdir webpack-zero-configuration
cd webpack-zero-configuration
npm install --save-dev webpack webpack-cli

在你的 Webpack 文件夹中,你需要创建一个package.json文件,为此,你可以使用以下命令:

npm init -y

如何做...

现在让我们开始配置:

  1. 打开package.json,并添加一个新的build脚本:
  {
    "name": "webpack-zero-configuration",
    "version": "1.0.0",
    "description": "Webpack 4 Zero Configuration",
    "main": "index.js",
    "scripts": {
      "build": "webpack"
    },
    "author": "Carlos Santana",
    "license": "MIT",
    "devDependencies": {
      "webpack": "⁴.6.0",
      "webpack-cli": "².0.15"
    }
  }

文件:package.json

  1. 在你的终端中运行构建脚本:
    npm run build
  1. 你会看到这个错误:

终端中出现的错误看起来像这样: ERROR in Entry module not found: Error: Can't resolver'./src' in '/Users/czantany/projects/React16Cookbook/Chapter9/Recipe1/webpack-zero-configuration'

  1. 因为我们现在使用的是 Webpack 4,默认情况下,主入口点是src/index.js。让我们创建这个文件,以便能够构建我们的第一个捆绑包:
    console.log('Index file...');

文件:src/index.js

  1. 如果你重新运行构建脚本,你会看到 Webpack 创建了一个名为main.js的新捆绑文件,放在dist文件夹中(同样,默认情况下):

警告让我们知道我们可以在生产或开发模式之间进行选择

  1. 终端中有一个警告消息:未设置mode选项,webpack 将回退到生产模式。将mode设置为developmentproduction以启用每个环境的默认值。您还可以将其设置为none以禁用任何默认行为。您可以在 https://webpack.js.org/concepts/mode/了解更多信息。默认情况下,生产模式已启用,这就是为什么我们的捆绑包(dist/main.js)被缩小和混淆的原因,类似于以下内容:
    !function(e){var n={};function r(t){if(n[t])return n[t].exports;var o=n[t]={i:t,l:!1,exports:{}};return e[t].call(o.exports,o,o.exports,r),o.l=!0,o.exports}r.m=e,r.c=n,r.d=function(e,n,t){r.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:t})},r.r=function(e){Object.defineProperty(e,"__esModule",{value:!0})},r.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(n,"a",n),n},r.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},r.p="",r(r.s=0)}([function(e,n){console.log("Index file...")}]);

文件:dist/main.js

它是如何工作的...

Webpack 4 有两种模式:生产模式和开发模式。在 Webpack 3 中,您需要为每种模式创建一个配置文件;现在您只需一行代码就可以得到相同的结果。让我们添加一个脚本,以便使用开发模式启动我们的应用程序:

  {
    "name": "webpack-zero-configuration",
    "version": "1.0.0",
    "description": "Webpack 4 Zero Configuration",
    "main": "index.js",
    "scripts": {
      "build-development": "webpack --mode development",
      "build": "webpack --mode production"
    },
    "author": "Carlos Santana",
    "license": "MIT",
    "devDependencies": {
      "webpack": "⁴.6.0",
      "webpack-cli": "².0.15"
    }
  }

文件:package.json

如果运行npm run build-development命令,现在您会发现捆绑包根本没有被压缩:

文件:dist/main.js

正如您所看到的,默认情况下,Webpack 4 在生产模式下会对代码进行缩小,并对该环境进行一些优化,在 Webpack 3 中,这个配置必须在配置文件中手动完成。

还有更多...

如果您想要在 Webpack 4 中使用 Babel 来转译 ES6 代码,您需要使用babel-loader,并且可能需要安装以下软件包:

npm install --save-dev babel-loader babel-core babel-preset-env
  1. 在项目的根目录创建一个.babelrc 文件,然后添加以下代码:
    {
      "presets": ["env"]
    }

文件:.babelrc

  1. 使用webpack.config.js文件添加我们的babel-loader
  const webpackConfig = {
    module: {
      rules: [
        {
          test: /\.js$/,
          exclude: /node_modules/,
          use: 'babel-loader'
        }
      ]
    }
  };

  module.exports = webpackConfig;

文件:webpack.config.js

  1. 创建一个名为src/numbers.js的文件,并将其导入到我们的src/index.js中以测试我们的babel-loader
    export const numbers = ['one', 'two', 'three'];

文件:src/numbers.js

  1. 在我们的index.js文件中,做以下操作:
  import { numbers } from './numbers';
  numbers.forEach(number => console.log(number));

文件:src/index.js

  1. 运行npm run build脚本,如果一切正常,您应该会得到这个结果:

  1. 还可以直接在终端中使用babel-loader,而无需配置文件,为此,我们需要使用--module-bind标志将扩展名绑定到加载器:
  {
    "name": "webpack-zero-configuration",
    "version": "1.0.0",
    "description": "Webpack 4 Zero Configuration",
    "main": "index.js",
    "scripts": {
      "build-development": "webpack --mode development --module-bind 
     js=babel-loader",
      "build": "webpack --mode production --module-bind js=babel-
       loader"
    },
    "author": "Carlos Santana",
    "license": "MIT",
    "devDependencies": {
      "babel-core": "⁶.26.3",
      "babel-loader": "⁷.1.4",
      "babel-preset-env": "¹.6.1",
      "webpack": "⁴.6.0",
      "webpack-cli": "².0.15"
    }
  }
  1. 还有更多绑定模块的标志(如果您想了解更多关于 Webpack CLI 的信息,可以访问官方网站webpack.js.org/api/cli/):
  • --module-bind-post:将扩展名绑定到后置加载器

  • --module-bind-pre:将扩展名绑定到前置加载器

向 Webpack 4 添加 React

在这个配方中,我们将实现 React 与 Webpack 4,但我们将使用一个名为html-webpack-plugin的插件来生成我们的index.html文件以渲染我们的 React 应用程序。在下一个配方中,我们将集成 Node.js,以在渲染 HTML 代码之前在服务器端具有更多的灵活性。

准备工作

对于这个配方,你需要安装以下包:

    npm install react react-dom babel-preset-react

如何做...

以下是将 React 添加到 Webpack 4 的步骤:

  1. 使用上一个配方的相同代码,创建一个.babelrc文件并添加一些预设:
  {
    "presets": [
      "env",
      "react"
    ]
  }

文件:.babelrc

  1. 在我们的webpack.config.js文件中,我们需要在我们的babel-loader中添加.jsx扩展名,以便能够将babel-loader应用到我们的 React 组件:
  const webpackConfig = {
    module: {
      rules: [
        {
          test: /\.(js|jsx)$/,
          exclude: /node_modules/,
          use: 'babel-loader'
        }
      ]
    }
  };

  module.exports = webpackConfig;

文件:webpack.config.js

  1. 在我们将.jsx扩展名添加到我们的babel-loader之后,我们需要创建src/components/App.jsx文件:
  // Dependencies
  import React from 'react';

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

  const App = props => (
    <div>
      <Home />
    </div>
  );

  export default App;

文件:src/components/App.jsx

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

  const Home = () => <h1>Home</h1>;

  export default Home;

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

  1. 在我们的主index.js文件中,我们需要包括react,从react-dom中的render方法和我们的App组件,并渲染应用程序:
  // Dependencies
  import React from 'react';
  import { render } from 'react-dom';

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

  render(<App />, document.querySelector('#root'));

文件:src/index.jsx

  1. 你可能会想知道#root div 在哪里,因为我们还没有创建index.html。在这个特定的配方中,我们将使用html-webpack-plugin插件来处理我们的 HTML:
    npm install --save-dev html-webpack-plugin
  1. 打开你的webpack.config.js文件。我们需要添加我们的html-webpack-plugin并在配置文件中创建一个插件节点:
  const HtmlWebPackPlugin = require('html-webpack-plugin');

  const webpackConfig = {
    module: {
      rules: [
        {
          test: /\.(js|jsx)$/,
          exclude: /node_modules/,
          use: 'babel-loader'
        }
      ]
    },
    plugins: [
      new HtmlWebPackPlugin({
        title: 'Codejobs',
        template: './src/index.html',
        filename: './index.html'
      })
    ]
  };

  module.exports = webpackConfig;

文件:webpack.config.js

  1. 在你的src目录级别创建index.html模板:
  <!DOCTYPE html>
  <html>
    <head>
      <meta charset="UTF-8">
      <title><%= htmlWebpackPlugin.options.title %></title>
    </head>
    <body>
      <div id="root"></div>
    </body>
  </html>

文件:src/index.html

它是如何工作的...

正如你所看到的,我们可以使用htmlWebpackPlugin.options对象从插件中注入变量,在<%=%>分隔符之间。现在是测试我们应用程序的时候了,尝试运行npm run build命令:

大红色错误: 无法解析./src目录,但这是什么意思?你还记得我们如何在文件中使用.jsx扩展名吗?即使我们将该扩展名添加到了我们的babel-loader规则中,为什么它还是不起作用呢?这是因为我们必须在配置中添加一个解析节点,并指定我们想要支持的文件扩展名。否则,我们只能使用.js扩展名:

  const HtmlWebPackPlugin = require('html-webpack-plugin');

  const webpackConfig = {
    module: {
      rules: [
        {
          test: /\.(js|jsx)$/,
          exclude: /node_modules/,
          use: 'babel-loader'
        }
      ]
    },
    plugins: [
      new HtmlWebPackPlugin({
        title: 'Codejobs',
        template: './src/index.html',
        filename: './index.html'
      })
    ],
    resolve: {
      extensions: ['.js', '.jsx']
    }
  };

 module.exports = webpackConfig;

文件:webpack.config.js

如果你再次运行npm run build,现在应该可以工作了:

运行该命令后,您会看到在 dist 目录中有两个文件:index.htmlmain.js。如果您用 Chrome 打开您的index.html文件,您应该会看到以下结果:

我们可以构建我们的捆绑包,但它是 100%静态的。在下一个教程中,我们将添加 Webpack Dev Server 来在实际服务器上运行我们的 React 应用程序,并在每次更改时刷新服务器。

还有更多...

我更喜欢在所有项目中使用 ES6 代码,甚至在配置中,我喜欢将我的 Webpack 配置分成单独的文件,以便更好地组织和更容易理解配置。如果你以前使用过 Webpack,你就知道webpack.config.js文件可能会很大,很难维护,所以让我解释一下如何做到这一点:

  1. webpack.config.js文件重命名为webpack.config.babel.js。当在.js文件上添加.babel后缀时,这将由 Babel 自动处理。

  2. 让我们将当前的 ES5 代码迁移到 ES6:

  import HtmlWebPackPlugin from 'html-webpack-plugin';

  export default {
    module: {
      rules: [
        {
          test: /\.(js|jsx)$/,
          exclude: /node_modules/,
          use: 'babel-loader'
        }
      ]
    },
    plugins: [
      new HtmlWebPackPlugin({
        title: 'Codejobs',
        template: './src/index.html',
        filename: './index.html'
      })
    ],
    resolve: {
      extensions: ['.js', '.jsx']
    }
  };

文件:webpack.config.babel.js

  1. 创建一个名为webpack的文件夹,里面再创建一个名为configuration的文件夹。

  2. 为我们 Webpack 配置的每个节点创建一个单独的文件并导出它。例如,让我们从为我们的节点模块创建一个文件开始,所以你应该叫module.js

  export default {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: 'babel-loader'
      }
    ]
  };

文件:webpack/configuration/module.js

  1. 让我们为我们的插件创建一个文件(plugins.js):
  import HtmlWebPackPlugin from 'html-webpack-plugin';

 const plugins = [
    new HtmlWebPackPlugin({
      title: 'Codejobs',
      template: './src/index.html',
      filename: './index.html'
    })
  ];

  export default plugins;

文件:webpack/configuration/plugins.js

  1. 将我们的插件数组添加到一个常量中非常有用,因为这样我们可以根据环境(开发或生产)添加更多的插件,所以现在你可以以条件方式添加插件(使用 push)。

  2. 最后一个节点是 resolve:

  export default {
    extensions: ['.js', '.jsx']
  }

文件:webpack/configuration/resolve.js

  1. 我们可以直接导入我们的文件,但我更喜欢使用一个index.js文件并导出所有文件。这样,我们只需要在我们的webpack.config.babel.js文件中导入我们需要的对象:
 // Configuration
  import module from './module';
  import plugins from './plugins';
  import resolve from './resolve';

  export {
    module,
    plugins,
    resolve
  };

文件:webpack/configuration/index.js

  1. 我们的webpack.config.babel.js将非常干净:
  import {
    module,
    plugins,
    resolve
  } from './webpack/configuration';

  export default {
    module,
    plugins,
    resolve
  };

文件:webpack.config.babel.js

使用 Webpack Dev Server 和 React 添加 Sass、Stylus 或 LessCSS

在上一个教程中,我们将 React 添加到了 Webpack 4 中,并且拆分了我们的 Webpack 配置,但最终,我们只能构建我们的捆绑包并将应用程序作为静态页面运行。在这个教程中,我们将添加 Webpack Dev Server 来在实际服务器上运行我们的 React 应用程序,并在每次更改时重新启动服务器。此外,我们将实现诸如 Sass、Stylus 和 LessCSS 之类的 CSS 预处理器。

准备工作

对于这个教程,您需要安装以下软件包:

    npm install webpack-dev-server **css-loader extract-text-webpack-plugin@v4.0.0-beta.0 style-loader**

如果要在项目中使用 Sass,必须安装:

    npm install sass-loader **node-sass** 

如果您更喜欢 Stylus,您将需要以下内容:

    npm install stylus-loader stylus

或者,如果您喜欢 LessCSS,请安装:

    npm install less-loader less

如何做...

我们将首先添加 Webpack Dev Server:

  1. 安装了webpack-dev-server依赖项后,我们需要在package.json中添加一个新的脚本来启动应用程序:
    "scripts": {
      "start": "webpack-dev-server --mode development --open",
      "build-development": "webpack --mode development",
      "build": "webpack --mode production"
    }

文件:package.json

  1. 如您所知,--mode标志指定我们想要的模式(默认为生产模式),--open标志在启动应用程序时打开浏览器。现在您可以使用npm start命令运行应用程序:

  1. 您的应用程序是使用端口 8080 打开的,这是webpack-dev-server的默认端口。如果要更改它,可以使用--port标志指定要使用的端口:
"start": "webpack-dev-server --mode development --open --port 9999"
  1. webpack-dev-server的很酷的一点是,如果更新任何组件,您将立即看到更改的反映。例如,让我们修改我们的Home组件:
  import React from 'react';

  const Home = () => <h1>Updated Home</h1>;

  export default Home;

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

  1. 您可以在同一页上看到反映的更改,而无需手动刷新页面:

  1. 让我们向我们的项目添加 Sass、Stylus 或 LessCSS,以在应用程序中添加一些样式。您必须编辑位于webpack/configuration/module.js的文件,并添加style-loadercss-loader以及我们想要的 sass(sass-loader)、stylus(stylus-loader)或 less(less-loader)加载器:
  export default {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: 'babel-loader'
      },
      {
        test: /\.scss$/, // Can be: .scss or .styl or .less
        use: [
          {
            loader: 'style-loader'
          },
          {
            loader: 'css-loader',
            options: {
              // Enables CSS Modules
              modules: true, 
              // Number of loaders applied before CSS loader
              importLoaders: 1, 
              // Formatting CSS Class name
              localIdentName: '[name]_[local]_[hash:base64]', // Enable/disable sourcemaps
              sourceMap: true, 
              // Enable/disable minification
              minimize: true 
            }
          },
          {
            loader: 'sass-loader' // sass-loader or stylus-loader
                                  // or less-loader
          }
        ]
      }
    ]
  };

文件:webpack/configuration/module.js

  1. 使用 Sass,我们可以创建 Home.scss 文件来添加一些样式:
  $color: red;
  .Home {
    color: $color;
  }

文件:src/components/Home/Home.scss

  1. 在 Home 组件中,您可以像这样导入 Sass 文件:
  import React from 'react';
  import styles from './Home.scss'; // For Sass
  // import styles from './Home.styl'; // For Stylus
  // import styles from './Home.less'; // For Less

  const Home = () => <h1 className={styles.Home}>Updated Home</h1>;

 export default Home;

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

  1. 每个导入行都是为不同的预处理器。使用您想要的行并删除其他行。Sass 生成这种样式:

  1. 如果你想使用 Stylus,在Home.styl文件中创建并在 Webpack 配置的module.js文件中更改配置:
  $color = green

  .Home
    color: $color

文件:src/components/Home/Home.styl

  1. 如果你想使用 Less CSS,在 Webpack 配置中做必要的更改,然后使用这个文件:
 @color: blue;

 .Home {
    color: @color;
  }

文件:src/components/Home/Home.less

它是如何工作的...

如果你好奇的话,你可能已经尝试过查看样式表是如何渲染的,以及我们的 HTML 中的类名是什么。如果你检查网站,你会看到类似这样的东西:

动态地注入了一个包含我们编译后的 css 的临时 URL 的<link>标签,然后我们的类名是"Home_Home_2kP...",这是因为我们的配置:localIdentName: '[name]_[local]_[hash:base64]'。通过这样做,我们创建了隔离的样式,这意味着如果我们使用相同的名称,我们永远不会影响任何其他类。

还有更多...

让我们实现 CSS 预处理器,比如 Sass、Stylus 和 LessCSS:

  1. 如果你想将你的 CSS 代码提取到一个style.css文件中,并在生产模式下压缩代码,你可以使用extract-text-webpack-plugin包:
   npm install extract-text-webpack-plugin@v4.0.0-beta.0
  1. 我们需要将这个添加到我们的 Webpack 插件中:
  import HtmlWebPackPlugin from 'html-webpack-plugin';
  import ExtractTextPlugin from 'extract-text-webpack-plugin';

  const isProduction = process.env.NODE_ENV === 'production';

  const plugins = [
    new HtmlWebPackPlugin({
      title: 'Codejobs',
      template: './src/index.html',
      filename: './index.html'
    })
  ];

  if (isProduction) {
    plugins.push(
      new ExtractTextPlugin({
        allChunks: true,
        filename: './css/[name].css'
      })
    );
  }

 export default plugins;

文件:webpack/configuration/plugins.js

  1. 正如你所看到的,我只有在生产模式下才会向插件数组中添加内容。这意味着我们需要在 package.json 中创建一个新的脚本来指定何时使用生产模式:
    "scripts": {
      "start": "webpack-dev-server --mode development --open",
      "start-production": "NODE_ENV=production webpack-dev-server --
      mode production",
      "build-development": "webpack --mode development",
      "build": "webpack --mode production"
    }
  1. 在你的终端中运行npm run start-production,你就可以以生产模式启动了。

  2. 你可能会遇到一些错误,因为我们还需要向我们的模块节点添加一个 Extract Text 插件的规则:

  import ExtractTextPlugin from 'extract-text-webpack-plugin';

  const isProduction = process.env.NODE_ENV === 'production';

  const rules = [
    {
      test: /\.(js|jsx)$/,
      exclude: /node_modules/,
      use: 'babel-loader'
    }
  ];

  if (isProduction) {
    rules.push({
      test: /\.scss/,
      use: ExtractTextPlugin.extract({
        fallback: 'style-loader',
        use: [
          'css-loader?minimize=true&modules=true&localIdentName=
          [name]_[local]_[hash:base64]',
          'sass-loader'
        ]
      })
    });
  } else {
    rules.push({
      test: /\.scss$/, // .scss - .styl - .less
      use: [
        {
          loader: 'style-loader'
        },
        {
          loader: 'css-loader',
          options: {
            modules: true,
            importLoaders: 1,
            localIdentName: '[name]_[local]_[hash:base64]',
            sourceMap: true,
            minimize: true
          }
        },
        {
          loader: 'sass-loader' // sass-loader, stylus-loader or 
                                //less-loader
        }
      ]
    });
  }

  export default {
    rules
  };
  1. 我们只在生产环境中使用 Extract Text 插件。对于任何其他环境,我们像以前一样直接使用style-loadercss-loadersass-loader。这就是为什么我喜欢将 Webpack 配置拆分成更小的文件,正如你所看到的,有些文件可能会很大,所以这有助于我们更有条理。如果你用npm run start-production启动生产模式,你会看到这个 CSS:

Webpack 4 优化 - 拆分捆绑包

Webpack 4 已经为生产模式提供了一些优化预设,比如代码最小化(之前是使用 UglifyJS 实现的),但是我们还可以使用更多的东西来提高我们应用的性能。在这个教程中,我们将学习如何拆分捆绑包(供应商和应用程序捆绑包),添加源映射,并实现BundleAnalyzerPlugin

准备就绪

对于这个教程,我们需要安装以下包:

npm install webpack-bundle-analyzer webpack-notifier

如何做...

让我们给我们的 Webpack 添加一个源映射:

  1. 创建webpack/configuration/devtool.js文件:
  const isProduction = process.env.NODE_ENV === 'production';

  export default !isProduction ? 'cheap-module-source-map' : 'eval';

文件:webpack/configuration/devtool.js

  1. 拆分捆绑包(使用新的“优化”Webpack 节点):一个用于我们的/node_modules/,它将是最大的,一个用于我们的 React 应用程序。你需要创建optimization.js文件并添加这段代码:
 export default {
    splitChunks: {
      cacheGroups: {
        default: false,
        commons: {
          test: /node_modules/,
          name: 'vendor',
          chunks: 'all'
        }
      }
    }
  }

文件:webpack/configuration/optimization.js

  1. 请记住,你需要把这些新文件添加到index.js中:
  // Configuration
  import devtool from './devtool';
  import module from './module';
  import optimization from './optimization';
  import plugins from './plugins';
  import resolve from './resolve';

  export {
    devtool,
    module,
    optimization,
    plugins,
    resolve
  };

文件:webpack/configuration/index.js

  1. 将节点添加到webpack.config.babel.js中:
  import {
    devtool,
    module,
    optimization,
    plugins,
    resolve
  } from './webpack/configuration';

  export default {
    devtool,
    module,
    plugins,
    optimization,
    resolve
  };

文件:webpack.config.babel.js

它是如何工作的...

让我们测试一下:

  1. 只需用npm start运行应用程序。如果你查看 HTML,你会发现它自动注入到vendor.jsmain.js捆绑包中:

  1. 如果你查看网络选项卡,你可以看到文件的大小:

  1. 如果你以生产模式运行应用程序,你会注意到捆绑包更小。运行npm run start-production命令:

  1. 通过这种优化,我们将捆绑包的大小减少了 40%。在下一个教程中,我们将实现 Node.js 与 Webpack 和 React,并且我们将能够应用 GZip 压缩,这将帮助我们进一步减少捆绑包的大小。

  2. BundleAnalyzer插件可以帮助我们查看所有包(node_modules)和我们组件的大小;这将给我们一个按大小组织的捆绑包的图像(大方块表示大尺寸,小方块表示小尺寸)。我们还可以实现WebpackNotifierPlugin插件,这只是一个通知,每当我们的 Webpack 构建时就会显示:

  import HtmlWebPackPlugin from 'html-webpack-plugin';
  import ExtractTextPlugin from 'extract-text-webpack-plugin';
  import WebpackNotifierPlugin from 'webpack-notifier';
 import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';

  const isProduction = process.env.NODE_ENV === 'production';

  const plugins = [
    new HtmlWebPackPlugin({
      title: 'Codejobs',
      template: './src/index.html',
      filename: './index.html'
    })
  ];

  if (isProduction) {
    plugins.push(
      new ExtractTextPlugin({
        allChunks: true,
        filename: './css/[name].css'
      })
    );
  } else {
    plugins.push(
      new BundleAnalyzerPlugin(),
 new WebpackNotifierPlugin({
 title: 'CodeJobs'
 })
    );
  }

 export default plugins;

文件:webpack/configuration/plugins.js

  1. BundleAnalyzerPlugin只会在开发模式下执行;如果你启动应用程序(npm start),你会看到一个新页面打开,并显示所有安装的包,指定每个包的大小:

这张图片的目的是展示安装包的大小

  1. 最大的文件当然是 vendor.js 文件,但我们也可以看到我们的 main.js 组件:

  1. 当你启动应用程序时,你可以看到漂亮的通知:

使用 Node.js 与 React/Redux 和 Webpack 4

到目前为止,在所有的教程中,我们直接使用了create-react-app或 Webpack 4 来使用 React。在这个教程中,我们将使用 Node.js 和 Webpack 4 来实现 React 和 Redux;这将帮助我们拥有更强大的应用程序。

准备工作

使用上一个教程的相同代码,你需要安装所有这些包:

npm install babel-cli express nodemon react-hot-loader react-router-dom webpack-hot-middleware compression-webpack-plugin react-redux redux

如何做…

让我们开始实施吧:

  1. 在我们的.babelrc文件中包含react-hot-loader插件,只用于开发环境:
  {
    "presets": ["env", "react"],
    "env": {
      "development": {
        "plugins": [
          "react-hot-loader/babel"
        ]
      }
    }
  }

文件:.babelrc

  1. 创建一个 Express 服务器;你需要在src/server/index.js中创建一个文件:
  // Dependencies
  import express from 'express';
  import path from 'path';
  import webpackDevMiddleware from 'webpack-dev-middleware';
  import webpackHotMiddleware from 'webpack-hot-middleware';
  import webpack from 'webpack';

  // Webpack Configuration
  import webpackConfig from '../../webpack.config.babel';

  // Client Render
  import clientRender from './render/clientRender';

 // Utils
  import { isMobile } from '../shared/utils/device';

  // Environment
  const isProduction = process.env.NODE_ENV === 'production';

  // Express Application
  const app = express();

  // Webpack Compiler
  const compiler = webpack(webpackConfig);

  // Webpack Middleware
  if (!isProduction) {
    // Hot Module Replacement
    app.use(webpackDevMiddleware(compiler));
    app.use(webpackHotMiddleware(compiler));
  } else {
    // Public directory
    app.use(express.static(path.join(__dirname, '../../public')));

    // GZip Compression just for Production
    app.get('*.js', (req, res, next) => {
      req.url = `${req.url}.gz`;
      res.set('Content-Encoding', 'gzip');
      next();
    });
  }

  // Device Detection
  app.use((req, res, next) => {
    req.isMobile = isMobile(req.headers['user-agent']);
    next();
  });

  // Client Side Rendering
  app.use(clientRender());

  // Disabling x-powered-by
  app.disable('x-powered-by');

  // Listen Port 3000...
  app.listen(3000);

文件:src/server/index.js

  1. 我们在 Node.js 中包含了设备检测,用于 Redux 的initialState。我们可以为此目的创建这个工具文件:
  export function getCurrentDevice(ua) {
    return /mobile/i.test(ua) ? 'mobile' : 'desktop';
  }
  export function isDesktop(ua) {
    return !/mobile/i.test(ua);
  }
  export function isMobile(ua) {
    return /mobile/i.test(ua);
  }

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

  1. 你还需要设备 reducer:
  export default function deviceReducer(state = {}) {
    return state;
  }

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

  1. 我们需要在 reducers 文件夹中创建index.js,在这个地方我们将合并我们的 reducers:
  // Dependencies
  import { combineReducers } from 'redux';

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

  const rootReducer = combineReducers({
    device
  });

 export default rootReducer;

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

  1. 让我们创建我们的初始状态文件。这是我们将从req对象中获取设备信息的地方:
  export default req => ({
    device: {
      isMobile: req.isMobile
    }
  });
  1. Redux 需要一个存储来保存所有的 reducers 和我们的initialState;这将是我们的configureStore
 // 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. 在上一个教程中,我们使用了html-webpack-plugin包来渲染初始 HTML 模板;现在我们需要在 Node 中做到这一点。为此,你需要创建src/server/render/html.js文件:
 // Dependencies
  import serialize from 'serialize-javascript';

  // Environment
  const isProduction = process.env.NODE_ENV === 'production';

  export default function html(options) {
    const { title, initialState } = options;
    let path = '/';
    let link = '';

    if (isProduction) {
      path = '/app/';
      link = `<link rel="stylesheet" href="${path}css/main.css" />`;
    }

    return `
      <!DOCTYPE html>
      <html>
        <head>
          <meta charset="utf-8">
          <title>${title}</title>
          ${link}
        </head>
        <body>
          <div id="root"></div>

          <script>
            window.initialState = ${serialize(initialState)};
          </script>
          <script src="${path}vendor.js"></script>
          <script src="${path}main.js"></script>
        </body>
      </html>
    `;
  }

文件:src/server/render/html.js

  1. 创建一个函数来渲染 HTML;我把这个文件叫做clientRender.js
 // HTML
  import html from './html';

 // Initial State
  import initialState from './initialState';

  export default function clientRender() {
    return (req, res) => res.send(html({
      title: 'Codejobs',
      initialState: initialState(req)
    }));
  }

文件:src/server/render/clientRender.js

  1. 在创建了服务器文件之后,我们需要为客户端添加主入口文件。在这个文件中,我们将把我们的主App组件包裹在 React 热加载器应用容器中:
  // Dependencies
  import React from 'react';
  import { render } from 'react-dom';
  import { Provider } from 'react-redux';
  import { AppContainer } from 'react-hot-loader';

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

  // Components
  import App from './client/App';

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

  // Root element
  const rootElement = document.querySelector('#root');

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

  // Rendering app
  renderApp(App);

  // Hot Module Replacement
  if (module.hot) {
    module.hot.accept('./client/App', () => {
      renderApp(require('./client/App').default);
    });
  }

文件:src/index.jsx

  1. 让我们为我们的客户端文件创建一个目录。我们需要创建的第一个文件是App.jsx,在这里我们将包含我们组件的路由:
  // Dependencies
  import React from 'react';
  import { BrowserRouter, Switch, Route } from 'react-router-dom';

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

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

  export default App;

文件:src/client/App.jsx

  1. 为了测试我们的路由和 Redux 状态(isMobile),让我们创建About组件:
  import React from 'react';
  import { bool } from 'prop-types';
  import { connect } from 'react-redux';
  import styles from './About.scss';

  const About = ({ isMobile }) => (
    <h1 className={styles.About}>About - {isMobile ? 'mobile' : 'desktop'}</h1>
  );

  About.propTypes = {
    isMobile: bool
  };

 export default connect(({ device }) => ({
    isMobile: device.isMobile
  }))(About);

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

  1. 为此组件添加基本样式:
  $color: green;

  .About {
    color: $color;
  }

文件:src/client/components/About/About.scss

  1. 当我们想要使用 React Hot Loader 在每次更改时刷新页面时,我们需要为我们的webpack-hot-middleware添加一个条目,并为react-hot-loader添加一个条目来连接到HMR热模块替换):
  const isProduction = process.env.NODE_ENV === 'production';
  const entry = [];

  if (!isProduction) {
    entry.push(
      'webpack-hot-middleware/client?
       path=http://localhost:3000/__webpack_hmr&reload=true',
      'react-hot-loader/patch',
      './src/index.jsx'
    );
  } else {
    entry.push('./src/index.jsx');
  }

  export default entry;

文件:webpack/configuration/entry.js

  1. 创建output.js文件以指定我们的 Webpack 应该保存文件的位置:
 // Dependencies
  import path from 'path';

  export default {
    filename: '[name].js',
    path: path.resolve(__dirname, '../../public/app'),
    publicPath: '/'
  };
  1. 您需要将这些文件导入到我们的index.js中:
  // Configuration
  import devtool from './devtool';
  import entry from './entry';
 import mode from './mode';
  import module from './module';
  import optimization from './optimization';
  import output from './output';
  import plugins from './plugins';
  import resolve from './resolve';

  export {
    devtool,
 entry,
 mode,
    module,
    optimization,
    output,
    plugins,
    resolve
  };

文件:webpack/configuration/index.js

  1. 我们还需要创建一个mode.js文件,并从我们的 JS 文件中处理环境模式,因为我们将要更改我们的启动脚本,不再直接指定模式:
  const isProduction = process.env.NODE_ENV === 'production';

  export default !isProduction ? 'development' : 'production';

文件:webpack/configuration/mode.js

  1. 为开发添加HotModuleReplacementPlugin到我们的插件文件,为生产添加CompressionPlugin
  import ExtractTextPlugin from 'extract-text-webpack-plugin';
  import WebpackNotifierPlugin from 'webpack-notifier';
  import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
  import CompressionPlugin from 'compression-webpack-plugin';
  import webpack from 'webpack';
  const isProduction = process.env.NODE_ENV === 'production';
  const plugins = [];
  if (isProduction) {
    plugins.push(
      new ExtractTextPlugin({
        allChunks: true,
        filename: './css/[name].css'
      }),
      new CompressionPlugin({
        asset: '[path].gz[query]',
        algorithm: 'gzip',
        test: /\.js$/,
        threshold: 10240,
        minRatio: 0.8
      })
    );
  } else {
    plugins.push(
      new webpack.HotModuleReplacementPlugin(),
      new BundleAnalyzerPlugin(),
      new WebpackNotifierPlugin({
        title: 'CodeJobs'
      })
    );
  }
 export default plugins;

文件:webpack/configuration/plugins.js

  1. package.json中,新的启动脚本应该如下所示:
    "scripts": {
      "build": "NODE_ENV=production webpack",
      "**clean**": "**rm** -rf public/app",
      "start": "**npm run** clean && NODE_ENV=development nodemon src/server --watch src/server --exec babel-node --presets es2015",
      "start-production": "npm run clean && npm run build && NODE_ENV=production babel-node src/server --presets es2015"
    }

文件:package.json 如果您使用 Windows,您必须使用SET关键字来指定NODE_ENV。例如,*SET NODE_ENV=development*SET NODE_ENV=production否则将无法在您的机器上工作。

它是如何工作的...

我们现在将看到它是如何工作的:

  1. 使用npm start启动应用程序。

  2. 您应该看到这个页面:

  1. 如果您打开浏览器的控制台,您将看到 HMR 现在已连接:

  1. 您可以对Home组件进行更改,以查看内容如何在不刷新的情况下更新:

  1. 如您在控制台中所见,HMR 指定每个事件的发生并为您提供更新的模块。如果您打开网络选项卡,您将看到我们捆绑包的巨大大小(vendor.js = 1MBmain.js = 46.3KB):

  1. 如果您访问http://localhost:3000/about网址,您将看到连接了 Redux 状态(isMobile)的About组件:

  1. 如果您想以生产模式运行应用程序,请执行npm run start-production。如果一切正常,您应该看到相同的网站,但捆绑包更小(vendor.js:262KB - 减少 74%和 main.js:5.2KB - 减少 88%):

还有更多...

我不喜欢在导入中使用相对路径,有时很难计算某些文件的深度。babel-plugin-module-resolver包可以帮助我们为我们的目录添加自定义别名。例如:

    // Instead of importing like this 
 import { isMobile } from  '../../../shared/utils/device';

    **// Using module resolver you can use an alias like:**
    **import** { isMobile } **from** '**@utils**/device';

正如你所看到的,使用别名更加一致,而且无论你从哪个路径导入 util,始终都会使用相同的路径别名,这很酷,不是吗?

首先,我们需要安装这个包:

    npm install babel-plugin-module-resolver

然后在我们的.babelrc中,我们可以为每个路径添加我们的别名:

  {
    "presets": ["env", "react"],
    "env": {
      "development": {
        "plugins": [
          "react-hot-loader/babel"
        ]
      }
    },
    "plugins": [
     ["module-resolver", {
       "root": ["./"],
       "alias": {
         "@App": "./src/client/App.jsx",
         "@client": "./src/client/",
         "@components": "./src/client/components",
         "@configureStore": "./src/shared/redux/configureStore.js",
         "@reducers": "./src/shared/reducers",
         "@server": "./src/server/",
         "@utils": "./src/shared/utils",
         "@webpack": "./webpack.config.babel.js"
       }
     }]
   ],
  }

*@*字符并不是必需的,但我喜欢使用它来快速识别我是否在使用别名。现在你可以修改我们在这个教程中制作的一些文件,并用新的别名替换路径:

文件:src/client/App.jsx文件:src/index.jsx文件:src/server/index.js

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

第十一章:实现服务器端渲染

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

  • 实现服务器端渲染

  • 使用服务器端渲染实现承诺

  • 实现 Next.js

介绍

React 通常使用客户端渲染(CSR)。这意味着它动态地将 HTML 代码注入到目标div中(通常使用#app#root ID),这就是为什么如果您尝试直接查看页面的代码(右键单击-查看页面代码),您将看到类似于这样的内容:

查看实际代码的唯一方法是使用 Chrome Dev 工具或其他工具检查网站,以下是 React 使用 CSR 生成的代码:

通过检查页面,您可以看到注入到我们的#root div 中的代码。服务器端渲染(SSR)对于改善我们网站的SEO并被主要搜索引擎(如GoogleYahooBing)索引非常有用。如果您不太关心 SEO,可能不需要担心SSR。目前,Googlebot支持CSR,并且可以在Google上索引我们的网站,但如果您关心 SEO 并且担心改善其他搜索引擎(如YahooBingDuckDuckGo)上的 SEO,则使用SSR是正确的选择。

实现服务器端渲染

在这个示例中,我们将在我们的项目中实现 SSR。

准备工作

我们将使用上一篇示例(使用 Node.js 与 React/Redux 和 Webpack 4 实现)中的代码,从第十章,掌握 Webpack 4.x,并安装一些其他依赖项:

npm install --save-dev webpack-node-externals webpack-dev-middleware webpack-hot-middleware webpack-hot-server-middleware webpack-merge babel-cli babel-preset-es2015

如何做...

现在让我们来看一下渲染的步骤:

  1. 首先,我们需要将 npm 脚本添加到我们的package.json文件中:
    "scripts": {
      "clean": "rm -rf dist/ && rm -rf public/app",
      "start": "npm run clean & NODE_ENV=development 
 BABEL_ENV=development 
 nodemon src/server --watch src/server --watch src/shared -- 
      exec babel-node --presets es2015",
      "start-analyzer": "npm run clean && NODE_ENV=development 
 BABEL_ENV=development ANALYZER=true babel-node src/server"
    }

文件:package.json

  1. 现在我们需要更改我们的webpack.config.js文件。因为我们要实现 SSR,我们需要将我们的 Webpack 配置分成客户端配置和服务器配置,并将它们作为数组返回。文件应该看起来像这样:
  // Webpack Configuration (Client & Server)
  import clientConfig from './webpack/webpack.config.client';
  import serverConfig from './webpack/webpack.config.server';

 export default [
    clientConfig,
    serverConfig
  ];

文件:webpack.config.js

  1. 现在我们需要在我们的webpack文件夹内为我们的客户端配置创建一个文件。我们需要将其命名为webpack.config.client.js
  // Dependencies
  import webpackMerge from 'webpack-merge';

  // Webpack Configuration
  import commonConfig from './webpack.config.common';
  import {
    context,
    devtool,
    entry,
    name,
    output,
    optimization,
    plugins,
    target
  } from './configuration';

  // Type of Configuration
  const type = 'client';

  export default webpackMerge(commonConfig(type), {
    context: context(type),
    devtool,
    entry: entry(type),
    name: name(type),
    output: output(type),
    optimization,
    plugins: plugins(type),
    target: target(type)
  });

文件:webpack/webpack.config.client.js

  1. 现在服务器配置应该是这样的:
 // Dependencies
  import webpackMerge from 'webpack-merge';

  // Webpack Configuration
  import commonConfig from './webpack.config.common';

  // Configuration
  import {
    context,
    entry,
    externals,
    name,
    output,
    plugins,
    target
  } from './configuration';

  // Type of Configuration
  const type = 'server';

 export default webpackMerge(commonConfig(type), {
    context: context(type),
    entry: entry(type),
    externals: externals(type),
    name: name(type),
    output: output(type),
    plugins: plugins(type),
    target: target(type)
  });

文件:webpack/webpack.config.server.js

  1. 正如您所看到的,在这两个文件中,我们正在导入一个包含需要添加到客户端和服务器端的配置的共同配置文件:
  // Configuration
  import { module, resolve, mode } from './configuration';
  export default type => ({
    module: module(type),
    resolve,
    mode
  });

文件:webpack/webpack.config.common.js

  1. 我们需要为 Webpack 节点添加新的配置文件,并修改一些我们已经有的文件。我们需要创建的第一个文件是context.js。在这个文件(和其他一些文件)中,我们将导出一个带有类型参数的函数,该参数可以是clientserver,并根据该值返回不同的配置:
  // Dependencies
  import path from 'path';
 export default type => type === 'server'
    ? path.resolve(__dirname, '../../src/server')
    : path.resolve(__dirname, '../../src/client');

文件:webpack/configuration/context.js

  1. 入口文件是我们将添加到捆绑包中的所有文件的位置。我们的入口文件现在应该是这样的:
  // Environment
  const isDevelopment = process.env.NODE_ENV !== 'production';

 export default type => {
    if (type === 'server') {
      return './render/serverRender.js';
    }

    const entry = [];

    if (isDevelopment) {
      entry.push(
        'webpack-hot-middleware/client',
        'react-hot-loader/patch'
      );
    }

    entry.push('./index.jsx');

    return entry;
  };

文件:webpack/configuration/entry.js

  1. 我们需要创建一个名为 externals.js 的文件,其中包含我们不打包的模块(除非它们在白名单上):
  // Dependencies
  import nodeExternals from 'webpack-node-externals';

  export default () => [
    nodeExternals({
      whitelist: [/^redux\/(store|modules)/]
    })
  ];

文件:webpack/configuration/externals.js

  1. 此外,我们需要修改我们的module.js文件,根据环境或配置类型返回我们的规则:
  // Dependencies
  import ExtractTextPlugin from 'extract-text-webpack-plugin';

 // Environment
  const isDevelopment = process.env.NODE_ENV !== 'production';

  export default type => {
    const rules = [
      {
        test: /\.(js|jsx)$/,
        use: 'babel-loader',
        exclude: /node_modules/
      }
    ];

    if (!isDevelopment || type === 'server') {
      rules.push({
        test: /\.scss$/,
        use: ExtractTextPlugin.extract({
          fallback: 'style-loader',
          use: [
            'css-loader?minimize=true&modules=true&localIdentName=
            [name]__[local]_[hash:base64]',
            'sass-loader'
          ]
        })
      });
    } else {
      rules.push({
        test: /\.scss$/,
        use: [
          {
            loader: 'style-loader'
          },
          {
            loader: 'css-loader',
            options: {
              modules: true,
              importLoaders: 1,
              localIdentName: '[name]__[local]_[hash:base64]',
              sourceMap: true,
              minimize: true
            }
          },
          {
            loader: 'sass-loader'
          }
        ]
      });
    }

    return {
      rules
    };
  };

文件:webpack/configuration/module.js

  1. 现在我们需要为名称创建一个节点:
  export default type => type;

文件:webpack/configuration/name.js

  1. 对于输出配置,我们需要根据配置的类型(客户端或服务器端)返回一个对象:
  // Dependencies
  import path from 'path';

 export default type => {
    if (type === 'server') {
      return {
        filename: 'server.js',
        path: path.resolve(__dirname, '../../dist'),
        libraryTarget: 'commonjs2'
      };
    }

    return {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, '../../public/app'),
      publicPath: '/'
    };
  };

文件:webpack/configuration/output.js

  1. 在我们的plugins.js文件中,我们正在验证用户是否发送了ANALYZER变量,以便在开发模式下运行应用程序时仅在该情况下显示BundleAnalyzerPlugin,而不是每次都显示。
  // Dependencies
  import CompressionPlugin from 'compression-webpack-plugin';
  import ExtractTextPlugin from 'extract-text-webpack-plugin';
  import webpack from 'webpack';
  import WebpackNotifierPlugin from 'webpack-notifier';
  import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';

 // Environment
  const isDevelopment = process.env.NODE_ENV !== 'production';

  // Analyzer
  const isAnalyzer = process.env.ANALYZER === 'true';

 export default type => {
    const plugins = [
      new ExtractTextPlugin({
        filename: '../../public/css/style.css'
      })
    ];

    if (isAnalyzer) {
      plugins.push(
        new BundleAnalyzerPlugin()
      );
    }

    if (isDevelopment) {
      plugins.push(
        new webpack.HotModuleReplacementPlugin(),
        new webpack.NoEmitOnErrorsPlugin(),
        new WebpackNotifierPlugin({
          title: 'CodeJobs'
        })
      );
    } else {
      plugins.push(
        new CompressionPlugin({
          asset: '[path].gz[query]',
          algorithm: 'gzip',
          test: /\.js$|\.css$|\.html$/,
          threshold: 10240,
          minRatio: 0.8
        })
      );
    }

    return plugins;
  };

文件:webpack/configuration/plugins.js

  1. 我们需要在我们的解析文件中指定我们的模块;文件应该是这样的:
  // Dependencies
  import path from 'path';

  export default {
    extensions: ['.js', '.jsx'],
    modules: [
      'node_modules',
      path.resolve(__dirname, '../../src/client'),
      path.resolve(__dirname, '../../src/server')
    ]
  };

文件:webpack/configuration/resolve.js

  1. 我们需要创建的最后一个配置是target.js文件:
 export default type => type === 'server' ? 'node' : 'web';

文件:webpack/configuration/target.js

  1. 在我们配置了 Webpack 之后,我们需要修改我们的App.jsx文件,其中我们需要使用<BrowserRouter>组件为客户端创建路由,并使用<StaticRouter>为服务器端创建路由:
  // Dependencies
  import React from 'react';
  import { 
    BrowserRouter, 
    StaticRouter, 
    Switch, 
    Route 
  } from 'react-router-dom';

 // Components
  import About from '@components/About';
  import Home from '@components/Home';

  export default ({ server, location, context = {} }) => {
    const routes = (
      <Switch>
        <Route exact path="/" component={Home} />
        <Route exact path="/about" component={About} />
      </Switch>
    );

    // Client Router
    let router = (
      <BrowserRouter>
        {routes}
      </BrowserRouter>
    );

    // Server Router
    if (server) {
      router = (
        <StaticRouter location={location} context={context}>
          {routes}
        </StaticRouter>
      );
    }

    return router;
  };

文件:src/client/App.jsx

  1. 现在我们需要修改我们的服务器文件(index.js)以使用我们的clientRenderserverRender中间件:
  // Dependencies
  import express from 'express';
  import path from 'path';
  import webpackDevMiddleware from 'webpack-dev-middleware';
  import webpackHotMiddleware from 'webpack-hot-middleware';
  import webpackHotServerMiddleware from 'webpack-hot-server-middleware';
  import webpack from 'webpack';

  // Utils
  import { isMobile, isBot } from '@utils/device';

  // Client Render
  import clientRender from './render/clientRender';

  // Webpack Configuration
  import webpackConfig from '@webpack';

  // Environment
  const isProduction = process.env.NODE_ENV === 'production';

  // Express Application
  const app = express();

  // Webpack Compiler
  const compiler = webpack(webpackConfig);

  // Public directory
  app.use(express.static(path.join(__dirname, '../../public')));

  // Device Detection
  app.use((req, res, next) => {
    req.isMobile = isMobile(req.headers['user-agent']);
    // We detect if a search bot is accessing...
    req.isBot = isBot(req.headers['user-agent']);

    next();
  });

  // Webpack Middleware
  if (!isProduction) {
    // Hot Module Replacement
    app.use(webpackDevMiddleware(compiler));
    app.use(webpackHotMiddleware(
      compiler.compilers.find(compiler => compiler.name === 'client'))
    );
  } else {
    // GZip Compression just for Production
    app.get('*.js', (req, res, next) => {
      req.url = `${req.url}.gz`;
      res.set('Content-Encoding', 'gzip');
      next();
    });
  }

  // Client Side Rendering
  app.use(clientRender());

  if (isProduction) {
    try {
      // eslint-disable-next-line
      const serverRender = require('../../dist/server.js').default; 

      app.use(serverRender());
    } catch (e) {
      throw e;
    }
  }

  // For Server Side Rendering on Development Mode
  app.use(webpackHotServerMiddleware(compiler));

 // Disabling x-powered-by
  app.disable('x-powered-by');

  // Listen Port...
  app.listen(3000);

文件:src/server/index.js

  1. 我们需要修改我们的clientRender.js文件。如果我们检测到一个搜索引擎爬虫使用isBot函数,我们将返回next()中间件。否则,我们渲染 HTML 并使用 CSR 执行应用程序:
  // HTML
  import html from './html';

  // Initial State
  import initialState from './initialState';

  export default function clientRender() {
    return (req, res, next) => {
      if (req.isBot) {
        return next();
      }

      res.send(html({
        title: 'Codejobs',
        initialState: initialState(req)
      }));
    };
  }

文件:src/server/render/clientRender.js

  1. 现在让我们创建我们的serverRender.js文件。在这里,我们需要使用react-dom/server库中的renderToString方法来渲染我们的App组件:
  // Dependencies
  import React from 'react';
  import { renderToString } from 'react-dom/server';
  import { Provider } from 'react-redux';

  // Redux Store
  import configureStore from '@configureStore';

  // Components
  import App from '../../client/App';

  import html from './html';

  // Initial State
  import initialState from './initialState';

  export default function serverRender() {
    return (req, res, next) => {
      // Configuring Redux Store
      const store = configureStore(initialState(req));

      const markup = renderToString(
        <Provider store={store}>
          <App
            server
            location={req.url}
          />
        </Provider>
      );

      res.send(html({
        title: 'Codejobs',
        markup,
        initialState: initialState(req)
      }));
    };
  }

文件:src/server/render/serverRender.js

它是如何工作的...

您可以通过运行npm start命令启动应用程序。

如果您在浏览器(例如 Chrome)中打开http://localhost:3000中的应用程序,然后右键单击,然后查看页面源代码,您可能会注意到我们没有使用 SSR:

这是因为我们将仅将 SSR 用于搜索引擎爬虫。isBot函数将检测所有搜索引擎爬虫,仅供测试,我添加了curl作为一个爬虫来测试我们的 SSR;这是该函数的代码:

  export function isBot(ua) {
    const b = /curl|bot|googlebot|google|baidu|bing|msn|duckduckgo|teoma|slurp|yandex|crawler|spider|robot|crawling/i;
    return b.test(ua);
  }

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

在另一个终端中打开一个新的终端,然后执行以下命令:

    curl http://localhost:3000

如您所见,#root div 内的 HTML 代码是使用 SSR 渲染的。

另外,如果您想尝试在 curl 中运行/about,您将看到它也将使用 SSR 进行渲染:

Chrome 有一个名为 User-Agent Switcher for Chrome 的扩展,您可以在其中指定要在浏览器中使用的用户代理。通过这种方式,您可以为 Googlebot 添加一个特殊的用户代理,例如:

然后,如果您在 User-Agent Switcher 中选择 Chrome | Bot,您会发现当您查看页面源代码时,HTML 代码会将其呈现为 SSR:

还有更多...

当我们使用 SSR 时,当我们尝试在客户端使用 window 对象时,我们必须非常小心。如果您直接使用 SSR,您将收到 ReferenceError,例如:

    ReferenceError: window is not defined

为了解决这个问题,您可以验证 window 对象是否存在,但这可能非常重复。我更喜欢创建一个可以验证我们是使用浏览器(客户端)还是服务器的函数。您可以这样做:

    export function isBrowser() {
      return typeof window !== 'undefined';
    }

然后,每次您需要使用 window 对象时,可以这样做:

 const store = isBrowser() ? configureStore(window.initialState) : {};

使用服务器端渲染实现承诺

在上一个示例中,我们看到了 SSR 的工作原理,但该示例仅限于显示具有简单组件的 SSR。在本示例中,我们将学习如何实现承诺以将我们的组件连接到 Redux,使用 API 获取数据并使用 SSR 渲染组件。

准备就绪

我们将使用上一个步骤中的相同代码,但我们将进行一些更改。在这个步骤中,我们需要安装这些软件包:

    npm install axios babel-preset-stage-0 react-router-dom redux-devtools-extension redux-thunk

如何做...

对于这个步骤,我们将实现一个基本的待办事项列表,从 API 中拉取数据,以展示如何使用 SSR 将 Redux 连接到我们的应用程序中:

  1. 我们需要做的第一件事是添加一个简单的 API 来显示待办事项列表:
  import express from 'express';

  const router = express.Router();

  // Mock data, this should come from a database....
  const todo = [
    {
      id: 1,
      title: 'Go to the Gym'
    },
    {
      id: 2,
      title: 'Dentist Appointment'
    },
    {
      id: 3,
      title: 'Finish homework'
    }
  ];

  router.get('/todo/list', (req, res, next) => {
    res.json({
      response: todo
    });
  });

  export default router;

文件:src/server/controllers/api.js

  1. 第二步是将这个 API 控制器导入到我们的src/server/index.js文件中,并将其添加为/api路由的中间件:
  ...
  // Controllers
  import apiController from './controllers/api';
  ...
  // Express Application
  const app = express();

  // Webpack Compiler
  const compiler = webpack(webpackConfig);

  // Routes
  app.use('/api', apiController);
  ...

文件:src/server/index.js

  1. 以前,在我们的serverRender.js文件中,我们直接渲染了我们的App组件。现在我们需要从具有名为initialAction的静态方法的组件中获取承诺,将它们保存到一个承诺数组中,解决它们,然后渲染我们的App方法:
  // Dependencies
  import React from 'react';
  import { renderToString } from 'react-dom/server';
  import { Provider } from 'react-redux';
  import { matchPath } from 'react-router-dom';

  // Redux Store
  import configureStore from '@configureStore';

  // Components
  import App from '../../client/App';

  // HTML
  import html from './html';

  // Initial State
  import initialState from './initialState';

  // Routes
  import routes from '@shared/routes';

  export default function serverRender() {
    return (req, res, next) => {
      // Configuring Redux Store
      const store = configureStore(initialState(req));

      // Getting the promises from the components which has  
      // initialAction.
      const promises = routes.paths.reduce((promises, route) => {
        if (matchPath(req.url, route) && route.component && route.component.initialAction) {          
promises.push(Promise.resolve(store.dispatch(route.component.initialAction())));
        }

        return promises;
      }, []);

      // Resolving our promises
      Promise.all(promises)
        .then(() => {
          // Getting Redux Initial State
          const initialState = store.getState();

          // Rendering with SSR
          const markup = renderToString(
            <Provider store={store}>
              <App
                server
                location={req.url}
              />
            </Provider>
          );

          // Sending our HTML code.
          res.send(html({
            title: 'Codejobs',
            markup,
            initialState
          }));
        })
        .catch(e => {
          // eslint-disable-line no-console
          console.log('Promise Error: ', e); 
        });
    };
  }

文件:src/server/render/serverRender.js

  1. 在这个步骤中,我们需要在客户端目录中稍微改变我们的文件夹结构。以前,我们有一个components目录,我们的组件都在里面。现在我们要将我们的组件封装为小应用程序,在里面我们可以创建我们的操作、API、组件、容器和减速器。我们的新结构应该是这样的:

  1. 我们将创建一个待办事项应用程序。为此,首先我们需要添加我们的操作文件夹,在里面我们需要首先创建我们的actionTypes.js文件。在这个文件中,我们需要添加我们的FETCH_TODO操作。我更喜欢创建一个具有两个函数的对象,一个用于请求,另一个用于成功;当我们在减速器中使用它们并分发我们的操作时,你将看到这一点的优势:
  // Actions
  export const FETCH_TODO = {
    request: () => 'FETCH_TODO_REQUEST',
    success: () => 'FETCH_TODO_SUCCESS'
  };

文件:src/client/todo/actions/actionTypes.js

  1. 在我们的index.js文件中,我们将创建一个 fetchTodo 操作,从我们的 API 中检索我们的待办事项列表项:
  // Base Actions
  import { request, received } from '@baseActions';

  // Api
  import api from '../api';

  // Action Types
  import { FETCH_TODO } from './actionTypes';

  export const fetchTodo = () => dispatch => {
    const action = FETCH_TODO;
    const { fetchTodo } = api;

    dispatch(request(action));

    return fetchTodo()
      .then(response => dispatch(received(action, response.data)));
  };

文件:src/client/todo/actions/index.js

  1. 正如你所看到的,我们正在使用基本操作中的两种特定方法(请求和接收)。这些函数将帮助我们轻松地分发我们的操作(你还记得我们在操作中使用了请求和成功方法吗?):
  // Base Actions
  export const request = ACTION => ({
    type: ACTION.request()
  });

  export const received = (ACTION, data) => ({
    type: ACTION.success(),
    payload: data
  });

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

  1. 现在让我们创建我们的api文件夹,在这里我们需要添加一个constants.js文件和我们的index.js文件:
 export const API = Object.freeze({
    TODO: 'api/todo/list'
  });

文件:src/client/todo/api/constants.js

  1. 在我们的index.js文件中,我们必须创建我们的 Api 类并添加一个名为fetchTodo的静态方法:
  // Dependencies
  import axios from 'axios';

  // Configuration
  import config from '@configuration';

  // Utils
  import { isBrowser } from '@utils/frontend';

  // Constants
  import { API } from './constants';

  class Api {
    static fetchTodo() {
      // For Node (SSR) we have to specify our base domain  
      // (http://localhost:3000/api/todo/list)
 // For Client Side Render just /api/todo/list.
      const url = isBrowser()
        ? API.TODO
        : `${config.baseUrl}/${API.TODO}`;

      return axios(url);
    }
  }

 export default Api;

文件:src/client/todo/api/index.js

  1. 在我们的 Todo 容器中,我们需要映射我们的待办事项列表,并将fetchTodo动作添加到 Redux 中。我们将导出一个布局组件,然后我们将添加我们的其他组件,并操纵我们想要显示布局的方式:
  // Dependencies
  import { connect } from 'react-redux';
  import { bindActionCreators } from 'redux';

  // Components
  import Layout from '../components/Layout';

  // Actions
  import { fetchTodo } from '../actions';

  export default connect(({ todo }) => ({
    todo: todo.list
  }), dispatch => bindActionCreators(
    {
      fetchTodo
    },
    dispatch
  ))(Layout);

文件:src/client/todo/container/index.js

  1. 我们的布局组件应该是这样的:
  // Dependencies
  import React from 'react';

  // Shared Components
  import Header from '@layout/Header';
  import Content from '@layout/Content';
  import Footer from '@layout/Footer';

  // Componenets
  import Todo from '../components/Todo';

  const Layout = props => (
    <main>
      <Header {...props} />
      <Content>
        <Todo {...props} />
      </Content>
      <Footer {...props} />
    </main>
  );
  export default Layout;

文件:src/client/todo/components/Layout.jsx

  1. 在这个教程中,我们不会看到布局组件(Header、Content 和 Footer),因为它们非常通用,我们在过去的教程中已经使用过它们。现在让我们创建我们的 reducer 文件:
  // Utils
  import { getNewState } from '@utils/frontend';

  // Action Types
  import { FETCH_TODO } from '../actions/actionTypes';

  // Initial State
  const initialState = {
    list: []
  };
  export default function todoReducer(state = initialState, action) {
    switch (action.type) {
      case FETCH_TODO.success(): {
        const { payload: { response = [] } } = action;

        return getNewState(state, {
          list: response
        });
      }

      default:
        return state;
    }
  }

文件:src/client/todo/reducer/index.js

  1. 我们的 Todo 组件将在 componentDidMount 方法中执行我们的 fetchTodo 动作,然后我们将待办事项列表呈现为 HTML 列表;非常简单:
  // Dependencies
  import React, { Component } from 'react';

  // Utils
  import { isFirstRender } from '@utils/frontend';

  // Styles
  import styles from './Todo.scss';

  class Todo extends Component {
    componentDidMount() {
      const { fetchTodo } = this.props;

      fetchTodo();
    }

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

      if (isFirstRender(todo)) {
        return null;
      }

      return (
        <div>
          <div className={styles.Todo}>
            <ol>
              {todo.map((item, key) => 
                <li key={key}>{item.title}</li>)}
            </ol>
          </div>
        </div>
      );
    }
  }

 export default Todo;

文件:src/client/todo/components/Todo.jsx

  1. 最后,我们需要为我们的待办事项应用程序创建一个index.jsx文件,在这个文件中,我们将添加我们的 initialAction(这将返回一个承诺)来执行我们的 fetchTodo 动作,并使用 SSR 呈现这个待办事项列表:
  // Dependencies
  import React from 'react';

  // Actions
  import { fetchTodo } from './actions';

  // Main Container
  import Container from './container';

 // Main Component
  const Main = props => <Container {...props} />;

  // Initial Action
  Main.initialAction = () => fetchTodo();

 export default Main;

文件:src/client/todo/index.jsx

它是如何工作的...

正如您在我们的serverRender.js文件中所看到的,我们获取承诺并解决它们,然后我们使用 SSR 渲染我们的应用程序。

如果您想测试该应用程序,您需要在浏览器中转到 http://localhost:3000/todo。

请记住,在我们的应用程序中,我们只是为搜索引擎爬虫和 curl 使用 SSR,否则将使用 CSR。这是因为我们必须使用 SSR 的唯一原因是为了改善我们在 Google、Yahoo 和 Bing 中的 SEO。

如果我们使用 CSR,我们将在 Todo 组件的componentDidMount()方法中执行我们的动作;如果我们使用 SSR,我们将使用initialAction方法,该方法返回一个将在serverRender.js中解决的承诺。

如果您打开页面,您应该会看到这个:

如果您想查看 SSR 是否正常工作,可以使用curl命令并在终端中执行相同的 URL:

正如您所看到的,待办事项列表 reducer 已添加到initialState中,从那里,我们可以使用 SSR 渲染列表。

实施 Next.js

Next.js 是一个用于服务器渲染的 React 应用程序的极简框架。

在这个教程中,我们将学习如何使用 Sass 实现 Next.js,并且我们还将使用 axios 从服务中获取数据。

准备工作

首先,让我们创建一个名为nextjs的新目录,初始化package.json,最后在其中创建一个新目录:

 mkdir nextjs
 cd nextjs
 npm init -y
 mkdir src

然后我们需要安装一些依赖项:

 npm install next react react-dom axios node-sass @zeit/next-sass

如何做...

现在我们已经安装了依赖项,让我们创建我们的第一个 Next.js 应用程序:

  1. 我们需要做的第一件事是在我们的 package.json 中创建一些脚本。在每个脚本中,我们需要指定src目录。否则,它将尝试从根目录而不是src路径启动 Next:
  "scripts": {
    "start": "next start src",
    "dev": "next src",
    "build": "next build src"
  }

文件:package.json

  1. Next 中的主目录称为pages。这是我们将使用 Next 渲染的所有pages的位置:
 cd src && mkdir pages
  1. 我们需要创建的第一个页面是index.jsx
 const Index = () => <h1>Home</h1>;

  export default Index;

文件:src/pages/index.jsx

  1. 现在让我们使用 dev 脚本运行我们的应用程序:
 npm run dev
  1. 如果一切正常,您应该在终端中看到这个:

  1. 打开http://localhost:3000

Next.js 有自己的 Webpack 配置和热重载功能。这意味着如果您编辑 index.js 文件,您将看到反映这些更改而无需刷新页面。

  1. 现在让我们创建一个关于页面,看看路由是如何工作的:
 const About = () => <h1>About</h1>;

  export default About;

文件:src/pages/about.jsx

  1. 现在,如果您转到 http://localhost:3000/about,您将看到关于页面。正如您所看到的,Next.js 会自动为我们创建的每个页面创建一个新路由。这意味着我们不需要安装 React Router 来处理路由。

在 Next 页面中,不需要导入 React,因为 Next 也会自动处理它。

  1. 现在我们需要创建一个next.config.js文件,并导入 withSass 方法来在我们的项目中使用 Sass。不幸的是,这个文件需要用 ES5 语法编写,因为目前不支持使用 ES6 的 babel 扩展(github.com/zeit/next.js/issues/2916):
 const withSass = require('@zeit/next-sass');

  module.exports = withSass();

文件:src/next.config.js 在这个文件中,如果需要,我们还可以添加自定义的 Webpack 配置。

  1. 然后我们需要在pages目录中创建一个特殊的文件叫做_document.js。这个文件会被 Next.js 自动处理,我们可以在这里定义文档的头部和正文:
  import Document, { Head, Main, NextScript } from 'next/document';

  export default class MyDocument extends Document {
    render() {
      return (
        <html>
          <Head>
            <title>Codejobs with Next</title>
            <link 
 rel="stylesheet" 
 href="/_next/static/style.css" />
          </Head>

          <body>
            <Main />
            <NextScript />
          </body>
        </html>
      );
    }
  }

文件:src/pages/_document.jsCSS 文件的路径(/_next/static/style.css)是默认的;我们应该使用它来在我们的项目中使用样式。

  1. 现在我们可以创建一些组件来包装我们的页面。我们需要创建的第一个是菜单选项的导航栏:
  import Link from 'next/link';
  import './Navbar.scss';

  const Navbar = () => (
    <div className="navbar">
      <ul>
        <li>Codejobs</li>
        <li><Link href="/">Home</Link></li>
        <li><Link href="/about">About</Link></li>
      </ul>
    </div>
  )
 export default Navbar;

文件:src/components/Navbar.jsx Link 组件与 React Router Link 不同。有一些区别;例如,React Router Link 使用“to”prop,而 Next Link 使用“href”来指定 URL。

  1. 现在我们可以为我们的navbar添加 Sass 样式:
 .navbar {
    background: black;
    color: white;
    height: 60px;

    ul {
      padding: 0;
      margin: 0;
      list-style: none;

      li {
        display: inline-block;
        margin-left: 30px;
        text-align: center;

        a {
          display: block;
          color: white;
          line-height: 60px;
          width: 150px;

          &:hover {
            background: white;
            color: black;
          }
        }
      }
    }
  }

文件:src/components/Navbar.scss

  1. 然后我们需要创建我们的 Layout 组件:
  import Navbar from './Navbar';
  import './Layout.scss';

  const Layout = ({ children }) => (
    <div className="layout">
      <Navbar />

      <div className="wrapper">
        {children}
      </div>
    </div>
  )

  export default Layout;

文件:src/components/Layout.jsx

  1. 我们的 Layout 的样式如下:
  body {
    font-family: verdana;
    padding: 0;
    margin: 0;
  }

  .layout {
    a {
      text-decoration: none;
    }

    .wrapper {
      margin: 0 auto;
      width: 96%;
    }
  }

文件:src/components/Layout.scss

  1. 你还记得第五章中的配方,精通 Redux,关于从 CoinMarketCap 列出前 100 个加密货币(Repository: Chapter05/Recipe2/coinmarketcap)吗?在这个配方中,我们将使用 Next.js 做同样的事情。我们需要做的第一件事是修改页面的index.js文件,并在getInitialProps方法中进行异步axios请求:
  import axios from 'axios';
  import Layout from '../components/Layout';
  import Coins from '../components/Coins';

  const Index = ({ coins }) => (
    <Layout>
      <div className="index">
        <Coins coins={coins} />
      </div>
    </Layout>
  );

  Index.getInitialProps = async () => {
    const url = 'https://api.coinmarketcap.com/v1/ticker/';
    const res = await axios.get(url);

    return {
      coins: res.data
    };
  };

 export default Index;

文件:src/pages/index.js

  1. 现在让我们创建Coins组件:
  // Dependencies
  import React, { Component } from 'react';
  import { array } from 'prop-types';

  // Styles
  import './Coins.scss';

  const Coins = ({ coins }) => (
    <div className="Coins">
      <h1>Top 100 Coins</h1>

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

  Coins.propTypes = {
    coins: array
  };

 export default Coins;

文件:src/components/Coins.jsx

  1. Coins组件的样式如下:
 .Coins {
    h1 {
      text-align: center;
    }

    ul {
      margin: 0 auto;
      margin-bottom: 20px;
      padding: 0;
      list-style: none;
      width: 400px;

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

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

          &:hover {
            color: #333;
            text-decoration: none;
            background: #baecff;
          }
        }
      }
    }
  }

文件:src/components/Coins.scss

它是如何工作的...

现在我们已经创建了所有页面和组件,让我们通过运行npm run dev来测试我们的 Next 应用程序:

现在让我们看看它在 HTML 视图中是如何渲染的:

万岁!HTML 以 SSR 方式呈现,非常适合改善 SEO。正如你所看到的,使用 Next 创建应用程序非常快速,而且在启用 SSR 时避免了大量的配置。

第十二章:测试和调试

在本章中,将涵盖以下教程:

  • 使用 Jest 和 Enzyme 测试我们的第一个组件

  • 测试 Redux 容器、操作和减速器

  • 使用 React 和 Redux Dev Tools 调试 React 应用程序

  • 模拟事件

介绍

测试和调试对于任何希望具有高质量的项目来说都非常重要。不幸的是,许多开发人员不关心测试(单元测试),因为他们认为这会减慢开发速度,有些人把它留到项目结束时再做。根据我的个人经验,我可以说,从项目开始就进行测试会节省您的时间,因为最后您将有更少的错误需要修复。React 使用 Jest 来测试其组件、容器、操作和减速器。

在接下来的教程中,我们还将学习如何调试我们的 React/Redux 应用程序。

使用 Jest 和 Enzyme 测试我们的第一个组件

在这个教程中,我们将学习如何在项目中安装和配置 Jest。

准备工作

在这个教程中,我们需要安装一些包来测试我们的 React 应用程序:

npm install --save-dev jest jsdom enzyme enzyme-adapter-react-16 identity-obj-proxy

如何做…

安装了 Jest 之后,我们需要对其进行配置:

  1. tests脚本和 Jest 配置添加到我们的package.json中:
  {
    "name": "react-pro",
    "version": "1.0.0",
    "scripts": {
      "clean": "rm -rf dist/ && rm -rf public/app",
      "start": "npm run clean & NODE_ENV=development 
      BABEL_ENV=development nodemon src/server --watch src/server --
      watch src/shared --exec babel-node --presets es2015",
      "start-analyzer": "npm run clean && NODE_ENV=development 
      BABEL_ENV=development ANALYZER=true babel-node src/server",
      "test": "node scripts/test.js src --env=jsdom",
      "coverage": "node scripts/test.js src --coverage --env=jsdom"
    },
    "jest": {
      "setupTestFrameworkScriptFile": "
 <rootDir>/config/jest/setupTestFramework.js",
      "collectCoverageFrom": [
        "src/**/*.{js,jsx}"
      ],
      "setupFiles": [
        "<rootDir>/config/jest/browserMocks.js"
      ],
      "moduleNameMapper": {
        "^.+\\.(scss)$": "identity-obj-proxy"
      }
    },
    "author": "Carlos Santana",
    "license": "MIT",
    "dependencies": {
      "axios": "⁰.18.0",
      "babel-preset-stage-0": "⁶.24.1",
      "express": "⁴.15.4",
      "react": "¹⁶.3.2",
      "react-dom": "¹⁶.3.2",
      "react-redux": "⁵.0.6",
      "react-router-dom": "⁴.2.2",
      "redux": "⁴.0.0",
      "redux-devtools-extension": "².13.2",
      "redux-thunk": "².2.0"
    },
    "devDependencies": {
      "babel-cli": "⁶.26.0",
      "babel-core": "⁶.26.0",
      "babel-eslint": "⁸.2.3",
      "babel-loader": "⁷.1.2",
      "babel-plugin-module-resolver": "³.1.1",
      "babel-preset-env": "¹.6.0",
      "babel-preset-es2015": "⁶.24.1",
      "babel-preset-react": "⁶.24.1",
      "compression-webpack-plugin": "¹.0.0",
      "css-loader": "⁰.28.5",
      "enzyme": "³.3.0",
      "enzyme-adapter-react-16": "¹.1.1",
      "eslint": "⁴.5.0",
      "eslint-plugin-babel": "⁵.1.0",
      "eslint-plugin-import": "².7.0",
      "eslint-plugin-jsx-a11y": "⁶.0.2",
      "eslint-plugin-react": "⁷.8.2",
      "eslint-plugin-standard": "³.0.1",
      "extract-text-webpack-plugin": "4.0.0-beta.0",
      "husky": "⁰.14.3",
      "identity-obj-proxy": "³.0.0",
      "jest": "²³.1.0",
      "jsdom": "¹¹.11.0",
      "node-sass": "⁴.5.3",
      "nodemon": "¹.17.4",
      "react-hot-loader": "⁴.2.0",
      "redux-mock-store": "¹.5.1",
      "sass-loader": "⁷.0.1",
      "style-loader": "⁰.21.0",
      "webpack": "⁴.8.3",
      "webpack-bundle-analyzer": "².9.0",
      "webpack-dev-middleware": "³.1.3",
      "webpack-hot-middleware": "².18.2",
      "webpack-hot-server-middleware": "⁰.5.0",
      "webpack-merge": "⁴.1.0",
      "webpack-node-externals": "¹.6.0",
      "webpack-notifier": "¹.6.0"
    }
  }

文件:package.json

  1. 正如您在我们的 Jest 配置中所看到的,我们需要添加setupTestFramework.js文件,我们将在其中配置我们的 enzyme 以便与 Jest 一起使用:
  import { configure } from 'enzyme';
  import Adapter from 'enzyme-adapter-react-16';

  configure({ adapter: new Adapter() });

文件:config/jest/setupTestFramework.js

  1. setupFiles节点中,我们可以指定我们的browserMocks.js文件,这是我们可以模拟在我们的应用程序中使用的任何浏览器方法的地方。例如,如果您想在应用程序中测试localStorage,这个文件就是模拟它的合适位置:
  // Browser Mocks
  const requestAnimationFrameMock = callback => {
    setTimeout(callback, 0);
  };

  Object.defineProperty(window, 'requestAnimationFrame', {
    value: requestAnimationFrameMock
  });

  const localStorageMock = (() => {
    let store = {}

    return {
      getItem: key => store[key] || null,
      setItem: (key, value) => store[key] = value.toString(),
      removeItem: key => delete store[key],
      clear: () => store = {}
    };
  })();

  Object.defineProperty(window, 'localStorage', {
    value: localStorageMock
  });

文件:config/jest/browserMocks.js

  1. 如果您在组件中使用 Sass、Stylus 或 Less,您需要使用正则表达式指定moduleNameMapper模式,以匹配项目中的所有.scss文件(或.styl/.less),并使用identity-obj-proxy处理这些文件,这是一个模拟 Webpack 导入的包,比如 CSS 模块。

  2. 您可能已经注意到我们添加了两个新的 NPM 脚本:一个用于测试我们的应用程序,另一个用于获取覆盖率(覆盖的单元测试百分比)。对于这些,我们使用了一个特殊的脚本,就在scripts/test.js,让我们创建这个文件:

  // Set the NODE_ENV to test
  process.env.NODE_ENV = 'test';

  // Requiring jest
  const jest = require('jest');

  // Getting the arguments from the terminal
  const argv = process.argv.slice(2);

  // Runing Jest passing the arguments
  jest.run(argv);

文件:scripts/test.js

  1. 让我们想象我们有这个Home组件:
  import React from 'react';
  import styles from './Home.scss';

  const Home = props => (
    <h1 className={styles.Home}>Hello {props.name || 'World'}</h1>
  );

 export default Home;

文件:src/client/home/index.jsx

  1. 如果您想测试这个组件,您需要创建一个同名的文件,但在文件名后面加上.test后缀。在这种情况下,我们的测试文件将被命名为index.test.jsx
  // Dependencies
  import React from 'react';
  import { shallow } from 'enzyme';

  // Component to test...
  import Home from './index';

  describe('Home', () => {
    const subject = shallow(<Home />);
    const subjectWithProps = shallow(<Home name="Carlos" />);

    it('should render Home component', () => {
      expect(subject.length).toBe(1);
    });

    it('should render by default Hello World', () => {
      expect(subject.text()).toBe('Hello World');
    });

    it('should render the name prop', () => {
      expect(subjectWithProps.text()).toBe('Hello Carlos');
    });

    it('should has .Home class', () => {
      expect(subject.find('h1').hasClass('Home')).toBe(true);
    });
  });

文件:src/client/home/index.test.jsx

它是如何工作的...

如果您想测试您的应用程序,您需要运行以下命令:

    npm test

如果您的测试正确,您应该会看到这个结果:

PASS标签表示该文件中的所有测试都已成功通过;如果您至少有一个测试失败,您将看到FAIL标签。让我们修改我们的"should has .Home class测试。我将把值改为"Home2"以强制失败:

正如您所见,现在我们得到了FAIL标签,并用 X 指定了失败的测试。此外,ExpectedReceived值提供了有用的信息,通过这些信息,我们可以看到期望的值和接收到的值。

还有更多...

现在,如果您想查看所有单元测试的覆盖百分比,您可以使用以下命令:

 npm run coverage

现在我们只有 1 个Home组件的单元测试,如您所见是绿色的并且达到了 100%,所有其他文件都是红色的并且为 0%,因为它们还没有被测试:

此外,覆盖命令会生成结果的 HTML 版本。有一个名为"coverage"的目录,里面有一个名为"Icov-report"的目录。如果您在浏览器中打开index.html,您将看到类似于这样的内容:

测试 Redux 容器、操作和减速器

在这个示例中,我们将测试 Redux 容器、操作和减速器。在这个例子中,我们将测试我们在第十一章中创建的待办事项列表,实现服务器端渲染。

请记住,如果您使用现有的示例,您必须先运行 npm install 命令来恢复所有项目依赖项,否则您将收到依赖错误。

准备就绪

我们需要安装redux-mock-storemoxiosredux-thunk包来测试我们的 Redux 容器。您需要先运行npm install来安装所有依赖项:

    npm install // This is to install the previous packages
    npm install redux-mock-store moxios redux-thunk

如何做...

让我们测试我们的 Redux 容器:

  1. Redux 容器不应该有任何 JSX 代码;最佳实践是在我们的connect方法中使用mapStateToPropsmapDispatchToProps,在导出时传递另一个组件(比如Layout组件),例如,让我们看看我们的 Todo List 容器:
  // Dependencies
  import { connect } from 'react-redux';
  import { bindActionCreators } from 'redux';

 // Components
  import Layout from '../components/Layout';

  // Actions
  import { fetchTodo } from '../actions';

  export default connect(({ todo }) => ({
    todo: todo.list
  }), dispatch => bindActionCreators(
    {
      fetchTodo
    },
    dispatch
  ))(Layout);

文件:src/client/todo/container/index.js

  1. 您可能想知道我们需要在这里测试什么。嗯,在容器中最重要的事情是测试动作分发(fetchTodo动作)并从 Redux 中获取我们的todo状态和数据。也就是说,这是我们的容器单元测试文件:
  // Dependencies
  import React from 'react';
  import { shallow } from 'enzyme';
  import configureStore from 'redux-mock-store';

  // Actions
  import { fetchTodo } from '../actions';

  // Testable Container
  import Container from './index';

  // Mocking Initial State
  const mockInitialState = {
    todo: {
      list: [
        {
          id: 1,
          title: 'Go to the Gym'
        },
        {
          id: 2,
          title: 'Dentist Appointment'
        },
        {
          id: 3,
          title: 'Finish homework'
        }
      ]
    }
  };

  // Configuring Mock Store
  const mockStore = configureStore()(mockInitialState);

  // Mocking the Actions
  jest.mock('../actions', () => ({
    fetchTodo: jest.fn().mockReturnValue({ type: 'mock-FETCH_TODO_SUCCESS' })
  }));

  describe('Todo Container', () => {
    let mockParams;
    let container;

    beforeEach(() => {
      fetchTodo.mockClear();
      mockParams = {};
      mockStore.clearActions();
      container = shallow(<Container {...mockParams} store={mockStore} />);
    });

    it('should dispatch fetchTodo', () => {
      const { fetchTodo } = container.props();

      fetchTodo();

      const actions = mockStore.getActions();

      expect(actions).toEqual([{ type: 'mock-FETCH_TODO_SUCCESS' }]);
    });

    it('should map todo and get the todo list from Initial State', () => {
      const { todo } = container.props();
      const { todo: { list }} = mockInitialState;

      expect(todo).toEqual(list);
    });
  });

文件:src/client/todo/container/index.test.js

  1. 测试fetchTodo动作。这是我们的动作文件的代码:
  // Base Actions
  import { request, received } from '@baseActions';

 // Api
  import api from '../api';

  // Action Types
  import { FETCH_TODO } from './actionTypes';

  export const fetchTodo = () => dispatch => {
    const action = FETCH_TODO;
    const { fetchTodo } = api;

    dispatch(request(action));

    return fetchTodo()
      .then(response => dispatch(received(action, response.data)));
  };

文件:src/client/todo/actions/index.js

  1. 这是我们的actionTypes.js文件:
  // Actions
 export const FETCH_TODO = {
    request: () => 'FETCH_TODO_REQUEST',
    success: () => 'FETCH_TODO_SUCCESS'
  };

文件:src/client/todo/actions/actionTypes.js

  1. 要测试异步 Redux 动作,我们需要使用redux-thunkmoxios来测试使用axios从服务器检索数据的动作。我们的测试文件应该是这样的:
  // Dependencies
  import configureMockStore from 'redux-mock-store';
  import thunk from 'redux-thunk';
  import moxios from 'moxios';

  // Action
  import { fetchTodo } from './index';

  // Action Types
  import { FETCH_TODO } from './actionTypes';

  // Configuring Store with Thunk middleware
  const mockStore = configureMockStore([thunk]);

  // Response Mock
  const todoResponseMock = [
    {
      id: 1,
      title: 'Go to the Gym'
    },
    {
      id: 2,
      title: 'Dentist Appointment'
    },
    {
      id: 3,
      title: 'Finish homework'
    }
  ];

  describe('fetchTodo action', () => {
    beforeEach(() => {
      moxios.install();
    });

    afterEach(() => {
      moxios.uninstall();
    });

    it('should fetch the Todo List', () => {
      moxios.wait(() => {
        const req = moxios.requests.mostRecent();

        req.respondWith({
          status: 200,
          response: todoResponseMock
        });
      });

      const expectedActions = [
        {
          type: FETCH_TODO.request()
        },
        {
          type: FETCH_TODO.success(),
          payload: todoResponseMock
        }
      ];

      const store = mockStore({ todo: [] })

      return store.dispatch(fetchTodo()).then(() => {
        expect(store.getActions()).toEqual(expectedActions);
      });
    });
  });

文件:src/client/todo/actions/index.test.js

  1. 让我们测试我们的 reducer。这是 Todo reducer 文件:
  // Utils
  import { getNewState } from '@utils/frontend';

 // Action Types
  import { FETCH_TODO } from '../actions/actionTypes';

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

  export default function todoReducer(state = initialState, action) {
    switch (action.type) {
      case FETCH_TODO.success(): {
        const { payload: { response = [] } } = action;

        return getNewState(state, {
          list: response
        });
      }

      default:
        return state;
    }
  }

文件:src/client/todo/reducer/index.js

  1. 我们需要在我们的 reducer 中测试两件事:初始状态和FETCH_TODO动作成功时的状态。
  // Reducer
  import todo from './index';

  // Action Types
  import { FETCH_TODO } from '../actions/actionTypes';

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

  describe('Todo List Reducer', () => {
    it('should return the initial state', () => {
      const expectedInitialState = todo(undefined, {});

      expect(expectedInitialState).toEqual(initialState);
    });

    it('should handle FETCH_TODO when is success', () => {
      const action = {
        type: FETCH_TODO.success(),
        payload: {
          response: [
            {
              id: 1,
              title: 'Go to the Gym'
            },
            {
              id: 2,
              title: 'Dentist Appointment'
            },
            {
              id: 3,
              title: 'Finish homework'
            }
          ]
        }
      };

      const expectedState = {
        list: action.payload.response
      };

      const state = todo(initialState, action);

      expect(state).toEqual(expectedState);
    });
  });

文件:src/client/todo/reducer/index.test.js

使用 React 和 Redux Dev Tools 调试 React 应用程序

调试对于任何应用程序都是必不可少的,它帮助我们识别和修复错误。Chrome 有两个强大的工具来调试 React/Redux 应用程序,并将其集成到其开发者工具中。React Dev Tool 和 Redux Dev Tool。

准备就绪

使用 Google Chrome,您需要安装这两个扩展程序:

此外,您需要安装redux-devtools-extension包:

npm install --save-dev redux-devtools-extension 

安装了 React Developer Tools 和 Redux DevTools 之后,您需要对其进行配置。

如果您尝试直接使用 Redux DevTools,它不会起作用;这是因为我们需要将composeWithDevTools方法传递到我们的 Redux 存储中,这应该是我们的configureStore.js文件:

  // Dependencies
  import { createStore, applyMiddleware } from 'redux';
  import thunk from 'redux-thunk';
  import { composeWithDevTools } from 'redux-devtools-extension';

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

 export default function configureStore({ initialState, appName, 
  reducer }) {
    const middleware = [
      thunk
    ];

    return createStore(
      rootReducer,
      initialState,
      composeWithDevTools(applyMiddleware(...middleware))
    );
  }

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

如何做...

让我们调试我们的应用程序:

  1. 如果您想要调试您的 React 应用程序,请使用 Google Chrome 打开您的应用程序(http://localhost:3000/todo),打开 Google Dev Tools(右键单击>检查),选择 React 选项卡,您将看到您的 React 组件:

  1. 您可以选择要调试的组件,其中最酷的一件事是您可以在组件的右侧看到组件的 props:

  1. 如果您想要调试 Redux 并查看应用程序中正在分发的动作,您需要在 Chrome Dev Tools 中选择 Redux 选项卡:

  1. 在我们的 Todo 应用程序中,我们正在分发两个动作:FETCH_TODO_REQUESTFETCH_TODO_SUCCESS@@INIT动作在 Redux 中默认被分发,这在任何应用程序中都会发生。

  2. 如果您选择FETCH_TODO_REQUEST动作,您会看到在 Diff 选项卡上显示“(states are equal)”。这意味着在该动作中没有任何更改,但您有四个选项卡:Action、State、Diff 和 Test。

  3. 如果您选择 Action 选项卡,您可以看到特定的动作:

  1. 如果您选择FETCH_TODO_SUCCESS,您将看到 todo reducer 的数据:

模拟事件

在这个示例中,我们将学习如何模拟简单计算器组件上的onClickonChange事件。

如何做到...

我们将重用上一个示例的代码(Repository: Chapter12/Recipe3/debugging):

  1. 我们将创建一个简单的Calculator组件来对两个值(输入)进行求和,然后当用户点击等号(=)按钮时,我们将得到结果:
  import React, { Component } from 'react';
  import styles from './Calculator.scss';

  class Calculator extends Component {
    state = {
      number1: 0,
      number2: 0,
      result: 0
    };

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

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

    handleResult = () => {
      this.setState({
        result: Number(this.state.number1) + Number(this.state.number2)
      });
    }

    render() {
      return (
        <div className={styles.Calculator}>
          <h1>Calculator</h1>

          <input
            name="number1"
            value={this.state.number1}
            onChange={this.handleOnChange}
          />

          {' + '}

          <input
            name="number2"
            value={this.state.number2}
            onChange={this.handleOnChange}
          />

          <button onClick={this.handleResult}>
            =
          </button>

          <input
            name="result"
            value={this.state.result}
          />
        </div>
      );
    }
  }

  export default Calculator;

文件:src/client/calculator/index.jsx

  1. 如果您想要在浏览器中查看此组件(它是为测试目的而创建的),您需要在路由文件中包含它:
 **import** React **from** 'react';
  **import** { Switch, Route } **from** 'react-router-dom';

 **// Components**
  **import** Calculator **from** '../../client/calculator';

  **const** paths = [
    {
      **component**: Calculator,
      **exact**: true,
      **path**: '/'
    }
  ];

  **const** all = (
 **   <Switch>**
     ** <Route** **exact** **path**={paths[0].**path**} **component**={paths[0].**component**} />
    **</Switch>**
  );

 **export default** {
    paths,
    all
  }; 

文件:src/shared/routes/index.jsx

  1. 如果您想要查看一些基本样式,我们可以使用这些:
  .Calculator {
    padding: 100px;

    input {
        width: 50px;
        height: 50px;
        padding: 40px;
        font-size: 24px;
    }

    button {
        padding: 10px;
        margin: 10px;
    }
  }

文件:src/client/calculator/Calculator.scss

  1. 在我们的测试文件中,我们需要模拟onChange事件来改变输入的值,然后模拟点击等号(=)按钮:
 ** // Dependencies**
  **import** React **from** 'react';
  **import** { shallow } **from** 'enzyme';

  **// Component to test...**
  **import** Calculator **from** './index';

  **describe**('Calculator', () => {
    **const** subject = **shallow**(<Calculator />);

   ** it**('should render Calculator component', () => {
      **expect**(subject.**length**).**toBe**(1);
    });

   ** it**('should modify the state onChange', () => {
      subject.**find**('input[name="number1"]').**simulate**('change', {
        **target**: {
          **name**: 'number1',
          **value**: 5
        }
      });

      subject.**find**('input[name="number2"]').**simulate**('change', {
        **target**: {
          **name**: 'number2',
          **value**: 15
        }
      });

      **// Getting the values of the number1 and number2 states**
      **expect**(subject.**state**('number1')).**toBe**(5);
      **expect**(subject.**state**('number2')).**toBe**(15);
    });

   ** it**('should perform the sum when the user clicks the = button', 
    () => {
      **// Simulating the click event**
      subject.**find**('button').**simulate**('click');

      **// Getting the result value**
      **expect**(subject.**state**('result')).**toBe**(20);
    });
  }); 

它是如何工作的...

如果您想要在浏览器中查看组件,请使用npm start运行应用程序,您将看到类似于这样的东西:

现在让我们使用npm test命令来测试我们的计算器:

抱歉,我无法识别图片中的文本。

第十三章:部署到生产环境

在这一章中,将涵盖以下内容:

  • 在 Digital Ocean 上部署到生产环境

  • 在我们的 Droplet 中配置 Nginx、PM2 和域名

  • 实施 Jenkins(持续集成)

介绍

如果你正在阅读这一章,很可能是因为你已经完成了你的 React 应用程序(恭喜!)。现在是时候将其部署到生产环境并展示给世界了。在这一章中,我们将学习如何使用最好的云服务之一——Digital Ocean 来部署我们的 React 应用程序。

在这一点上,你需要投资一些钱来租用你需要的服务器。我会向你展示最便宜的方法来做到这一点,然后,如果你想增加服务器的功率,你将能够在不重新配置的情况下做到这一点。

在 Digital Ocean 上部署到生产环境

Digital Ocean 是我最喜欢的云计算平台,因为它非常容易创建、配置和删除 droplets,并且价格低廉(你可以每月得到一个 droplet,每月只需 5 美元,也就是每小时 0.007 美元)。我认为 Digital Ocean 很棒的另一个原因是他们的所有文档都是最新的,客户服务也很快解决你可能遇到的任何问题。

对于这个步骤,我们将使用 Ubuntu 18.04,所以你需要了解一些基本的 Linux 命令来配置你的 droplet。如果你完全是新手,不用担心,我会尽量以简单的方式解释每一步。

准备工作

首先,你需要创建你的 Digital Ocean 账户,访问www.digitalocean.com。你可以使用你的 Google 账户注册;这是推荐的方式。一旦你点击使用 Google 注册的链接,你将看到账单信息视图:

你可以注册你的信用卡/借记卡,或者你可以使用 PayPal 支付。一旦你配置好你的付款信息,你就可以创建你的第一个 Droplet 了:

如何做...

让我们创建我们的第一个 Droplet:

  1. 选择你的 Linux 发行版;正如我之前提到的,我们将使用 Ubuntu 18.04:

  1. 一键应用是预配置的 Droplet,但我更喜欢从头开始设置我的 Droplet,以便拥有控制权并能够优化我的配置。在此之后,如果您需要快速配置某些内容,可以查看这些选项:

  1. 选择您的 Droplet 的大小。我更喜欢使用 2GB 内存的 Droplet,每月费用为10 美元。也许您会想为什么我不选择最便宜的 1GB 内存版本;这是因为我曾尝试使用这个版本,但我注意到1GB 内存不足以处理安装包时的 NPM。大多数情况下,这会使您的 Droplet 挂起——我知道这听起来很荒谬,但 NPM 消耗大量内存。

  2. 如果您选择了 10 美元的 Droplet,您不必立即支付这笔钱。Digital Ocean 最好的一点是他们只会按您使用 Droplet 的时间收费。这意味着如果在完成此操作后(假设您花了 2 小时来完成它),您关闭(关机)您的 Droplet,您只会被收取 2 小时的费用,即0.030 美元。如果您将 Droplet 保持开启一个月(30 天),您将被收取 10 美元,所以不用担心:

  1. 选择数据中心区域;这将取决于您的位置。如果您在美国,您需要选择纽约或旧金山。您需要选择距离您位置最近的数据中心:

  1. 给您的 Droplet 命名。如果您需要多个 Droplet,您可以在这里选择数量:

  1. 点击“创建”按钮后,将需要 30-45 秒来创建您的 Droplet。完成后,您将看到您的 Droplet:

  1. 此时,您应该会收到一封包含服务器凭据的电子邮件:

  1. 在您的终端中,您可以使用ssh root@YOUR_DROPLET_IP命令访问您的 Droplet。第一次访问时,您将收到一条消息,要求将此 IP 添加到已知主机中,然后您需要输入 Droplet 密码:

  1. 如果一切正常,您将被要求更改您的 UNIX 密码。您需要粘贴当前密码,然后输入您想要的新密码并重新输入,之后您将连接到 Droplet:

  1. 让我们开始配置我们的 Droplet。安装 Node.js。为此,我们将使用 PPA 安装 Node 的最新版本。现在,Node 的当前版本是 10.x。如果在阅读此配方时,Node 有一个新版本,请在命令中更改版本(setup_10.x):
  cd ~
  curl -sL https://deb.nodesource.com/setup_10.x -o nodesource_setup.sh
  1. 一旦我们得到nodesource_setup.sh文件,运行以下命令:
 ** sudo bash** **nodesource_setup.sh**
  1. 要安装 Node,请运行以下命令:
 **sudo apt** **install** **nodejs -y**
  1. 如果您想验证刚刚安装的 Node 和 NPM 的版本,请运行:
 node -v
 v10.8.0
 npm -v
 6.2.0

它是如何工作的...

使用我们在第十一章中执行的一些配方,实施服务器端渲染,我创建了一个新的 GitHub 存储库,并将该代码推送到生产环境。您可以在github.com/csantany/production上看到这个存储库。

在我们的 Droplet 中,我们将克隆此 git 存储库(如果您已经准备好您的应用程序,请使用您的存储库)。生产存储库是公开的,但如果您使用私人存储库,则需要在 GitHub 帐户中添加 Droplet 的 SSH 密钥。为此,您需要在 Droplet 中运行ssh-keygen命令,然后按三次Enter而不写任何密码:

如果您的终端在五分钟内处于非活动状态,可能会关闭您的连接,您将不得不重新连接。

创建 SSH 密钥后,您可以通过执行以下操作查看它:vi /root/.ssh/id_rsa.pub。您需要复制 SSH 密钥并转到您的 GitHub 帐户|设置|SSH 和 GPG 密钥(github.com/settings/ssh/new)。然后将密钥粘贴到文本区域中,并为密钥添加一些标题。单击“添加 SSH 密钥”按钮时,GitHub 将要求您输入密码以确认:

现在我们可以使用git clone git@github.com:csantany/production.git克隆我们的存储库,或者您的存储库:

然后转到生产文件夹并安装 NPM 包:

    cd production
 npm install

要测试我们的应用程序,让我们运行 npm run start-production 脚本:

    npm run start-production

如果您想验证它是否有效,请转到浏览器并打开 Droplet 的 IP,然后添加端口 3000—在我的情况下将是http://178.128.177.84:3000,如果一切正常,您应该看到您的应用程序(在我们的情况下,我们将打开我们的/todo 部分):

还有更多...

如果您想关闭您的 Droplet,您可以转到电源部分,或者您可以使用开/关开关:

当您点击它时,您将会得到这个模态框:

配置 Nginx、PM2 和 Droplet 中的域

在这一点上,我们的第一个 Droplet 已经准备好使用,但我们可以看到我们的 React 应用程序使用端口 3000。在这个配方中,我们将学习如何在服务器上配置 Nginx 以及如何实现代理将流量从端口 80 重定向到 3000。这意味着我们不再需要直接指定我们的端口。PM2(Node 生产进程管理器)将帮助我们在生产环境中安全地运行我们的 Node 服务器。通常,如果我们直接使用nodebabel-node命令运行 Node,并且我们的应用程序出现错误,它将崩溃并停止工作;如果发生错误,PM2 将重新启动 Node 服务器。

准备就绪

对于这个配方,我们需要全局安装 PM2:

 npm install -g pm2

此外,我们需要安装 Nginx:

 sudo apt-get update
    sudo apt-get install nginx

如何做...

让我们从配置开始:

  1. 调整防火墙以允许流量只通过端口 80。要列出可用的应用程序配置,我们运行以下命令:
   **sudo** **ufw app list**

  ** Available applications:**
     ** Nginx Full**
 **Nginx HTTP**
 **Nginx HTTPS**
 **OpenSSH** 
  1. Nginx Full 意味着我们将允许端口 80(HTTP)和 443(HTTPS)的流量。在这一点上,我们还没有为 SSL 配置任何域,所以我们应该限制流量只通过端口 80(HTTP)传递:
   **sudo ufw allow** **'Nginx HTTP'**
  1. 如果我们尝试访问我们的 IP,我们应该看到我们的 Nginx 正在工作:

  1. 如果您想管理 Nginx 的进程,您可以使用这些命令:
  • 启动服务器sudo systemctl start nginx

  • 停止服务器sudo systemctl stop nginx

  • 重新启动服务器sudo systemctl restart nginx

  • 重新加载服务器sudo systemctl reload nginx

  • 禁用服务器sudo systemctl disable nginx

  1. 设置 Nginx 作为反向代理服务器,为此我们需要打开我们的 Nginx 配置文件:
 ** sudo vi** **/etc/nginx/sites-available/default**
  1. location /块中,我们需要将其替换为:
  location / {
    proxy_pass http://localhost:3000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection 'upgrade';
    proxy_set_header Host $host;
    proxy_cache_bypass $http_upgrade;
  }

它是如何工作的...

一旦您保存并关闭文件,我们需要验证是否有任何语法错误。使用以下命令:

    sudo nginx -t

如果一切正常,您应该看到:

最后,我们重新启动我们的 Nginx 服务器:

 **sudo systemctl restart nginx**

现在我们可以访问我们的 IP 而不带端口,React 应用程序将正常工作:

还有更多...

如果您想要在 Droplet 上使用域名,这非常简单;您需要将域名的 Nameservers 更改为指向 Digital Ocean 的 Nameservers。例如,我有一个名为 educnow.com 的域名,我将用于我的 Droplet。我在 Godaddy 注册了这个域名,所以我必须转到域名管理并选择它。您可以直接转到https://dcc.godaddy.com/manage/YOURDOMAIN.COM/dns URL。然后转到 Nameservers:

我们必须点击“更改”按钮,选择“自定义”,指定 Digital Ocean Nameservers,并点击“保存”:

一旦您修改了 Nameservers,您需要转到 Droplet 仪表板并选择“添加域名”选项:

然后输入要链接到 Droplet 的域名,然后单击“添加域名”:

现在您需要为 CNAME 创建一个新记录。选择 CNAME 选项卡,在主机名中写入www,在别名字段中写入@,默认情况下 TTL 为43200—这是为了能够使用www.yourdomain.com前缀访问您的域名:

如果您一切都做对了,您应该能够访问您的域名并看到您的 React 应用程序正在运行;这个过程可能需要 30 分钟到 24 小时,具体取决于 DNS 传播速度。

实施 Jenkins(持续集成)

Jenkins 是最受欢迎的持续集成软件之一,它基于 Java 并且是开源的。

准备就绪

运行 Jenkins 有一些先决条件:

  • 您需要一个带有 Ubuntu 18 的 Droplet(服务器)。

  • 您需要安装 Java 8。

如果您尚未安装 Java 8,可以使用以下命令进行安装:

sudo apt  install openjdk-8-jre-headless

如果要检查已安装的 Java 版本,可以使用java -version命令:

如何做到这一点...

现在让我们安装和配置 Jenkins:

  1. 将存储库密钥添加到系统中:
 **wget -q -O - https://pkg.jenkins.io/debian/jenkins.io.key | sudo apt-key add -**
  1. 将 Debian 软件包地址追加到sources.list
 **sudo sh -c 'echo deb http://pkg.jenkins.io/debian-stable** binary/ **> /etc/apt/sources.list.d/jenkins.list'**
  1. 更新 apt 软件包:
 **sudo apt update**
  1. 安装 Jenkins:
 **sudo apt install jenkins**

如果在安装 Jenkins 时出现错误,可以使用以下命令卸载它:

sudo apt-get remove --purge jenkins

  1. 启动 Jenkins 服务:
 sudo systemctl start jenkins
  1. 如果您想查看 Jenkins 状态,请使用此命令:
 **sudo systemctl status jenkins**

  1. Jenkins 默认运行在端口 8080 上,我们需要打开防火墙以允许流量通过该端口:
 **sudo ufw allow 8080**
  1. 如果你想验证防火墙状态,执行以下操作:
    **sudo ufw status**

如果你看到状态:inactive,你需要运行以下命令来启用防火墙:

sudo ufw allow OpenSSH

sudo ufw enable

  1. 是时候第一次运行并配置我们的 Jenkins 了。为此,你需要访问 http://<your_droplet 的 IP 或域名>:8080。在我的情况下,是 http://142.93.28.244:8080

  1. 要查看第一个密码,你需要运行:
**sudo cat /var/lib/jenkins/secrets/initialAdminPassword**

  1. 你会看到欢迎来到 Jenkins 页面。你需要选择“安装建议的插件”选项:

  1. 你会看到安装过程:

  1. 安装完成后,你需要创建第一个管理员用户:

  1. 确认 Jenkins URL 如果你不想更改它。点击保存并完成:

  1. Jenkins 已准备就绪:

  1. 你在 Jenkins 中看到的第一个视图是这个:

  1. 转到“管理 Jenkins” > “管理插件”来安装 GitHub 插件:

  1. 选择“可用”选项卡,然后搜索 GitHub 集成。现在选择复选框选项,然后点击“立即下载并在重启后安装”按钮:

  1. 选择“安装完成后重新启动 Jenkins,且没有任务正在运行”选项:

  1. 你会看到这个消息:

  1. 等待一分钟,然后刷新页面。你可能需要重新登录。

  2. 返回到“管理插件”;现在你需要安装“后构建任务插件”。

  3. 我们可以通过在主页上点击创建新任务来创建我们的第一个任务:

  1. 输入你的任务名称,选择自由风格项目选项,然后点击确定按钮:

  1. 在常规配置中,转到源代码管理部分,选择 Git 选项,然后写入你的 GitHub 项目 HTTPS URL(如果选择 SSH URL,你需要在 GitHub 中为 Jenkins 添加新的 SSH 密钥):

  1. 如果你的存储库是私有的,你需要点击“添加”按钮来指定你的 GitHub 凭据(用户名和密码):

  1. 选择你的凭据,并确保master分支被选为你的主分支(建议使用主分支而不是其他分支):

  1. 在后构建操作中选择“后构建任务”选项:

  1. 在文本框脚本中,添加npm install && npm run start-production。点击应用,然后点击保存按钮:

它是如何工作的...

我们已经配置好了 Jenkins 作业,现在让我们来测试一下。我将修改一个简单的文件,以确保 Jenkins 正常工作。

到这一步(如果你按照第一个教程操作),你必须使用命令“npm run stop”停止 PM2 服务器,然后删除之前克隆的生产目录,以避免与 Jenkins 作业出现问题。

让我们修改我们的主页组件;我会添加额外的文本(Jenkins)

  import React from 'react';
  import styles from './Home.scss';

  const Home = props => (
    <h1 className={styles.Home}>Hello {props.name || 'World'} (Jenkins)</h1>
  );

  export default Home;

文件:src/client/home/index.jsx

之后,你需要提交并推送到主分支。现在转到 Jenkins,选择你的作业,然后点击“立即构建”:

之后,点击最新的构建(在我的情况下是#5,因为我之前做了一些测试,但对你来说,它将是#1):

在构建中,你会看到是谁(用户)启动了构建,正在构建的是哪个修订版本(主分支的最新提交)。如果你想查看控制台输出,你可以点击左侧菜单中的选项:

如果你查看控制台输出,你会看到大量的命令:

每次运行新构建时,Jenkins 都会获取存储库的最新更改:

 git config remote.origin.url https://github.com/csantany/production.git

然后将获取主分支的最新提交:

 git rev-parse refs/remotes/origin/master^{commit}

最后,它将执行我们在后构建任务中指定的命令:

 npm install && npm run start-production

如果一切正常,你应该在输出的末尾看到“完成:成功”:

现在等待 30 秒或 1 分钟,然后访问你的生产网站(在我的案例中是http://142.93.28.244/)- 你会看到新的改变:

如果你想知道文件存储在哪里,你可以在/var/lib/jenkins/workspace/<your_jenkins_job_name>看到它们。

第十四章:使用 React Native

在本章中,将涵盖以下内容:

  • 创建我们的第一个 React Native 应用程序

  • 用 React Native 创建一个待办事项列表

  • 实现 React Navigation V2

介绍

React Native 是使用 JavaScript 和 React 构建移动应用程序的框架。许多人认为使用 React Native 可以制作一些"移动 Web 应用程序"或"混合应用程序"(如 Ionic、PhoneGap 或 Sencha),但实际上你构建的是原生应用程序,因为 React Native 将你的 React 代码转换为 Android 的 Java 代码或 iOS 应用程序的 Objective-C 代码。React Native 使用了大部分 React 的概念,如组件、属性、状态和生命周期方法。

React Native 的优势

  • 你只需编写一次代码,就可以得到两个原生应用程序(Android 和 iOS)

  • 你不需要有 Java、Objective-C 或 Swift 的经验

  • 更快的开发

  • MIT 许可证(开源)

Windows 的要求

  • Android Studio

  • Android SDK(>= 7.0 Nougat)

  • Android AVD

Mac 的要求

  • XCode(>= 9)

  • 模拟器

创建我们的第一个 React Native 应用程序

在这个教程中,我们将构建一个 React Native 应用程序,并了解 React 和 React Native 之间的主要区别。

准备工作

要创建我们的新的 React Native 应用程序,我们需要安装react-native-cli包:

 npm install -g react-native-cli

如何做...

现在,要创建我们的第一个应用程序:

  1. 让我们用这个命令来做:
    react-native init MyFirstReactNativeApp
  1. 在我们构建了 React Native 应用程序之后,我们需要安装 Watchman,这是 React Native 所需的文件监视服务。要安装它,去facebook.github.io/watchman/docs/install.html下载最新版本适合你的操作系统(Windows、Mac 或 Linux)。

  2. 在这种情况下,我们将使用 Homebrew 在 Mac 上安装它。如果你没有 Homebrew,你可以用这个命令安装它:

    /usr/bin/ruby -e "$(curl -fsSL 
  https://raw.githubusercontent.com/Homebrew/install/master/install)"
  1. 要安装 Watchman,你需要运行:
    brew update 
    brew install watchman
  1. 要启动 React Native 项目,我们需要使用:
    react-native start
  1. 如果一切正常,你应该看到这个:

有时你可能会从 Watchman 得到错误,例如,

Watchman 错误:太多待处理的缓存作业。确保 watchman 正在为此项目运行。

如果你遇到了这个错误或其他错误,你需要通过以下方式卸载 Watchman:

brew unlink watchman

然后重新安装:

brew update && brew upgrade

brew install watchman

  1. 打开一个新的终端(Cmd + T)并运行这个命令(取决于你想要使用的设备):
    react-native run-ios 
    or
    react-native run-android
  1. 如果没有错误,您应该看到模拟器运行默认应用程序:

现在我们的应用程序正在运行,让我们打开我们的代码并稍微修改一下:

  1. 更改App.js文件:
  ...
  export default class App extends Component<Props> {
    render() {
      return (
        <View style={styles.container}>
          <Text style={styles.welcome}>
 This is my first React Native App!          </Text>
          <Text style={styles.instructions}>
            To get started, edit App.js
          </Text>
          <Text style={styles.instructions}>{instructions}</Text>
        </View>
      );
    }
  }
  ...

文件:App.js

  1. 如果您再次进入模拟器,您需要按下Cmd + R重新加载应用程序以查看新更改的反映:

  1. 你可能想知道是否有一种自动重新加载的方法,而不是手动进行这个过程,当然,有一种方法可以启用实时重新加载选项;您需要按下Cmd + D打开开发菜单,然后选择启用实时重新加载选项:

  1. 另一个令人兴奋的选项是远程调试 JS。如果您点击它,它将自动打开一个 Chrome 标签,我们可以在那里看到我们使用console.log添加到我们的应用程序的日志。例如,如果我在我的渲染方法中添加console.log('====调试我的第一个 React Native 应用!====');,我应该看到它像这样:

  1. 让我们回到代码。也许您对在App.js中看到的代码有点困惑,因为您没有看到<div>标签,甚至更糟糕的是样式的创建方式像是一个对象,而不是像我们在 React 中使用 CSS 文件。我有一些好消息和一些坏消息;坏消息是 React Native 不支持 CSS 和 JSX/HTML 代码,就像 React 一样。好消息是,一旦您理解了<View>组件相当于使用<div><Text>相当于使用<p>,样式就像 CSS 模块(对象),其他一切都与 React 相同(props,state,生命周期方法)。

  2. 创建一个新的组件(Home)。为此,我们必须创建一个名为 components 的目录,然后将此文件保存为Home.js

  // Dependencies
  import React, { Component } from 'react';
  import { StyleSheet, Text, View } from 'react-native';

  class Home extends Component {
    render() {
      return (
        <View style={styles.container}>
          <Text style={styles.home}>Home Component</Text>
        </View>
      );
    }
  }

  const styles = StyleSheet.create({
    container: {
      flex: 1,
      justifyContent: 'center',
      alignItems: 'center',
      backgroundColor: '#F5FCFF',
    },
    home: {
      fontSize: 20,
      textAlign: 'center',
      margin: 10,
    }
  });

 export default Home;

文件:components/Home.js

  1. App.js中,我们导入Home组件,并将其渲染出来:
  // Dependencies
  import React, { Component } from 'react';
  import { StyleSheet, Text, View } from 'react-native';

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

  class App extends Component {
    render() {
      return (
        <Home />
      );
    }
  }

  export default App;

文件:App.js

它是如何工作的...

正如您所看到的,创建一个新的 React Native 应用程序非常容易,但是 React(使用 JSX)和 React Native 之间存在一些关键差异,使用特殊的标记和对象样式,甚至样式也有一些限制,例如,让我们创建一个 flex 布局:

    // Dependencies
    import React, { Component } from 'react';
    import { StyleSheet, Text, View } from 'react-native';

    class Home extends Component {
      render() {
        return (
          <View style={styles.container}>
            <View style={styles.header}>
              <Text style={styles.headerText}>Header</Text>
            </View>

            <View style={styles.columns}>
              <View style={styles.column1}>
                <Text style={styles.column1Text}>Column 1</Text>
              </View>

              <View style={styles.column2}>
                <Text style={styles.column2Text}>Column 2</Text>
              </View>

              <View style={styles.column3}>
                <Text style={styles.column3Text}>Column 3</Text>
              </View>
            </View>
          </View>
        );
      }
    }

    const styles = StyleSheet.create({
      container: {
        flex: 1,
        height: 100
      },
      header: {
        flex: 1,
        backgroundColor: 'green',
        justifyContent: 'center',
        alignItems: 'center'
      },
      headerText: {
        color: 'white'
      },
      columns: {
        flex: 1
      },
      column1: {
        flex: 1,
        alignItems: 'center',
        justifyContent: 'center',
        backgroundColor: 'red'
      },
      column1Text: {
        color: 'white'
      },
      column2: {
        flex: 1,
        alignItems: 'center',
        justifyContent: 'center',
        backgroundColor: 'blue'
      },
      column2Text: {
        color: 'white'
      },
      column3: {
        flex: 1,
        alignItems: 'center',
        justifyContent: 'center',
        backgroundColor: 'orange'
      },
      column3Text: {
        color: 'white'
      },
    });

    export default Home;

文件:components/Home.js

您可能不喜欢看一个庞大的文件(我也不喜欢),所以让我们将我们的组件和样式分开:

  import { StyleSheet } from 'react-native';

  export default StyleSheet.create({
    container: {
      flex: 1,
      height: 100
    },
    header: {
      flex: 1,
      backgroundColor: 'green',
      justifyContent: 'center',
      alignItems: 'center'
    },
    headerText: {
      color: 'white'
    },
    columns: {
      flex: 1
    },
    column1: {
      flex: 1,
      alignItems: 'center',
     justifyContent: 'center',
      backgroundColor: 'red'
    },
    column1Text: {
      color: 'white'
    },
    column2: {
      flex: 1,
      alignItems: 'center',
      justifyContent: 'center',
      backgroundColor: 'blue'
    },
    column2Text: {
      color: 'white'
    },
    column3: {
      flex: 1,
      alignItems: 'center',
      justifyContent: 'center',
      backgroundColor: 'orange'
    },
    column3Text: {
      color: 'white'
    },
  });

文件:components/HomeStyles.js

然后在我们的Home组件中,我们可以导入样式并以与以前相同的方式使用它们:

  // Dependencies
  import React, { Component } from 'react';
  import { StyleSheet, Text, View } from 'react-native';

  // Styles
  import styles from './HomeStyles';
  ...

文件:components/Home.js

这是代码的结果:

但有一些不寻常的地方。

正如您所看到的,我为<Text>组件(headerText,column1Text 等)创建了样式,这是因为某些样式不允许在 View 组件中使用。例如,如果您尝试将color: 'white'属性添加到<View>组件中,您会发现该属性不起作用,标题将具有黑色文本:

使用 React Native 创建待办事项列表

在这个示例中,我们将学习如何在 React Native 中处理事件以及如何通过创建一个简单的待办事项列表来处理状态。

如何做...

对于这个示例,我创建了一个名为“MySecondReactNativeApp”的新 React 应用程序:

  1. 创建一个src文件夹并将App.js文件移动到其中。还要修改此文件以包含我们的待办事项列表:
  import React, { Component } from 'react';

  import Todo from './components/Todo';

  export default class App extends Component {
    render() {
      return (
        <Todo />
      );
    }
  }

文件:src/App.js

  1. 我们的Todo组件将是:
  import React, { Component } from 'react';
  import { 
    Text, 
    View, 
    TextInput, 
    TouchableOpacity, 
    ScrollView 
  } from 'react-native';

  import styles from './TodoStyles';

  class Todo extends Component {
    state = {
      task: '',
      list: []
    };

    onPressAddTask = () => {
      if (this.state.task) {
        const newTask = this.state.task;
        const lastTask = this.state.list[0] || { id: 0 };
        const newId = Number(lastTask.id + 1);

        this.setState({
          list: [{ id: newId, task: newTask }, ...this.state.list],
          task: ''
        });
      }
    }

    onPressDeleteTask = id => {
      this.setState({
        list: this.state.list.filter(task => task.id !== id)
      });
    }

    render() {
      const { list } = this.state;
      let zebraIndex = 1;

      return (
        <View style={styles.container}>
          <ScrollView
            contentContainerStyle={{
              flexGrow: 1,
            }}
          >
            <View style={styles.list}>
              <View style={styles.header}>
                <Text style={styles.headerText}>Todo List</Text>
              </View>

              <View style={styles.add}>
                <TextInput
                  style={styles.inputText}
                  placeholder="Add a new task"
                  onChangeText={(value) => this.setState({ task: 
 value })}
                  value={this.state.task}
                />

                <TouchableOpacity
                  style={styles.button}
                  onPress={this.onPressAddTask}
                >
                  <Text style={styles.submitText}>+ Add Task</Text>
                </TouchableOpacity>
              </View>

              {list.length === 0 && (
                <View style={styles.noTasks}>
                  <Text style={styles.noTasksText}>
                    There are no tasks yet, create a new one!
 </Text>
                </View>
              )}

              {list.map((item, i) => {
                zebraIndex = zebraIndex === 2 ? 1 : 2;

                return (
                  <View key={`task${i}`} style=
                   {styles[`task${zebraIndex}`]}>
                    <Text>{item.task}</Text>
                    <TouchableOpacity onPress={() => { 
                     this.onPressDeleteTask(item.id) }}>
                      <Text style={styles.delete}>
                        X
                      </Text>
                    </TouchableOpacity>
                  </View>
                );
              })}
            </View>
 </ScrollView>
 </View>
      );
    }
  }

 export default Todo;

文件:src/components/Todo.js

  1. 这是样式:
  import { StyleSheet } from 'react-native';

 export default StyleSheet.create({
    container: {
      flex: 1,
      backgroundColor: '#F5FCFF',
      height: 50
    },
    list: {
      flex: 1
    },
    header: {
      backgroundColor: '#333',
      alignItems: 'center',
      justifyContent: 'center',
      height: 60
    },
    headerText: {
      color: 'white'
    },
    inputText: {
      color: '#666',
      height: 40,
      borderColor: 'gray',
      borderWidth: 1
    },
    button: {
      paddingTop: 10,
      paddingBottom: 10,
      backgroundColor: '#1480D6'
    },
    submitText: {
      color:'#fff',
      textAlign:'center',
      paddingLeft : 10,
      paddingRight : 10
    },
    task1: {
      flexDirection: 'row',
      height: 50,
      backgroundColor: '#ccc',
      alignItems: 'center',
      justifyContent: 'space-between',
      paddingLeft: 5
    },
    task2: {
      flexDirection: 'row',
      height: 50,
      backgroundColor: '#eee',
      alignItems: 'center',
      justifyContent: 'space-between',
      paddingLeft: 5
    },
    delete: {
      margin: 10,
      fontSize: 15
    },
    noTasks: {
      flex: 1,
      alignItems: 'center',
      justifyContent: 'center'
    },
    noTasksText: {
      color: '#888'
    }
  });

文件:src/components/TodoStyles.js

它是如何工作的...

在我们的组件中做的第一件事是设置我们的状态。task状态是为了创建新项目的输入,list状态是为了保存所有任务项目:

 state = {
      task: '',
      list: []
    };

TextInput组件创建一个输入元素,与 React 中的输入的主要区别在于,它使用onChangeText而不是onChange方法,并且默认获取值,我们可以直接更新我们的状态:

 <TextInput
    style={styles.inputText}
    placeholder="Add a new task"
    onChangeText={(value) => this.setState({ task: value })}
    value={this.state.task}
  />

TouchableOpacity组件用于处理点击事件(在 React Native 中为onPress),可以用作按钮。也许您想知道为什么我没有直接使用Button组件;这是因为在 iOS 上无法向按钮添加背景颜色,它只能在 Android 上使用背景。使用TouchableOpacity(或TouchableHighlight),您可以个性化样式,并且它完全可以作为按钮使用:

  <TouchableOpacity
    style={styles.button}
    onPress={this.onPressAddTask}
  >
    <Text style={styles.submitText}>+ Add Task</Text>
  </TouchableOpacity>

在任务的渲染中,我为任务实现了斑马样式(混合颜色)。此外,我们正在处理onPressDeleteTask以通过单击 X 按钮删除每个项目:

    {list.map((item, i) => {
      zebraIndex = zebraIndex === 2 ? 1 : 2;

      return (
        <View key={`task${i}`} style={styles[`task${zebraIndex}`]}>
          <Text>{item.task}</Text>
          <TouchableOpacity onPress={() => { 
           this.onPressDeleteTask(item.id) }}>
            <Text style={styles.delete}>
              X
            </Text>
          </TouchableOpacity>
 </View>
      );
    })}

如果我们运行应用程序,我们将首先看到这个视图:

如果我们没有任何任务,我们将看到“目前没有任务,创建一个新任务!”的消息。

如您所见,顶部有一个输入框,其中有“添加新任务”的占位符。让我们添加一些任务:

最后,我们可以通过点击 X 来删除任务;我将删除支付房租任务:

如您所见,通过这个基本的待办事项列表,我们学会了如何在 React Native 中使用本地状态以及如何处理点击和更改事件。

还有更多...

如果您想要防止用户意外删除任务,可以添加一个警报,询问用户是否确定要删除所选任务。为此,我们需要从 react-native 导入 Alert 组件并修改我们的 onPressDeleteTask 方法:

  import { 
    Text, 
    View, 
    TextInput, 
    TouchableOpacity, 
    ScrollView, 
 Alert 
  } from 'react-native';

  ...

  onPressDeleteTask = id => {
    Alert.alert('Delete', 'Do you really want to delete this task?', [
      {
        text: 'Yes, delete it.',
        onPress: () => {
          this.setState({
            list: this.state.list.filter(task => task.id !== id)
          });
        }
      }, {
        text: 'No, keep it.'
      }
    ]);
  }

  ...

如果您运行应用程序并尝试删除任务,您现在将看到这个本机警报:

实现 React Navigation V2

在这个教程中,我们将学习如何在 React Native 应用程序中实现 React Navigation V2。我们将在部分之间创建一个简单的导航。

准备工作

我们需要安装react-navigation依赖项:

 npm install react-navigation

如何做到...

让我们实现 React Navigation v2:

  1. 从 react-navigation 中包括createDrawerNavigationDrawerItems以及我们想要作为部分渲染的组件(主页和配置):
  // Dependencies
  import React, { Component } from 'react';
  import { StyleSheet, View, ScrollView, Image } from 'react-
  native';

  // React Navigation
  import { createDrawerNavigator, DrawerItems } from 'react-
  navigation';

  // Components
  import Home from './sections/Home';
  import Configuration from './sections/Configuration';

文件:App.js

  1. 在 CustomDrawerComponent 中,我们将渲染 Codejobs 标志和菜单(您可以根据需要进行修改):
 // Custom Drawer Component
 // Here we are displaying the menu options 
  // and customizing our drawer
  const CustomDrawerComponent = props => (
    <View style={styles.area}>
      <View style={styles.drawer}>
        <Image
          source={require('./assets/codejobs.jpeg')}
          style={styles.logo}>
        </Image>
      </View>

 <ScrollView>
        <DrawerItems {...props} />
 </ScrollView>
 </View>
  );

文件:App.js

  1. 创建AppDrawerNavigator,指定我们要在菜单中显示的组件作为部分(主页和配置)。此外,我们需要传递contentComponent与之前创建的CustomDrawerComponent
 // The left Drawer navigation
 // The first object are the components that we want to display
 // in the Drawer Navigation.
  const AppDrawerNavigator = createDrawerNavigator({
    Home,
    Configuration
  },
  {
    contentComponent: CustomDrawerComponent
  });

文件:App.js

  1. 创建 App 类并渲染AppDrawerNavigator组件:
  class App extends Component {
    render() {
      return (
        <AppDrawerNavigator />
      );
    }
  }

  // Styles for left Drawer
  const styles = StyleSheet.create({
    area: {
      flex: 1
    },
    drawer: {
      height: 150,
      backgroundColor: 'white',
      alignItems: 'center',
      justifyContent:'center'
    },
    logo: {
      height: 120,
      width: 120,
      borderRadius: 60
    }
  });

  export default App;

文件:App.js

  1. 创建部分组件;第一个是主页组件:
  // Dependencies
  import React, { Component } from 'react';
  import { View, Text, Image, TouchableOpacity } from 'react-native';
  // Styles
  import styles from './SectionStyles';
  class Home extends Component {
    // Here we specify the icon we want to render
 // in the menu for this option
    static navigationOptions = {
      drawerIcon: () => (
        <Image
          style={styles.iconsItem}
          source={require('../assets/home.png')}
        />
      )
    }
    render() {
      return(
        <View style={styles.container}>
          {/* Hamburger menu */}
          <TouchableOpacity 
            onPress={() => this.props.navigation.openDrawer()} 
            style={styles.iconMenu}
          >
            <Image
              style={styles.menu}
              source={require('../assets/menu.png')}
            />
          </TouchableOpacity>

          {/* Here is the content of the component */}
          <Text style={styles.titleText}>I'm the home section</Text>
        </View>
      );
    }
  }
  export default Home;

文件:sections/Home.js

  1. 这是配置部分组件:
  // Dependencies
  import React, { Component } from 'react';
  import { View, Text, Image, TouchableOpacity } from 'react-native';

  // Styles
  import styles from './SectionStyles';

  class Configuration extends Component {
 // Here we specify the icon we want to render
 // in the menu for this option
    static navigationOptions = {
      drawerIcon: () => (
        <Image
          style={styles.iconsItem}
          source={require('../assets/config.png')}
        />
      )
    };

    render() {
      return(
        <View style={styles.container}>
          {/* Hamburger menu */}
          <TouchableOpacity 
            onPress={() => this.props.navigation.openDrawer()} 
            style={styles.iconMenu}
          >
            <Image
              style={styles.menu}
              source={require('../assets/menu.png')}
            />
          </TouchableOpacity>

          {/* Here is the content of the component */}
          <Text style={styles.titleText}>I'm the configuration 
          section</Text>
        </View>
      );
    }
  }

 export default Configuration;

文件:sections/Configuration.js

  1. 您可能已经注意到我们在两个组件上使用了相同的样式,这就是为什么我为样式创建了一个单独的文件:
  import { StyleSheet } from 'react-native';

 export default StyleSheet.create({
    container: {
      flex: 1,
      backgroundColor: '#fff',
      alignItems: 'center',
      justifyContent: 'center',
    },
    iconMenu: {
      position: 'absolute',
      left: 0,
      top: 5
    },
    titleText: {
      fontSize: 26,
      fontWeight: 'bold',
    },
    menu: {
      width: 80,
      height: 80,
    },
    iconsItem: {
      width: 25,
      height: 25
    }
  });

文件:sections/sectionStyles.js

  1. 您可以在存储库(Chapter14/Recipe3/ReactNavigation/assets)中找到我们正在使用的资产。

它是如何工作的...

如果您一切都做对了,您应该会看到这个:

正在呈现的第一个组件是Home组件。如果您点击汉堡菜单,您会看到抽屉中有两个部分(HomeConfiguration),它们各自的图标以及顶部的 Codejobs 标志:

最后,如果您点击配置,您也会看到该组件:

如果您再次看到抽屉,您会注意到当前打开的部分也在菜单中处于活动状态(在这种情况下是配置)。

第十五章:最常见的 React 面试问题

我想通过给你一些关于 React 和 JavaScript 在工作面试中最常见的问题来结束这本书:

  • React 问题:

  • React 是什么?它与其他 JS 库/框架有什么不同?

  • React 组件的生命周期中发生了什么?

  • 你能告诉我关于 JSX 的一些信息吗?

  • 真实 DOM 和虚拟 DOM 之间有什么区别?

  • React 有哪些限制?

  • 解释 React 中render()的目的

  • 在 React 中,状态是什么,如何使用它?

  • 状态和属性之间有什么区别?

  • 在 React 中,箭头函数是什么?如何使用它?

  • 类组件和函数组件之间有什么区别?

  • 无状态组件和纯组件之间有什么区别?

  • 详细解释 React 组件的生命周期方法。

  • 什么是高阶组件(HOC)?

  • Redux 是什么?

  • Flux 和 Redux 有什么不同?

  • 在 React 中,ref 用于什么?

  • 在 Redux 中,动作和减速器之间有什么区别?

  • 如何提高 React 应用程序的性能?

  • JavaScript 问题:

  • 回调和 Promise 之间有什么区别?

  • 变量提升是什么?

  • apply 和 call 之间有什么区别?

  • 什么是闭包,如何/为什么使用它?

  • 事件委托是如何工作的?

  • 冒泡和捕获之间有什么区别?

  • bind()是做什么的?

  • null、undefined 和未声明的变量之间有什么区别?

  • =====之间有什么区别?

  • 什么是“词法”作用域?

  • 什么是函数式编程?

  • 经典继承和原型继承之间有什么区别?

posted @ 2024-05-16 14:50  绝不原创的飞龙  阅读(18)  评论(0编辑  收藏  举报