React-示例-全-

React 示例(全)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

ReactJS 是一个开源的 JavaScript 库,旨在将响应式编程的方面引入 Web 应用程序和网站。它旨在解决开发单页应用程序时遇到的挑战。React 的核心原则是声明式代码、效率、灵活性和改进的开发者体验。

在工作过程中深入研究新技术,还有什么更好的学习方法呢?这本书将通过不同的项目引导你,每个项目都专注于你在掌握 React 过程中的特定功能。我们将涵盖从 JSX、插件、性能到 Redux 的所有内容。

让我们的旅程开始吧!

有什么比在工作过程中深入研究新技术更好的学习方法呢?

第一章, React 入门, 通过构建一个使用静态数据的简单应用程序来介绍 ReactJS 的基础知识。我们将研究 React 的顶级 API 及其基本构建块。

第二章, 深入 JSX, 深入探讨 JSX 及其与 React 的用法。我们还将探讨在处理 JSX 时需要考虑的一些陷阱。

第三章, 数据流和生命周期事件, 专注于 React 组件之间的数据流以及组件的完整生命周期。

第四章, 复合动态组件和表单, 展示了如何使用 React 构建复合动态组件,在构建表单向导应用程序时更加关注表单。

第五章, 混入和 DOM, 涵盖了混入、refs 以及 React 如何与 DOM 交互。

第六章, 服务器端的 React, 使用 React 在服务器端渲染 HTML,并通过基于 Open Library Books API 的搜索应用程序来了解服务器端渲染带来的好处。

第七章, React 插件, 继续使用搜索应用程序,并通过 React 提供的各种插件来增强它。我们将研究这些插件的用例。

第八章, React 应用的性能, 通过深入研究 React 如何渲染内容,讨论了 React 应用的性能的各个方面,并帮助我们使应用程序更快。

第九章, React Router 和数据模型, 帮助构建 Pinterest 风格的程序,并讨论使用 react-router 的路由。我们还将讨论如何使用 React,包括 Backbone 模型,与各种数据模型一起使用。

第十章, 动画, 专注于通过动画使我们的 Pinterest 应用程序更加互动,以及如何有效地使用 React 来使用它们。

第十一章,React 工具,回顾并讨论了我们在与 React 一起工作时将使用的各种工具。我们将研究 Babel、ESLint、React 开发者工具和 Webpack 等工具。

第十二章,Flux,解释了在使用 Flux 架构的同时如何构建社交媒体追踪应用。我们将讨论 Flux 架构的需求以及它带来的好处。

第十三章,Redux 和 React,介绍了如何使用 Redux——一个流行的状态管理库——来进一步增强社交媒体追踪应用,以便使用基于 Redux 的状态管理。

您需要为本书准备什么

您需要拥有一个现代的网页浏览器,例如 Chrome 或 Firefox,才能运行本书中的示例。您还需要安装 Node.js——nodejs.org/en/——并设置 npm 包管理器。额外的设置说明可以在github.com/bigbinary/reactjs-by-example找到。

本书面向谁

如果您是一位希望从头开始学习 ReactJS 的网页开发者,那么这本书是为您量身定制的。本书预期您对 JavaScript、HTML 和 CSS 有良好的理解。

惯例

在本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称如下所示:"注意,我们是如何使用this.props.headings来访问传递的头信息。"

代码块设置如下:

return <div>
         <h1>Recent Changes</h1>
           <table>
          ….
          </table>
         </div>
…

当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:

    TestUtils.Simulate.click(submitButton);
    expect(app.state.searching).toEqual(true);
    expect(app.state.searchCompleted).toEqual(false);
 let spinner = TestUtils.findRenderedComponentWithType(app, Spinner);
 expect(spinner).toBeTruthy();

任何命令行输入或输出都如下所示:

Listening at localhost:9000
Hash: 8ec0d12965567260413b
Version: webpack 1.9.11
Time: 1639ms

新术语重要词汇将以粗体显示。屏幕上显示的单词,例如在菜单或对话框中,在文本中如下所示:"你注意到自动运行 JS选项了吗?"

注意

警告或重要注意事项如下所示:

小贴士

小技巧和技巧看起来像这样。

读者反馈

我们始终欢迎读者的反馈。告诉我们您对这本书的看法——您喜欢或不喜欢什么。读者反馈对我们来说很重要,因为它帮助我们开发出您真正能从中受益的标题。

要向我们发送一般反馈,只需发送电子邮件至<feedback@packtpub.com>,并在邮件主题中提及本书的标题。

如果您在某个领域有专业知识,并且有兴趣撰写或为本书做出贡献,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在您已经是 Packt 书籍的骄傲拥有者,我们有多种方式可以帮助您从您的购买中获得最大收益。

下载示例代码

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

您可以通过以下步骤下载代码文件:

  1. 使用您的电子邮件地址和密码登录或注册我们的网站。

  2. 将鼠标指针悬停在顶部的支持标签上。

  3. 点击代码下载与错误清单

  4. 搜索框中输入书籍名称。

  5. 选择您想要下载代码文件的书籍。

  6. 从下拉菜单中选择您购买此书的来源。

  7. 点击代码下载

文件下载完成后,请确保使用最新版本的以下软件解压缩或提取文件夹:

  • WinRAR / 7-Zip for Windows

  • Zipeg / iZip / UnRarX for Mac

  • 7-Zip / PeaZip for Linux

错误清单

尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以避免其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何错误清单,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击错误提交表单链接,并输入您的错误详情。一旦您的错误清单得到验证,您的提交将被接受,错误清单将被上传到我们的网站或添加到该标题的错误清单部分。

要查看之前提交的错误清单,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在错误清单部分。

盗版

盗版互联网上的版权材料是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过<copyright@packtpub.com>与我们联系,并提供疑似盗版材料的链接。

我们感谢您在保护我们作者和我们为您提供有价值内容的能力方面的帮助。

问题

如果您在这本书的任何方面遇到问题,您可以通过<questions@packtpub.com>联系我们,我们将尽力解决问题。

第一章:React 入门

在过去几年中,Web 开发领域迎来了单页应用(SPA)的巨大发展。早期的开发相对简单——通过重新加载整个页面来执行显示或用户操作的改变。这种做法的问题在于,整个请求从客户端到达服务器并返回客户端需要巨大的往返时间。

然后出现了 AJAX,它向服务器发送请求,可以在不重新加载当前页面的情况下更新页面的一部分。沿着相同的方向,我们看到了 SPA 的出现。

将大量前端内容封装起来,仅一次交付给客户端浏览器,同时基于任何事件与服务器保持小通道的通信;这通常由网络服务器上的轻量级 API 来补充。

这种应用的增长得到了 JavaScript 库和框架如 Ext JS、KnockoutJS、BackboneJS、AngularJS、EmberJS 等的补充,最近还有 React 和 Polymer。

让我们来看看 React 如何融入这个生态系统,并在本章中对其进行介绍。

在本章中,我们将涵盖以下主题:

  • 什么是 React 以及为什么我们要使用 React?

  • 数据在组件中流动

  • 组件根据组件的状态显示视图

  • 组件定义了视图的显示,无论包含什么数据,从而减少了显示对状态的依赖和复杂性

  • 用户交互可能会通过处理程序改变组件的状态

  • 组件被重用和重新渲染

什么是 React?

ReactJS 试图从视图层解决问题。它可以很好地定义并用作任何MVC框架中的V。它对如何使用它没有意见。它创建了视图的抽象表示。它在组件中分解视图的各个部分。这些组件既包含处理视图显示的逻辑,也包含视图本身。它可以包含用于渲染应用状态的所需数据。

为了避免复杂交互和随后的渲染处理,React 会对应用进行完整的渲染。它保持简单的工作流程。

React 基于这样一个理念:DOM 操作是一个昂贵的操作,应该尽量减少。它还认识到,手动优化 DOM 操作将导致大量样板代码,这些代码容易出错、无聊且重复。

React 通过为开发者提供一个虚拟 DOM 来渲染,而不是实际的 DOM,来解决这一问题。它找出真实 DOM 和虚拟 DOM 之间的差异,并执行所需的最小 DOM 操作以实现新状态。

React 也是声明式的。当数据发生变化时,React 概念上点击刷新按钮,并知道只更新更改的部分。

这种简单的数据流,加上极其简单的显示逻辑,使得使用 ReactJS 进行开发变得直接且易于理解。

谁使用 React?如果你使用过 Facebook、Instagram、Netflix、阿里巴巴、Yahoo、E-Bay、可汗学院、AirBnB、索尼和 Atlassian 等任何服务,你已经在 Web 上遇到过并使用过 React。

在不到一年的时间里,React 已经在主要互联网公司的核心产品中得到了采用。

在其首次会议上,React 还宣布了 React Native 的开发。React Native 允许使用 React 开发移动应用程序。它将 React 代码转换为原生应用程序代码,例如 iOS 应用程序的 Objective-C。

在撰写本文时,Facebook 已经在其群组和广告管理器应用中使用了 React Native。

在这本书中,我们将跟随两位开发者迈克和肖恩之间的对话。迈克是 Adequate 咨询的高级开发者,肖恩刚刚加入公司。迈克将指导肖恩,并与他进行结对编程。

当肖恩遇到迈克和 ReactJS

在 Adequate 咨询,这是一个晴朗的日子。这也是肖恩在公司第一天。肖恩加入 Adequate 是为了工作于其惊人的产品,并且因为它使用和开发令人兴奋的新技术。

在加入公司后,CTO 雪莉向肖恩介绍了迈克。迈克是 Adequate 的高级开发者,他是个乐天的人,喜欢探索新事物。

“肖恩,这是迈克,”雪莉说,“他将会指导你,也会与你一起进行开发。我们遵循结对编程,所以和他一起你会有很多这样的机会。他是个极好的帮手。”

随着这句话,雪莉离开了。

“嗨,肖恩!”迈克开始说,“你准备好开始了吗?”

“是的,都准备好了!那么我们在做什么?”

“嗯,我们即将开始开发一个使用openlibrary.org/的应用。Open Library 是世界经典文学的集合。它是一个面向所有书籍的开放、可编辑的图书馆目录。它是archive.org/下的一个倡议,列出了免费书籍的标题。我们需要构建一个应用来显示 Open Library 记录的最新更改。你可以称之为活动页面。许多人向 Open Library 做出了贡献。我们希望显示这些用户对书籍所做的更改、新增书籍、编辑等,如下面的截图所示:

当肖恩遇到迈克和 ReactJS

“哦,太好了!我们用它来构建什么?”

“Open Library 为我们提供了一个整洁的 REST API,我们可以消费它来获取数据。我们只是将要构建一个简单的页面来显示获取的数据,并对其进行格式化以供显示。我一直在尝试并使用 ReactJS 来做这件事。你之前用过它吗?”

“没有。不过,我听说过它。难道不是来自 Facebook 和 Instagram 的那个吗?”

“没错。这是一个定义我们 UI 的绝佳方式。由于应用在服务器上不会有太多逻辑或执行任何显示,所以使用它是一个简单选择。”

“既然你之前没有使用过,让我给你做一个快速介绍。”

"你之前尝试过 JSBin 和 JSFiddle 这样的服务吗?"

"不,但我见过它们。"

"酷。我们将使用其中之一,因此我们不需要在我们的机器上设置任何东西来开始。"

"让我们在你的机器上试一试",迈克指示道。“打开jsbin.com/?html,output

"你应该会看到类似标签和代码的选项卡以及它们在相邻面板中的输出。"

当肖恩遇见迈克和 ReactJS

"请确保HTMLJavaScript输出标签被点击,这样我们就可以看到它们的三个框架,以便我们能够编辑 HTML 和 JS 并看到相应的输出。"

"很好。"

"是的,这个的好处是您不需要进行任何设置。你注意到自动运行 JS选项了吗?请确保它被选中。此选项会导致 JSBin 重新加载我们的代码并查看其输出,这样我们就不需要不断说用 JS 运行来执行并查看其输出了。"

"好的。"

需要 React 库

"那么,让我们开始吧。请将页面的标题改为React JS Example。接下来,我们需要设置,并在我们的文件中需要 React 库。"

"React 的主页位于facebook.github.io/react/。在这里,我们也会找到可供我们使用的下载,以便我们可以将它们包含在我们的项目中。有不同方式来包含和使用这个库。"

我们可以使用 bower 或通过 npm 安装。我们也可以直接从 fb.me 域下载它,作为一个单独的下载。这里有开发版本,它是库的完整版本,以及生产版本,它是其最小化版本。还有一个附加版本。我们稍后会看看这个。"

"让我们从使用开发版本开始,这是 React 源代码的非最小化版本。将以下内容添加到文件标题:"

<script src="img/react-0.13.0.js"></script>

"完成了。"

"太棒了,让我们看看这看起来怎么样。"

<!DOCTYPE html>
<html>
<head>
  <script src="img/react-0.13.0.js"></script>
  <meta charset="utf-8">
  <title>React JS Example</title>
</head>
<body>

</body>
</html>

构建我们的第一个组件

"肖恩,我们已经准备好开始了。让我们构建我们的第一个 React 应用。请将以下代码添加到 JSBin 的 JavaScript 部分:"

var App = React.createClass({
  render: function(){
    return(React.createElement("div", null, "Welcome to Adequate, Mike!"));
  }
});

React.render(React.createElement(App), document.body);

"这就是它。你应该会看到页面的输出部分显示类似以下的内容:"

   Welcome to Adequate, Mike!

"迈克,我注意到我们正在使用这个 React 对象来创建类?"

"没错。我们正在创建,在 React 中被称为组件的东西。"

"ReactJS 库的入口点是 React 对象。一旦包含了react.js库,它就会在全局 JavaScript 命名空间中对我们可用。"

"React.createClass创建一个具有给定规范的组件。组件必须实现返回单个子元素的render方法,如下所示:"

var App = React.createClass({
  render: function(){
    return(React.createElement("div", null, "Welcome to Adequate, Mike!"));
  }
});

React 将负责调用组件的render方法来生成 HTML。

注意

即使渲染方法需要返回一个子元素,这个子元素也可以有一个任意深度的结构来包含完整的 HTML 页面部分。

"在这里,我们使用React.createElement来创建我们的内容。这是一个单例方法,允许我们创建一个包含"Welcome to Adequate, Mike!"内容的div元素。React.createElement创建一个ReactElement,这是 React 使用的 DOM 元素的内部表示。我们将 null 作为第二个参数传递。这是用来传递和指定元素属性的。目前,我们将其留空以创建一个简单的div。"

"ReactElement的类型可以是有效的 HTML 标签名,如spandivh1等,或者是由React.createClass本身创建的组件。"

"一旦我们完成了组件的创建,就可以使用React.render方法如下显示它:"

React.render(React.createElement(App), document.body);

"在这里,我们为之前创建的App组件创建了一个新的ReactElement,然后将其渲染到 HTML 元素——document.body中。这被称为mountNode,或我们组件的挂载点,它充当根节点。我们不是直接将document.body作为组件的容器传递,任何其他 DOM 元素也可以传递。"

"迈克,请将传递给div的文本更改为Hello React World!。我们应该开始看到变化,并且它应该看起来类似于以下内容:"

Hello React World!

"很好。"

"迈克,在构建第一个组件的同时,我们还对 React 的顶级 API 有一个概述,即使用React.createClassReact.createElementReact.render。"

"现在,我们刚刚构建的用于显示此问候消息的组件非常简单直接。然而,当构建复杂事物时,语法可能会变得具有挑战性,并且随着构建复杂事物的增长而增长。这就是 JSX 派上用场的地方。"

"JSX 吗?"

"JSX 是 ECMAScript 的 XML-like 语法扩展,没有定义任何语义。它具有简洁且熟悉的语法,与纯 HTML 相似,对于设计师或非程序员来说都很熟悉。它也可以直接从我们的 JavaScript 文件中使用!"

"什么?这不是很糟糕吗?"

"嗯,是时候重新思考最佳实践了。没错,我们将把视图及其 HTML 放入 JavaScript 文件中!"

"让我们看看如何开始使用它。请继续更改我们的 JavaScript 文件内容如下:"

var App = React.createClass({
  render: function(){
    return <div>
     Hello, from Shawn!
    </div>;
  }
});

React.render(React.createElement(App), document.body);

"正如你所见,我们在这里所做的是,我们没有使用createElement,而是直接编写了div标签。这非常类似于直接编写 HTML 标记。它也可以直接从 JavaScript 文件中工作。"

"迈克,代码在 JSBin 上抛出了一些错误。"

"哦,对了。我们需要使用 JSX 转换器库,以便 React 和浏览器可以理解语法。在我们的情况下,我们需要将我们使用的 JavaScript 类型更改为用于解释此代码的类型。我们需要做的是从 JavaScript 帧标题的下拉菜单中将类型从JavaScript更改为JSX (React),如下所示:"

构建我们的第一个组件

"这就完成了。"

"看起来不错,迈克。它正在工作。"

"现在你将看到以下类似的内容:"

Hello, from Shawn!

"回到工作状态"

"肖恩,这是个好开始。现在让我们回到使用 Open Library 的 Recent changes API 构建我们的应用的任务。我们已经准备好了一个基本的原型,而不使用 ReactJS。"

"我们将使用 ReactJS 逐步替换它的部分。"

"这是当前使用服务器端逻辑显示信息的方式,如下所示:"

回到工作

"我们面临的第一项任务是使用 ReactJS 在表格中显示从 Open Library Recent Changes API 获取的信息,类似于现在使用服务器端显示的方式。"

"我们将从 Open Library API 获取数据,类似于以下内容:"

var data = [{ "when": "2 minutes ago",
              "who": "Jill Dupre",
              "description": "Created new account"
            },
            {
              "when": "1 hour ago",
              "who": "Lose White",
              "description": "Added fist chapter"
            },
            {
              "when": "2 hours ago",
              "who": "Jordan Whash",
              "description": "Created new account"
            }];

"让我们先用这个来原型化我们的应用。在那之前,让我们看看这个应用的简单 HTML 版本。在我们的 React.render 方法中,我们开始返回一个表格元素,如下所示:"

var App = React.createClass({

  render: function(){
 return <table>
 <thead>
   <th>When</th>
   <th>Who</th>
   <th>Description</th>
 </thead>  
   <tr>
     <td>2 minutes ago</td>
     <td>Jill Dupre</td>
     <td>Created new account</td>
   </tr>
   <tr>
     <td>1 hour ago</td>
     <td>Lose White</td>
     <td>Added fist chapter</td>
   </tr>  
   <tr>
     <td>2 hours ago</td>
     <td>Jordan Whash</td>
     <td>Created new account</td>
   </tr>  
 </table>
  }
});

"这应该开始显示我们的三行表格。现在,请从 React App 中添加一个标题到这个表格的顶部,如下所示:"

…
return <h1>Recent Changes</h1>
           <table>
          ….
          </table>
…

"是这样的吗?"肖恩问道。"哦,这没起作用。"

"这是因为 React 会扩展我们的渲染方法,使其始终返回一个单一的 HTML 元素。在这种情况下,在你添加了 h1 标题之后,我们的应用开始返回两个元素,这是错误的。你将会遇到很多类似的情况。为了避免这种情况,只需将元素包裹在一个 divspan 标签中。主要思想是我们只想从渲染方法中返回一个元素。"

"明白了。像这样吗?"

…
return <div>
         <h1>Recent Changes</h1>
           <table>
          ….
          </table>
         </div>
…

显示静态数据

"太棒了!看起来不错。现在,让我们将显示静态信息的表格改为从我们之前拥有的 JSON 数据中获取并显示这些信息。"

"我们将在 render 方法本身中定义这些数据,并看看我们将如何使用它来创建我们的表格。我们基本上将只是遍历数据并创建元素,即在我们的情况下,为事件的单个数据集创建表格行。类似于以下内容:"

…
  var data = [{ "when": "2 minutes ago",
              "who": "Jill Dupre",
              "description": "Created new account"
            },
            {
              "when": "1 hour ago",
              "who": "Lose White",
              "description": "Added fist chapter"
            },
            {
              "when": "2 hours ago",
              "who": "Jordan Whash",
              "description": "Created new account"
            }];

  var rows = data.map(function(row){
  return  <tr>
     <td>{row.when}</td>
     <td>{row.who}</td>
     <td>{row.description}</td>
   </tr>
  });
…

"注意我们在这里是如何使用 {} 的。{} 在 JSX 中用于在视图模板中嵌入动态信息。我们可以用它来在视图中嵌入 JavaScript 对象,例如,一个人的名字或这个表格的标题。正如你所看到的,我们在这里使用 map 函数遍历我们的数据集。然后,我们返回一个由行对象中可用的信息构建的表格行 - 事件创建的详细信息、创建者以及事件描述。"

"我们在这里使用 JSX 语法来构建表格的行。然而,它并不是作为渲染函数的最终返回值使用。"

"没错,肖恩。React 与 JSX 允许我们任意创建元素用于我们的视图,在我们的情况下,从我们的数据集中动态创建。rows 变量现在包含了我们之前在另一个地方使用的部分视图。我们也可以在此基础上构建视图的另一个组件。"

"这就是它的美妙之处。React 允许我们动态创建、使用和重用视图的部分。这有助于我们以系统化的方式,部分由部分地构建视图。"

"现在,在我们完成行的构建后,我们可以在最终的渲染调用中使用它们。"

"所以现在,返回语句将看起来类似于以下内容:"

…
 return <table>
 <thead>
   <th>When</th>
   <th>Who</th>
   <th>Description</th>
 </thead>  
{rows} 
 </table>
…

"下面是如何在构建了静态数据行之后,现在看起来完整的渲染方法:"

  render: function(){
  var data = [{ "when": "2 minutes ago",
              "who": "Jill Dupre",
              "description": "Created new account"
            },
            {
              "when": "1 hour ago",
              "who": "Lose White",
              "description": "Added fist chapter"
            },
            {
              "when": "2 hours ago",
              "who": "Jordan Whash",
              "description": "Created new account"
            }];

  var rows = data.map(function(row){
  return  <tr>
     <td>{row.when}</td>
     <td>{row.who}</td>
     <td>{row.description}</td>
   </tr>
  })
 return <table>
 <thead>
   <th>When</th>
   <th>Who</th>
   <th>Description</th>
 </thead>
{rows}
</table>}

显示静态数据

"这开始看起来像是我们要达到的地方。"

传递数据到组件

"我们在渲染方法中定义数据和所有其他内容吗?"

"我正要说到这一点。我们的组件不应该包含这些信息。信息应该作为参数传递给它。"

"React 允许我们将 JavaScript 对象传递给组件。这些对象会在我们调用 React.render 方法并创建 <App> 组件实例时传递。以下是如何向其传递对象的方法:"

React.render(<App title='Recent  Changes'/>, document.body);

"注意这里我们使用的是 <App/> 语法,而不是 createElement。正如我之前提到的,我们可以从我们的组件中创建元素,并使用 JSX 来表示,就像之前做的那样。"

React.render(React.createElement(App), document.body)

"前面的代码变成了以下内容:"

React.render(<App/>, document.body)

"看起来更整洁了",肖恩说。

"正如你所见,我们正在将表格的标题作为 title 参数传递,然后是标题的内容。React 将传递给组件的数据称为 propsprops 是组件的配置选项,在初始化组件时传递给组件。"

"这些 props 只是普通的 JavaScript 对象。它们通过 this.props 方法使我们能够访问。让我们尝试从 render 方法中访问它,如下所示:"

…
  render: function(){
   console.log(this.props.title);
  }
…

"这应该会将我们传递给组件的标题记录到控制台。"

"现在,让我们尝试将标题以及 JSON 数据从 render 方法中抽象出来,并开始将它们传递给组件,如下所示:"

       var data = [{ "when": "2 minutes ago",
                           "who": "Jill Dupre",
                           "description": "Created new account"
                         },
                         ….
                        }];  
var headings = ['When', 'Who', 'Description']
<App headings = {headings} data = {data} />

"看这里。我们已经从 render 方法中提取了数据,现在正在将其传递给我们的组件。"

"我们为我们的表格定义了动态标题,我们将在组件中使用它。"

"在这里,用于传递参数的大括号被用来指定将被评估并用作属性值的 JavaScript 表达式。"

"例如,前面的 JSX 代码将被 React 转换为 JavaScript,如下所示:"

React.createElement(App, { headings: headings, data: data });

"我们稍后会重新访问 props。然而,现在,让我们继续完成我们的组件。"

"现在,使用通过 props 传递的数据和标题,我们需要在应用程序的 render 方法中生成表格结构。"

"让我们首先生成标题,如下所示:"

var App = React.createClass({

  render: function(){
      var headings = this.props.headings.map(function(heading) {
          return(<th>
                 {heading}
                 </th>);
      });
  }
});

"注意,我们是如何使用 this.props.headings 来访问传递的标题信息的。现在让我们创建与之前类似的表格行:"

var App = React.createClass({

  render: function(){
      var headings = this.props.headings.map(function(heading) {
          return(<th>
                 {heading}
                 </th>);
      });

      var rows = this.props.data.map(function(change) {
          return(<tr>
                   <td> { change.when } </td>
                   <td> { change.who } </td>
                   <td> { change.description } </td>
                 </tr>);
      });
  }
});

"最后,让我们在我们的表格中将标题和行组合起来。"

var App = React.createClass({

  render: function(){
      var headings = this.props.headings.map(function(heading) {
          return(<th>
                 {heading}
                 </th>);
      });

      var rows = this.props.data.map(function(change) {
          return(<tr>
                   <td> {change.when} </td>
                   <td> {change.who} </td>
                   <td> {change.description} </td>
                 </tr>);
      });

      return(<table>
               {headings}
               {rows}
             </table>);
  }
});

React.render(<App headings = {headings} data = {data} />,
                      document.body);

"现在表格显示的是传递的动态标题和 JSON 数据。"

"标题可以改为 ["最后更改时间", "作者", "摘要"],我们视图中的表格将自动更新。"

"好吧,肖恩,给我们的表格加一个标题吧。确保从 props 中传递它。"

"好的,” 肖恩说。

"现在,渲染方法将变为以下形式:"

…
 return <div>
               <h1>
                 {this.props.title}
               </h1>
               <table>
                 <thead>
                   {headings}
                 </thead>  
                 {rows} 
                </table>
            </div>
…

"当调用 React.render 时,将变为以下形式:"

var title =  'Recent Changes';
React.render(<App headings={headings} data={data} title={title}/>, document.body);

"太棒了。你开始上手了。让我们看看完成后的效果怎么样,好吗?"

var App = React.createClass({
  render: function(){
      var headings = this.props.headings.map(function(heading) {
          return(<th>
                 {heading}
                 </th>);
      });

  var rows = this.props.data.map(function(row){
  return  <tr>
     <td>{row.when}</td>
     <td>{row.who}</td>
     <td>{row.description}</td>
   </tr>

  })
 return <div><h1>{this.props.title}</h1><table>
 <thead>
{headings}
 </thead>  
{rows} 
 </table></div>
  }
});
  var data = [{ "when": "2 minutes ago",
              "who": "Jill Dupre",
              "description": "Created new account"
            },
            {
              "when": "1 hour ago",
              "who": "Lose White",
              "description": "Added fist chapter"
            },
            {
              "when": "2 hours ago",
              "who": "Jordan Whash",
              "description": "Created new account"
            }];

var headings = ["Last updated at", "By Author", "Summary"]
var title = "Recent Changes";
React.render(<App headings={headings} data={data} title={title}/>, document.body);

"我们再次开始看到以下内容:"

向组件传递数据

"这里就是了,肖恩。我们用 React 做的第一个组件!" 迈克说。

"这看起来太棒了。我迫不及待想尝试 React 中的更多功能!" 肖恩兴奋地喊道。

摘要

在本章中,我们从 React 开始,构建了我们的第一个组件。在这个过程中,我们学习了 React 的顶级 API 来构建组件和元素。我们使用 JSX 来构建组件。我们看到了如何使用 React 显示静态信息,然后逐渐用 props 将所有静态信息替换为动态信息。最后,我们能够将所有环节串联起来,使用 React 以 Open Library 的最近更改 API 返回的格式显示模拟数据。

在下一章中,我们将深入探讨 JSX 的内部机制,并继续为最近更改 API 构建我们的应用程序。

第二章 JSX 深入

在第一章中,我们使用 React 构建了我们的第一个组件。我们看到了使用 JSX 如何使开发变得简单。在本章中,我们将深入探讨 JSX。

JavaScript XML (JSX) 是一种 XML 语法,用于在 React 组件中构建标记。React 可以不使用 JSX 工作,但使用 JSX 可以使阅读和编写 React 组件以及对其结构化变得容易,就像任何其他 HTML 元素一样。

在本章中,我们将涵盖以下要点:

  • 为什么使用 JSX?

  • 将 JSX 转换为 JavaScript

  • 指定 HTML 标签和 React 组件

  • 多个组件

  • 不同类型的 JSX 标签

  • 在 JSX 中使用 JavaScript 表达式

  • 命名空间组件

  • 属性展开

  • CSS 样式和 JSX

  • JSX 的注意事项

在本章结束时,我们将熟悉 JSX 语法,如何与 React 一起使用它,以及使用它的最佳实践。我们还将研究一些在使用 JSX 时可能会遇到的一些边缘情况。

为什么使用 JSX?

肖恩第一天过得很愉快,他在 Adequate Consulting 正在开始新的一天。拿着一杯咖啡,他惊醒了迈克。

"嗨,迈克,我看到我们用 JSX 构建了我们的第一个组件。为什么我们要使用 JSX,当 React 有 React.createElement 时呢?"

"您可以在不使用 JSX 的情况下使用 React。但 JSX 使构建 React 组件变得容易。它减少了编写代码所需的数量。它看起来像 HTML 标记。其语法简单简洁,并且很容易可视化正在构建的组件。"

"以一个不使用 JSX 的组件的渲染函数为例。"

// render without JSX
render: function(){
    return(React.createElement("div", 
                               null, 
                               "Hello React World!"));
}

"有了 JSX,看起来好多了。"

// render with JSX
render: function(){
    return <div>
      Hello React World
    </div>;
  }

"与之前的非 JSX 示例相比,JSX 代码的阅读性更好,易于理解,并且接近实际的 HTML 标记。"

"JSX 和 HTML 标记之间的相似性意味着团队中的非开发者,如 UI 和 UX 设计师,可以使用 JSX 为项目做出贡献。拥有类似 XML 的标签语法也使得阅读大型组件树比 JavaScript 中的函数调用或对象字面量更容易。" 迈克解释道。

"是的,语法看起来很熟悉。我们将在我们的项目中全程使用 JSX,对吧?"

"是的,我们会",迈克说道。

将 JSX 转换为 JavaScript

"肖恩,正如我提到的,JSX 被转换成原生 JavaScript 语法。"

// Input (JSX):
var app = <App name="Mike" />;

"这最终会被转换成"

// Output (JS):
var app = React.createElement(App, {name:"Mike"});

小贴士

下载示例代码

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

您可以通过以下步骤下载代码文件:

  • 使用您的电子邮件地址和密码登录或注册我们的网站。

  • 将鼠标指针悬停在顶部的 SUPPORT 选项卡上。

  • 点击 代码下载与勘误

  • 搜索 框中输入书籍名称。

  • 选择您想要下载代码文件的书籍。

  • 从下拉菜单中选择您购买此书的来源。

  • 点击代码下载

文件下载后,请确保使用最新版本解压或提取文件夹:

  • WinRAR / 7-Zip for Windows

  • Zipeg / iZip / UnRarX for Mac

  • 7-Zip / PeaZip for Linux

"如果你想看到这个实时演示,请尝试这个示例babeljs.io/repl/。这是一个实时 REPL,可以将 JSX 代码转换为原生 JavaScript 代码。"

"还有一个编辑器可以将 HTML 转换为 JSX。你可以在facebook.github.io/react/html-jsx.html查看它。这允许你粘贴任意 HTML 代码,该代码会被转换为 JSX,并提取样式、类和其他信息,然后在此基础上创建一个组件。" 迈克说。

"非常方便。然而,这只是为了开发方便,对吧?当我们部署我们的代码时会发生什么?" 肖恩问道。

"JSX 不是用来在运行时编译的。尽管有一个 JSX 转换器可以在浏览器中将 JSX 转换为 JavaScript。在浏览器中用它来编译 JSX 会减慢我们的应用程序。我们将使用像 Babel 这样的工具,它是一个 JavaScript 编译器,在部署应用程序之前将我们的 JSX 代码转换为原生 JavaScript 代码。"

HTML 标签与 React 组件

"迈克,我对另一件事很感兴趣。在 JSX 中,我们就像简单的 HTML 标签一样混合 React 组件。我们在第一个组件中就是这样做的。"

ReactDOM.render(<App headings = {['When', 'Who', 'Description']} 
                     data = {data} />, 
             document.getElementById('container'));

"这里的App标签不是一个有效的 HTML 标签。但这仍然有效。"

"是的。这是因为我们可以在 JSX 中指定 HTML 标签和 React 组件。不过,这里有一个细微的区别。HTML 标签以小写字母开头,而 React 组件以大写字母开头。" 迈克解释道。

// Specifying HTML tags
render: function(){
    return(<table className = 'table'>
           .....
           </table>);
}

// Specifying React components
var App = React.createClass({..});
ReactDOM.render(<App headings = {['When', 'Who', 'Description']}  
                     data = {data} />, 
                document.getElementById('container'));

"这是主要区别。JSX 使用这个约定来区分本地组件类和 HTML 标签。"

自闭合标签

"迈克补充说,你肯定注意到了在ReactDOM.render中组件标签是如何关闭的。"

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

"由于 JSX 基于 XML,它允许添加自闭合标签。所有组件标签都必须以自闭合格式或闭合标签结束。"

"谢谢迈克!现在事情变得更有意义了。"

多个组件

"肖恩,让我们回到我们的应用程序。我们几乎使用了上次相同的代码,但你可以设置一个新的 JSBin。我们在 HTML 标签中包含了最新的 React 库和 bootstrap 库。我们还添加了一个容器元素,我们将在这里渲染我们的 React 应用程序。"

<!DOCTYPE html>
<html>
  <head>
    <script src="img/jquery.min.js"></script>
    <link href="https://netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/css/bootstrap-combined.min.css" rel="stylesheet" type="text/css" />
    <script src="img/bootstrap.min.js"></script>
    <script src="img/"></script>
    <script src="img/"></script>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <title>JSX in Detail</title>
  </head>
  <body>
    <div id="container">
    </div>
  </body>
</html>

"目前,我们只使用单个组件来显示最近更改 API 的数据。"

var App = React.createClass({
  render: function(){
    var headings = this.props.headings.map(function(heading) {
      return(<th>
        {heading}
      </th>);
    });

    var rows = this.props.data.map(function(row){
      return  <tr>
        <td>{row.when}</td>
        <td>{row.who}</td>
        <td>{row.description}</td>
      </tr>

    })
      return <div><h1>{this.props.title}</h1><table>
        <thead>
          {headings}
        </thead>
        {rows}
      </table></div>
  }
});

"让我们将这个单个组件拆分成小的可组合组件。这些简单的模块化组件将使用其他模块化组件,这些组件具有定义良好的自包含接口。"

"明白了" 肖恩说。

"好的。第一步是尝试识别我们单个组件中存在的不同组件。"

"目前,我们的渲染方法将tableHeadingstableRows作为表格元素的子元素列出。"

return(<table>
             {tableHeadings}
             {tableRows}
       </table>);

"我想我们将为标题和行创建组件?" 肖恩问道。

"是的。但我们可以更进一步。标题和行都是更小单元的列表,分别是HeadingRow标签。它可以这样可视化:"

<table>
  <Headings>
    <Heading/>
    <Heading/>
  </Headings>
  <Rows >
    <Row/>
    <Row/>
  </Rows>
</table>

"有道理,迈克。我现在尝试创建Heading。"

"当然,继续吧。"

var Heading = React.createClass({
  render: function() {
    return(<th>{heading}</th>);
  }
});

"迈克,我认为这会起作用,除了标题。我不确定如何在<th>标签中渲染实际的标题。"

"不用担心。我们只需假设它将被作为 props 传递给Heading组件。"

"当然。这是Heading组件的代码:"

var Heading = React.createClass({
  render: function() {
    return(<th>{this.props.heading}</th>);
  }
}); 

"太好了!Row组件也将类似于Heading。它将在其 props 中获取changeSet对象。"

var Row = React.createClass({
  render: function() {
    return(<tr>
             <td>{this.props.changeSet.when}</td>
             <td>{this.props.changeSet.who}</td>
             <td>{this.props.changeSet.description}</td>
          </tr>);
  }
});

"肖恩,我们已经完成了最低级别的组件。现在,是时候提升一个层次了。让我们先构建Headings。"

"类似于Heading组件将通过 props 获取其标题,Headings将获取传递给它的标题列表。"

var Headings = React.createClass({
  render: function() {
    var headings = this.props.headings.map(function(heading) {
      return(<Heading heading = {heading}/>);
    });

   return (<thead><tr>{headings}</tr><thead>);
  }
});

"我们正在遍历标题列表,并将它们转换为Heading组件的列表。Headings组件控制如何将 props 传递给单个Heading组件。从某种意义上说,单个Heading组件是Headings的拥有者。" 迈克解释道。

"在 React 中,拥有者是指设置其他组件 props 的组件。我们也可以说,如果 X 组件存在于 Y 组件的render()方法中,那么 Y 就拥有 X。" 迈克进一步补充道。

"肖恩,去构建一个类似于HeadingsRows组件。"

"给你:"

var Rows = React.createClass({
  render: function() {
    var rows = this.props.changeSets.map(function(changeSet) {
      return(<Row changeSet = {changeSet}/>);
    });
    return ({rows});
  }
});

"只有一个问题。你不能渲染行,因为它是一组组件的集合。记住,render()函数只能渲染一个标签。" 迈克说。

"我想我应该将行包裹在<tbody>标签中。" 肖恩说。

var Rows = React.createClass({
  render: function() {
    var rows = this.props.changeSets.map(function(changeSet) {
      return(<Row changeSet = {changeSet}/>);
    });

    return (<tobdy>{rows}</tbody>);
  }
});

"太好了。我们现在几乎有了所有东西。让我们通过添加顶级的App组件来完成它。"

var App = React.createClass({
  render: function(){
    return <table className = 'table'>
             <Headings headings = {this.props.headings} />
             <Rows changeSets = {this.props.changeSets} />
           </table>;
    }
});

"现在我们的完整代码看起来是这样的:"

var Heading = React.createClass({
  render: function() {
    return <th>{this.props.heading}</th>;
  }
});

var Headings = React.createClass({
  render: function() {
    var headings = this.props.headings.map(function(name) {
      return <Heading heading = {name}/>;
    });
   return <thead><tr>{headings}</tr></thead>;
  }
});

var Row = React.createClass({
  render: function() {
    return <tr>
             <td>{this.props.changeSet.when}</td>
             <td>{this.props.changeSet.who}</td>
             <td>{this.props.changeSet.description}</td>
           </tr>;
  }
});

var Rows = React.createClass({
  render: function() {
    var rows = this.props.changeSets.map(function(changeSet) {
      return(<Row changeSet = {changeSet}/>);
    });
    return <tbody>{rows}</tbody>;
  }
});

var App = React.createClass({
  render: function() {
    return <table className = 'table'>
             <Headings headings = {this.props.headings} />
             <Rows changeSets = {this.props.changeSets} />
           </table>;
    }
});

var data = [{ "when": "2 minutes ago",
              "who": "Jill Dupre",
              "description": "Created new account"
            },
            {      
              "when": "1 hour ago",
              "who": "Lose White",
              "description": "Added fist chapter"
           }];
var headings = ['When', 'Who', 'Description'];

ReactDOM.render(<App headings = {headings} 
                     changeSets = {data} />, 
                                      document.getElementById('container')); 

"肖恩,我想你现在已经理解了组件的可组合性力量。这使得我们的 UI 易于推理和重用以及组合。我们将在整个 React 中使用这种哲学。"

"我同意。每个组件都做了一件事,最终,它们都被组合在一起,从而构建了整个应用程序。代码的不同部分被分离,这样它们就不会相互干扰。"

JavaScript 表达式

"肖恩,让我们讨论一下我们是如何渲染RowsHeadings标签的。"

render: function() {
    var headings = this.props.headings.map(function(name) {
      return(<Heading heading = {name}/>);
    });

   return <tr>{headings}</tr>;
  }

"我们正在直接通过在<tr>标签的子元素中添加花括号来渲染{headings},这是一个 React 组件列表。用于指定子组件的表达式被称为子表达式。"

"还有一种称为 JavaScript 表达式的表达式类别。这些是用于传递 props 或评估一些可以用作属性值的 JavaScript 代码的简单表达式。"

// Passing props as expressions
ReactDOM.render(<App headings = {['When', 'Who', 'Description']} 
                  data = {data} />, 
                document.getElementById('container'));

// Evaluating expressions
ReactDOM.render(<App headings = {['When', 'Who', 'Description']} 
                  data = {data.length > 0 ? data : ''} />, 
                document.getElementById('container'));

"任何在花括号中的内容都会被 JSX 评估。它适用于子表达式以及 JavaScript 表达式。" 迈克补充道。

"感谢详细的解释。不过,我有一个疑问。在 JSX 代码中写注释有办法吗?我的意思是,我们可能并不总是需要它,但知道如何添加注释可能很有用。" 肖恩问道。

"记住花括号规则。注释只是简单的 JavaScript 表达式。当我们处于子元素中时,只需将注释放在花括号中。"

render: function() {
    return(<th> 
             {/* This is a comment */}
             {this.props.heading}
           </th>);
  }

"你还可以在 JSX 标签中添加注释。在这种情况下,没有必要将它们放在花括号中。" 迈克补充道。

ReactDOM.render(<App 
                  /* Multi
                     Line 
                     Comment
                  */
                  headings = {headings} 
                  changeSets = {data} />, 
                document.getElementById('container'));

命名空间组件

"肖恩,你一定在 Ruby 和 Java 等语言中使用过模块和包。这些概念背后的想法是创建一个代码的命名空间层次结构,使得一个模块或包中的代码不会干扰另一个模块或包。"

"是的。React 中有类似的东西吗?" 肖恩问道。

"是的。React 允许创建在父组件下命名的组件,这样它们就不会干扰其他组件或全局函数。"

"我们正在使用非常通用的名称,如 Rows 和 Headings,这些名称可以在应用程序的其他部分使用。因此,现在命名空间它们比以后命名空间它们更有意义。" 迈克解释说。

"同意。我们立刻这么做。" 肖恩说。

"我们需要将顶层组件表示为自定义组件,而不是使用 <table> 元素。"

var RecentChangesTable = React.createClass({
  render: function() {
    return <table>
             {this.props.children}
           </table>;
  } 
});

"现在,我们可以将 App 组件替换为使用 RecentChangesTable 而不是 <table>。"

var App = React.createClass({
  render: function(){
    return(<RecentChangesTable>
                <Headings headings = {this.props.headings} />
                <Rows changeSets = {this.props.changeSets} />
           </RecentChangesTable>);
    }
});

"等等,迈克。我们刚刚用自定义组件替换了 <table>。它所做的只是渲染 this.props.children。它是如何获取所有标题和行的?" 肖恩问道。

"啊!很好的观察。React 默认情况下,会捕获组件打开和关闭标签之间的所有子节点,并将它们作为一个数组添加到该组件的 props 中,作为 this.props.children。因此,我们可以使用 {this.props.children} 来渲染它。在 RecentChangesTable 组件中,我们将得到所有标题和行作为 this.props.children。输出与之前使用 <table> 标签时相同。"

"太棒了!" 肖恩兴奋地喊道。

"太好了。让我们继续下一步,将所有其他组件在 RecentChangesTable 下命名空间。"

RecentChangesTable.Headings = React.createClass({
  render: function() {
    var headings = this.props.headings.map(function(name) {
      return(<RecentChangesTable.Heading heading = {name}/>);
    });

   return (<thead><tr>{headings}</tr></thead>);
  }
});

RecentChangesTable.Heading = React.createClass({
  render: function() {
    return(<th>
             {this.props.heading}
           </th>);
  }
});

RecentChangesTable.Row = React.createClass({
  render: function() {
    return(<tr>
             <td>{this.props.changeSet.when}</td>
             <td>{this.props.changeSet.who}</td>
             <td>{this.props.changeSet.description}</td>
          </tr>);
  }
});

RecentChangesTable.Rows = React.createClass({
  render: function() {
    var rows = this.props.changeSets.map(function(changeSet) {
      return(<RecentChangesTable.Row changeSet = {changeSet}/>);
    });

    return (<tbody>{rows}</tbody>);
  }
});

"我们现在还需要更新 App 组件,以使用命名空间组件。"

var App = React.createClass({
  render: function(){
    return(<RecentChangesTable>
                 <RecentChangesTable.Headings headings = {this.props.headings} />
                 <RecentChangesTable.Rows changeSets = {this.props.changeSets} />
               </RecentChangesTable>);
    }
});

"我们现在完成了。现在,所有内容都在 RecentChangesTable 的命名空间下。" 迈克说。

属性展开

肖恩学到了很多关于 JSX 的知识,但在反思之前的步骤时,他又提出了另一个问题。

"迈克,到目前为止,我们只是向 App 组件传递了两个 props:headingschangesets。然而,明天这些 props 可以增加到任意数量。逐个传递它们可能会很繁琐。特别是,当我们需要直接从最近更改的 API 传递一些数据时。这将很难跟踪传入数据的结构,并相应地在 props 中传递。有没有更好的方法?"

"另一个出色的问题,肖恩。是的,逐个传递大量属性给组件可能会有些繁琐。但我们可以使用 spread 属性来解决这个问题。"

var props = { headings: headings, changeSets: data, timestamps: timestamps };
ReactDOM.render(<App {...props } />, 
                     document.getElementById('container'));

"在这种情况下,对象的所有属性都作为 props 传递给 App 组件。我们可以传递任何可以包含任意数量键值对的任何对象,并且所有这些都会作为 props 传递给组件" 迈克解释道。

"非常酷。(…) 运算符只存在于 JSX 中吗?"

"不,它实际上是基于 ES2015 中的扩展属性特性,这是下一个 JavaScript 标准。ES2015,或称为 ES6,在 JavaScript 语言中引入了一些新特性,React 正在利用这些发展中的标准,以便在 JSX 中提供更干净的语法" 迈克补充道。

注意

ES2015 已经支持数组使用扩展运算符,详情请见 developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator。对于对象也有一个提案,请见 github.com/sebmarkbage/ecmascript-rest-spread

"不仅如此,散列属性可以多次使用,或者可以与其他属性结合使用。不过,属性的顺序很重要。新属性会覆盖之前的属性。"

var data = [{ "when": "2 minutes ago",
              "who": "Jill Dupre",
              "description": "Created new account"
            },
            {
              "when": "1 hour ago",
              "who": "Lose White",
              "description": "Added fist chapter"
            }];
var headings = ['When', 'Who', 'Description'];

var props = { headings: headings, changeSets: data };

ReactDOM.render(<App {...props} headings = {['Updated at ', 'Author', 'Change']} />, document.getElementById('container'));

"在这种情况下,将显示 WhenWhoDescription,而 Updated atAuthorChange 将作为标题显示" 迈克解释说。

散列属性

注意

ES2015 或 ES6 是最新的 JavaScript 标准版本。它有很多特性,这些特性被 React 使用,类似于扩展运算符。在接下来的章节中,我们将使用更多的 ES2015 或 ES6 代码。

JSX 中的样式

"迈克,我们今天所做的一切都非常酷。我们什么时候开始添加样式?我怎样才能让这个页面看起来更漂亮?现在它有点单调。" 肖恩问道。

"啊,对了。我们就这样做。React 允许我们以与传递 props 相同的方式传递样式。例如,我们想让我们的标题颜色为花白色,也许我们还想改变字体大小。我们将以典型的 CSS 方式表示如下:"

background-color: 'FloralWhite',
font-size: '19px';

"我们可以用驼峰式表示法将其表示为一个 JavaScript 对象。"

    var headingStyle = { backgroundColor: 'FloralWhite',
                         fontSize: '19px' 
                       };

然后,我们可以在每个标题组件中将其用作一个 JavaScript 对象。"

RecentChangesTable.Heading = React.createClass({
  render: function() {
    var headingStyle = { backgroundColor: 'FloralWhite',
                         fontSize: '19px' };
    return(<th style={headingStyle}>{this.props.heading}</th>);
  }
});

"同样,让我们改变行以拥有它们自己的样式。"

  RecentChangesTable.Row = React.createClass({
  render: function() {
  var trStyle = { backgroundColor: 'aliceblue' };
    return <tr style={trStyle}>
                <td>{this.props.changeSet.when}</td>
                <td>{this.props.changeSet.who}</td>
                <td>{this.props.changeSet.description}</td>
            </tr>;
  }
});

"我们现在有一些闪闪发光的新标题和行,它们被 CSS 样式所点缀" 迈克补充道。

JSX 中的样式

"好的。传递这些样式的属性名必须是 'style',对吧?" 肖恩问道。

"是的。此外,这个样式对象的键也需要使用驼峰式命名,例如,backgroundColorbackgroundImagefontSize 等等。"

"迈克,我理解了内联样式,但是如何添加 CSS 类?"

"啊,对了。我们可以将类名作为属性传递给 DOM 标签。让我们将样式提取到一个新的 recentChangesTable CSS 类中。"

// css
recentChangesTable {
  background-color: 'FloralWhite',
  font-size: '19px'
}

"现在,为了将这个类应用到我们的组件上,我们只需要使用解释过的 className 属性将其传递给组件。"

render: function(){
    return <table className = 'recentChangesTable'>
             <Headings headings = {this.props.headings} />
             <Rows changeSets = {this.props.changeSets} />
           </table>;
    }
});

"正如我们之前所看到的,React 使用驼峰式属性。在这里,当 React 渲染实际的 HTML 时,className 属性将被转换为正常的类属性。"

<table class = 'recentChangesTable'> 
…
</table>

"我们也可以将多个类传递给 className 属性。"

<table className = 'recentChangesTable userHeadings'>

"就这样!我们可以在我们的组件中自由地使用样式。这使得将 React 与现有的 CSS 样式集成变得非常简单。"

JSX 的注意事项

"那天即将结束。迈克和肖恩还在讨论这个闪亮的新事物——JSX。迈克决定是时候告诉肖恩使用 JSX 的问题了。"

"肖恩,你对使用 JSX 感觉如何?"

"到目前为止,我很喜欢它。它与 HTML 标记非常相似。我可以传递属性、样式,甚至类。我还可以使用所有的 DOM 元素。" 肖恩解释说。

"是的。但是 JSX 不是 HTML。我们一定要记住这一点。否则,我们可能会遇到麻烦。"

"例如,如果你想传递一些在 HTML 规范中不存在的自定义属性,那么 React 将简单地忽略它。"

// custom-attribute won't be rendered
<table custom-attribute = 'super_awesome_table'>
</table>

"它必须作为数据属性传递,这样 React 才会渲染它。"

// data-custom-attribute will be rendered
<table data-custom-attribute = 'super_awesome_table'>
</table>

"在动态渲染 HTML 内容时,我们可能会遇到一些问题。在 JSX 标签中,我们可以直接添加一个有效的 HTML 实体。"

// Using HTML entity inside JSX tags.
<div> Mike &amp; Shawn </div>
// will produce
 React.createElement("div", null, " Mike & Shawn ")

"但是如果我们用动态表达式渲染它,它就会逃逸掉 ampersand。"

// Using HTML entity inside dynamic expression
var first = 'Mike';
var second = 'Shawn';
<div> { first + '&amp;' + second } </div>

var first = 'Mike';
var second = 'Shawn';
React.createElement("div", null, " ", first + '&amp;' + second, " ")

"React 默认会转义所有字符串,以防止 XSS 攻击。为了克服这个问题,我们可以直接传递 &amp; 的 Unicode 字符,或者我们可以使用字符串数组和 JSX 元素。" 迈克解释说。

// Using mixed arrays of JSX elements and normal variables
<div> {[first, <span>&amp;</span>, second]} </div>

React.createElement("div", null, " ", [first, 
                                   React.createElement("span", null, "&"), second], " ")

"哇。这可能会变得相当混乱" 肖恩表示。

"嗯,是的,但是如果我们记住规则,那么这很简单。而且,作为最后的手段,React 也允许使用特殊的 dangerouslySetInnerHTML 属性来渲染原始 HTML。"

// Rendering raw HTML directly
<div dangerouslySetInnerHTML={{__html: 'Mike &amp; Shawn'}} />

"迈克解释说,尽管这个选项应该在考虑了渲染内容之后使用,以防止 XSS 攻击。"

JSX 中的条件

"React 赞同将标记和生成标记的逻辑联系在一起的想法。这意味着我们可以使用 JavaScript 的循环和条件语句的强大功能。"

"但是,在标记中表达 if/else 逻辑有点困难。因此,在 JSX 中,我们不能使用 if/else 这样的条件语句。"

// Using if/else directly doesn't work
<div className={if(success) { 'green' } else { 'red' }}/>
Error: Parse Error: Line 1: Unexpected token if

"相反,我们可以使用三元运算符来指定 if/else 逻辑。"

// Using ternary operator
<div className={ success ? 'green' : 'red' }/>
React.createElement("div", {className:  success ? 'green' : 'red'})

"但是,当我们要使用 React 组件作为子组件时,大型表达式使用三元运算符会变得繁琐。在这种情况下,将逻辑卸载到块或函数中会更好。" 迈克补充说。

// Moving if/else logic to a function
var showResult = function() {
  if(this.props.success === true)
    return <SuccessComponent />
  else
    return <ErrorComponent />
};

非 DOM 属性

"好吧,肖恩,是时候再次详细查看我们的应用程序了。如果你仔细查看控制台输出,你会看到一些与键相关的警告。"

"Each child in an array should have a unique \"key\" prop. Check the render method of Rows. See http://fb.me/react-warning-keys for more information."

"在Rows组件的render()方法中,我们正在渲染Row组件的集合。"

RecentChangesTable.Rows = React.createClass({
  render: function() {
    var rows = this.props.changeSets.map(function(changeSet) {
      return(<Row changeSet = {changeSet}/>);
    });

    return <tbody>{rows}</tbody>;
  }
});

"在渲染列表项期间,根据用户交互,组件可能在 DOM 树中上下移动。例如,在搜索或排序的情况下,列表中的项可以改变其位置。如果获取到新数据,新项也可以添加到列表的前面。在这种情况下,React 可能会根据 diff 算法删除并重新创建组件。但如果我们为列表中的每个元素提供一个唯一的标识符,那么 React 将智能地决定是否销毁它。这将提高渲染性能。这可以通过将唯一的key属性传递给列表中的每个项来实现。"

在我们这个例子中,行数目前是固定的。但稍后,我们希望在从 API 获取新数据时显示更新页面。当动态添加或删除子组件时,情况会变得复杂,因为每个组件的状态和标识必须在每次渲染过程中保持不变。key属性将帮助 React 在这种情况下唯一地识别组件。迈克进一步解释说:“继续使用Row组件的索引来完成这个目的,因为现在它是唯一的。”

"很好。那么让我尝试给Rows组件添加key属性。我也注意到Headings组件也存在相同的问题,因此,我将为Headings也添加一个键。" 肖恩说。

RecentChangesTable.Rows = React.createClass({
  render: function() {
    var rows = this.props.changeSets.map(function(changeSet, index) {
      return(<Row key={index} changeSet = {changeSet}/>);
    });

    return (<div>{rows}</div>);
  }
});
RecentChangesTable.Headings = React.createClass({
  render: function() {
    var headings = this.props.headings.map(function(name, index) {
      return(<RecentChangesTable.Heading key={index} heading = {name}/>);
    });

    return (<thead><tr>{headings}</tr></thead>);
  }
});

"完美。请注意,给定列表的键值应该是唯一的。当我们开始根据动态数据更新 DOM 时,我们将了解更多关于键的信息。但这对现在来说已经足够了。" 迈克说。

"有道理。还有其他这样的关键字/标识符可供我们使用吗?"

"是的。除了键,还有引用或 refs。它允许父组件保持对子组件的引用。目前,我们无法在组件的render()方法之外访问子组件。但拥有ref允许我们在组件的任何地方使用子组件,而不仅仅是render()方法。"

<input ref="myInput" />

"现在,我们可以在render方法之外访问这些引用了。"

    this.refs.myInput

"当我们想要在运行时通知组件更改某些内容时,这非常有用。我们将在处理事件处理程序时详细讨论和使用refs。" 迈克补充说。

摘要

在本章中,我们深入探讨了 JSX。我们讨论了为什么使用 JSX 使得使用 React 进行开发变得容易,以及 JSX 如何被转换成纯原生 JavaScript。我们将大的单个 React 组件拆分成小而专注的组件,并理解了可重用、模块化组件的优势。我们看到了不同的 JSX 标签、JavaScript 表达式,以及 React 如何利用 ES6 特性,如扩展属性。最后,我们讨论了高级主题,例如命名空间组件和一些在使用 JSX 时应该注意的陷阱。

在下一章中,我们将重点关注数据流和模型,以访问数据和组件生命周期及其使用。

第三章 数据流和生命周期事件

在上一章中,我们看到了 JSX 的强大功能。JSX 使得编写 React 组件变得容易。

在本章中,我们将关注组件之间的数据流以及如何管理组件的状态和生命周期。

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

  • React 中的数据流

  • Props

  • PropTypes

  • 状态

  • 状态与 props

  • 何时使用状态和 props

  • 组件生命周期概述

  • 组件生命周期方法

在本章结束时,我们将熟悉 React 组件中的数据流以及维护和管理状态的方法。我们还将习惯于组件的生命周期以及 React 提供的各种生命周期钩子。

React 中的数据流

肖恩和马克正准备在雨天喝着咖啡开始工作。

“迈克,我对我们用来传递headingschangeSet数据的 props 有一个问题。”

“哎呀!”迈克惊呼。

“在我看来,我们似乎正在将数据传递给当前组件下方的组件,但一个组件如何将数据传递给父组件呢?”

“啊。在 React 中,默认情况下,所有数据都只在一个方向上流动:从父组件到子组件。就是这样。”

这使得子组件的工作变得简单且可预测。从父组件接收 props 并渲染。”迈克解释道。

var RecentChangesTables = React.createClass({
  render: function(){
    return(<table className = 'table'>
             <Headings headings = {this.props.headings} />
             <Rows changeSets = {this.props.changeSets} />
           </table>);
    }
});

“让我们看看我们的例子。RecentChangesTables组件将 props 传递给HeadingsRows组件。所以基本上,我们可以这样说,RecentChangesTables拥有HeadingsRows组件。”

“在 React 中,一个拥有者组件为另一个组件设置 props。”迈克解释道。

“明白了。因此,在前面的例子中,<table>也是RecentChangesTables的拥有者吗?”肖恩问道。

“不。拥有者关系是特定于 React 组件的。在这种情况下,表格是HeadingsRows的父组件,类似于 DOM 中的父子关系。但它不是它们的拥有者。”迈克解释道。

“如果一个子组件是在父组件的渲染方法中创建的,那么这个组件就是子组件的拥有者。我想这会解决混淆。”迈克补充道。

“是的。我明白了拥有者和父子关系之间的区别。”肖恩说。

“此外,一个组件不应该修改其 props。它们应该始终与父组件设置的保持一致。这是一个非常重要的点,它使得 React 的行为既一致又快速,正如我们很快就会看到的。”迈克进一步补充道。

“可以通过this.props访问 props,正如我们之前看到的。如果父组件的 props 中有任何变化,React 将确保这些变化会向下流动,并重新渲染组件树。”迈克说。

“太好了。昨天,我读了一些关于验证 props 的内容。”肖恩回忆道。

“是的。React 允许使用PropTypes验证 props。让我们看看它们。”迈克一边说着,一边喝了一口新鲜磨的咖啡。

Props 验证

"React 提供了一个使用 PropTypes 来验证 props 的方法。这非常有用,可以确保组件被正确使用。这里是一个使用propTypes为我们应用程序的例子。" 迈克解释说。

var App = React.createClass({
  propTypes: {
   headings: React.PropTypes.array,
   changeSets: React.PropTypes.array,
   author: React.PropTypes.string.isRequired
   },

  render: function(){
    return(<table className = 'table'>
             <Headings headings = {this.props.headings} />
             <Rows changeSets = {this.props.changeSets} />
           </table>);
    }
});

"哦!它会不会显示错误,因为我们没有传递作者,这是必需的,我猜?我看到propTypes已经将作者值设置为isRequired。" 肖恩问道。

Props validation

"不。它不会抛出错误,但它会显示一个漂亮的警告,让我们看看。" 迈克说。

"此外,propTypes只在开发时进行检查。它们的工作只是检查我们对我们组件所做的所有假设是否得到满足。" 迈克补充说。

"明白了。我同意有它比在生产过程中被随机的小故障所惊讶要好得多," 肖恩说。

"是的。它特别有用,因为我们不仅可以用标准类型,还可以验证自定义类型。" 迈克通知说。

var App = React.createClass({
  propTypes: {
   headings: function(props, propName, componentName) {
   if(propName === 'headings')
     return Error('Failed Validation');
   }
  },

  render: function(){
    return(<table className = 'table'>
             <Headings headings = {this.props.headings} />
             <Rows changeSets = {this.props.changeSets} />
           </table>);
    }
});

"因此,如果 props 的结构与你的假设不符,你可以通过定义一个自定义验证器来发出警告,就像上一个案例中展示的那样",迈克解释说。

指定默认 props

"肖恩,React 还允许我们为 props 定义一些默认值。这在父组件根据某些条件传递 props 或者由于某些变化而没有传递任何 props 时非常有用",迈克说。

var App = React.createClass({

 getDefaultProps: function() {
    return {
      headings: ['When happened ', 'Who did it', 'What they change']
    };
  },

  render: function(){
            …
  }
});

var data = [{ "when": "2 minutes ago",
              "who": "Jill Dupre",
              "description": "Created new account"
            },
            {
              "when": "1 hour ago",
              "who": "Lose White",
              "description": "Added first chapter"
            }];

React.render(<App changeSets={data}/>, document.body);

"在这里,我们更新了代码,不再从 props 中发送标题。相反,我们使用了getDefaultProps函数来定义在它们未传递时将使用的默认 props。"

Specifying default props

"因此,我们的输出看起来像这样。"

"哦,好的。这很有道理。与其通过 if-else 语句检查 props 是否存在,不如使用默认 props 来预先定义我们的数据简单得多。" 肖恩说。

修改 this.props.children

"肖恩。我们应该了解一个特殊的 props。它是this.props.children," 迈克继续说。

"React 会将所有存在于开始和结束标签之间的子元素捕获到 props 中,这些 props 可以通过this.props.children访问。" 迈克说。

"让我们尝试修改我们的代码以使用this.props.children。这也很有必要,因为我们想为我们的输出表格显示一个标题。" 迈克补充道。

var RecentChangesTable = React.createClass({
  render: function(){
          return(
          <div>
            <h1> Recent Changes </h1>
            <table className='table'>
               {this.props.children}
            </table>
          </div>
          );
  }
});

var App = React.createClass({
  render: function(){
    return(<RecentChangesTable>
             <Headings headings = {this.props.headings} />
             <Rows changeSets = {this.props.changeSets} />
           </RecentChangesTable>);
    }
});

Modifying this.props.children

"太好了。所以我们提取了表格到它自己的组件中,并添加了一个标题。" 肖恩确认道。

"是的,我们使用this.props.children来渲染标题。" 迈克解释道。

"太棒了。让我根据我们关于 props 的讨论来修改我们的代码。" 肖恩兴奋地说。

状态

"肖恩,让我们再讨论一种在组件中处理数据的技术,状态。在 React 中,每个组件都可以有自己的状态。状态和 props 之间的主要区别是,props 是从父组件传递给组件的;而状态是组件内部的东西。

组件实例化时传递 props。state 是随时间可以变化的东西。因此,state 的变化会影响组件的渲染。可以把 state 看作是组件的一种私有数据结构。"迈克补充道。

"迈克,但到目前为止我们根本没使用过 state。我们只是使用了 props。"肖恩问道。

"确实。这是因为只有在需要的时候才应该引入 state。你已经知道管理 state 是困难的。当我们玩ChangeSets API 的静态数据时,我们不需要 state。然而,我们很快就会需要它。"迈克补充道。

设置初始状态

"可以使用getInitialState函数设置初始状态。"迈克说。

var App = React.createClass({
  getInitialState: function() {
    return {
      changeSets: []
    };
  },

  render: function(){
    console.log(this.state.changeSets); // prints []  
});

"state 可以通过this.state类似 props 的方式访问。"迈克进一步解释。

设置 state

"我们可能需要根据某些用户事件更新初始状态。使用setState()函数更新 state 也很简单。"迈克通知道。

var App = React.createClass({
  getInitialState: function() {
    return {
      changeSets: [],
      headings: ['Updated At', 'Author', 'Change']
    };
  },

  handleEvent: function(data) {
    this.setState({ changeSets: data.changeSets });
  },

  render: function(){
    …    
});

避免使用 state

"目前,我们不需要 state;然而,当我们从RecentChanges API 获取动态数据时,我们将使用 state 和 props。"迈克进一步补充。

"太好了。基于我们的讨论,我认为我们应该尽可能避免使用 state。"肖恩建议。

"确实。如果一个组件没有变化,那么就没有必要使用 state。在这种情况下,最好依赖于父组件传递的 props。这也避免了组件因为 state 的变化而反复重新渲染。"迈克解释道。

state 与 props 的区别

"肖恩,理解 props 和 state 之间的区别以及何时使用什么非常重要。"迈克说道。

"props 是不可变的。它们不应该被传递给它们的组件更新。它们属于将它们传递给其他组件的组件。state 是组件内部和私有的东西。state 会根据与外部世界的交互而改变。"迈克说。

"state 应该存储尽可能简单的数据,例如输入复选框是否被选中或一个用于隐藏或显示组件的 CSS 类。"迈克补充道。

"还有一点需要确保的是,不要在 state 中重复 props。"迈克说。

var App = React.createClass({
  getInitialState: function() {
    return {
      changeSets: this.props.changeSets
    };
  }
});

"根据传递给 props 的数据设置 state 是可能的。然而,父组件可以更新 props 并将它们再次发送。在这种情况下,如果 state 有任何变化,它将与新数据混淆。"

"此外,数据现在存在于两个地方,因此,管理两个数据源变得更加困难。"迈克解释道。

"我想在这种情况下,直接使用 props 是最好的,对吧?"肖恩问道。

"是的。state 完全是可选的。最好尽可能避免使用。你理解得对。"迈克高兴地说。

组件生命周期概述

"肖恩,现在让我们开始看看如何从openlibrary.org/动态获取数据,将其存储在我们的组件中,并在使其兼容渲染后进行渲染。

组件会经历不同的生命周期事件。它们帮助我们确定何时初始化组件的哪个部分,或者何时获取外部数据。

我们已经看到了一些这些方法,例如rendergetInitialStategetDefaultProps

可以在videos.bigbinary.com/react/react-life-cycle-methods-in-depth.html找到关于同一内容的更新详细列表和示例。

让我们逐一了解这些方法以及它们的使用方式,这样我们就可以开始获取用于显示的动态信息。以下是我们将讨论的方法列表:

  • componentWillMount

  • componentDidMount

  • componentWillReceiveProps(object nextProps)

  • boolean shouldComponentUpdate(object nextProps, object nextState)

  • componentWillUpdate(object nextProps, object nextState)

  • componentDidUpdate(object prevProps, object prevState)

  • componentWillUnmount()

  • React.unmountComponentAtNode(document.body)

    提示

    你可以跟随下一个示例在jsbin.com/tijeco/3/edit

组件生命周期方法

"肖恩,让我们从一个能够触发这些方法的详尽示例开始。"迈克通知说。

console.log('Start') // Marks entry point of JS code.
var App = React.createClass({
    componentWillMount: function(){
      console.log('componentWillMount');
    },

    componentDidMount: function(){
      console.log('componentDidMount');
    },

    getInitialState: function(){
      return { status: true}
    },

    getDefaultProps: function(){
      return {name: 'John'};
    },

    componentWillReceiveProps: function(nextProps){
      console.log('componentWillReceiveProps');
    },

    shouldComponentUpdate: function(nextProps, nextState){
      console.log('shouldComponentUpdate');
      return true;
    },

    componentWillUpdate: function(){
      console.log('componentWillUpdate');
    },

    render: function() {
      console.log('render');
      return <h1 onClick={this.toggleState}>    
             {this.state.status.toString()}
             </h1>
    },

    componentWillUnmount: function(){
      console.log('componentWillUnmount')
    },

    toggleState: function() {
      this.setState({status: !this.state.status})
    }
    });

/* List of methods and signatures for reference
* componentWillMount
* componentDidMount
* componentWillReceiveProps(object nextProps)
* boolean shouldComponentUpdate(object nextProps, object nextState)
* componentWillUpdate(object nextProps, object nextState)
* componentDidUpdate(object prevProps, object prevState)
* componentWillUnmount()
/

React.render(<App name='Jane'/>, document.body);

组件生命周期方法

"它只是显示一个带有true文本的正文,然后点击后,它会改变以显示false。"迈克。

"肖恩,为了使事情简单,我为每个生命周期方法添加了一个简单的console.log()方法,这样我们就可以知道它被调用了。如果我们进行一次全新的运行,以下内容会被打印出来:"

"Start"
"componentWillMount"
"render"
"componentDidMount"

"啊,明白了。基本上,窗口首先打印了Start来表示文件已被加载。"肖恩说。

"正确。接下来,它打印出了componentWillMount。这是我们组件的入口点。当组件第一次在主体上挂载时,会调用此方法。如果你能看到,我们正在调用React.render。”

React.render(<App name='Jane'/>, document.body);

这会触发componentWillMount。在这个方法中,我们可以调用setState来对我们内部数据进行一些更改。然而,这不会调用新的重新渲染或此方法再次调用。

接下来是实际的render方法调用。这负责实际的组件显示。

最后,我们有一个对componentDidMount的调用。这会在组件挂载后立即调用,并且仅在组件渲染后调用一次。

我们可以利用这个方法在组件的初始渲染之后获取我们想要在组件中显示的动态信息。

"一旦完成,我们就完成了组件显示的初始运行!"

"不错。"肖恩惊呼。

"现在,我们添加了一个简单的onClick事件。这会调用this.toggleState,它会将当前状态从true切换到false,反之亦然。"迈克说。

由于状态受到影响,React 重新渲染了App组件。当发生这种情况时,我们可以看到方法调用序列,如下所示:"

"…"
"shouldComponentUpdate"
"componentWillUpdate"
"render"
"…"

"啊,不错。它又经历了一个重新渲染周期。"肖恩说。

"正确。当状态发生变化时,React 知道它需要重新渲染App组件。它首先调用shouldComponentUpdate。这个方法返回truefalse,指示 React 是否渲染组件。"

"我们甚至可以在状态更新时控制组件是否应该重新渲染。这种方法可以返回false,然后即使状态发生变化,React 也不会重新渲染组件。"

    shouldComponentUpdate: function(nextProps, nextState){
      console.log('shouldComponentUpdate');
      return false; // Will not re-render the component.
    },

"我们还可以比较nextPropsnextState与现有值,然后决定是否重新渲染。"

"太棒了,这意味着我们可以得到更快的组件!"肖恩兴奋地说。

"确实如此。默认情况下,它总是返回true,这意味着在变化时总是渲染。"迈克总结道。

"接下来,componentWillUpdate将在渲染之前被调用。我们可以处理任何我们想要做的更改或任何家务工作。需要注意的是,我们在这个方法中不能调用setState。状态更新应该在别处处理。"

"哦,好的,"肖恩。

"我们只剩下componentWillReceiveProps了。"

 componentWillReceiveProps: function(nextProps){
      console.log('componentWillReceiveProps');
    },

"它接收nextProps,这是子组件从父组件接收的新属性。这个方法在初始渲染时不会被调用。我们可以根据属性的变化更新状态或做一些其他家务工作。"

"很好,迈克。我觉得我对这个越来越有感觉了。"

"最后,我们有componentWillUnmount。当组件从主体中卸载时会被调用。我们可以使用这个方法释放资源,执行清理工作,取消任何定时器等等。"

"明白了。"

"好的!让我们更新我们的组件,开始从openlibrary.org/获取信息。"

"所以,我们将更新componentDidMount来执行 AJAX 调用并获取要显示的数据。"

  componentDidMount : function(){
    $.ajax({
      url: 'http://openlibrary.org/recentchanges.json?limit=10',
      context: this,
      dataType: 'json',
      type: 'GET'
    }).done(function (data) {
      var changeSets = this.mapOpenLibraryDataToChangeSet(data);
      this.setState({changeSets: changeSets});
    });
  }

"在这里,我们正在调用openlibrary.org/recentchanges.json?limit=10并请求最近的十个更改。我们将以以下格式获取数据:"

[{
     comment:   "Added new cover",
     kind:      "add-cover",
     author:    {
         key: "/people/fsrc"
     },
     timestamp: "2015-05-25T19:20:33.981700",
     changes:   [
                    {
                        key:      "/books/OL25679864M",
                        revision: 2
                    }
                ],
     ip:        null,
     data:      { url: "" },
     id:        "49441324"
 }, 
{
…
}
]

"我们需要根据我们的要求格式化数据,以便它能够很好地显示。让我们看看它:"

mapOpenLibraryDataToChangeSet : function (data) {
  return data.map(function (change, index) {
    return {
      "when": jQuery.timeago(change.timestamp),
      "who": change.author.key,
      "description": change.comment
    }
  });
} 

"在这里,我们正在提取时间戳、作者信息和更改的描述,即更改中的评论。由于更改的时间是一个时间戳,我们使用了jQuery.timeago插件来获取期望的显示时间,如2 分钟前等等。要使用此插件,我们需要将其包含在我们的 HTMLhead标签中。"迈克解释道。

<script src="img/jquery.timeago.js" type="text/javascript"></script>

"看起来一切都在顺利进行。"肖恩说。

"是的,让我们看看所有这些是如何实际运作的,怎么样?"

var Heading = React.createClass({
    render: function () {
        var headingStyle = {
            backgroundColor: 'FloralWhite',
            fontSize: '19px'
        };
        return (<th style={headingStyle}> {this.props.heading} </th>);
    }
});
var Headings = React.createClass({
    render: function () {
        var headings = this.props.headings.map(function (name, index) {
            return (<Heading key={"heading-" + index} heading={name}/>);
        });

        return (<tr className='table-th'> {headings} </tr>);
    }
});
var Row = React.createClass({
    render: function () {
        var trStyle = {backgroundColor: 'aliceblue'};
        return (<tr style={trStyle}>
            <td> {this.props.changeSet.when} </td>
            <td> {this.props.changeSet.who} </td>
            <td> {this.props.changeSet.description} </td>
        </tr>);
    }
});
var Rows = React.createClass({
    render: function () {
        var rows = this.props.changeSets.map(function (changeSet, index) {
            return (<Row key={index} changeSet={changeSet}/>);
        });

        return (<tbody>{rows}</tbody>);
    }
});

var App = React.createClass({
    getInitialState: function () {
        return {changeSets: [];
    },
    mapOpenLibraryDataToChangeSet: function (data) {
        return data.map(function (change, index) {
            return {
                "when": jQuery.timeago(change.timestamp),
                "who": change.author.key,
                "description": change.comment
            }
        });
    },
    componentDidMount: function () {
        $.ajax({
            url: 'http://openlibrary.org/recentchanges.json?limit=10',
            context: this,
            dataType: 'json',
            type: 'GET'
        }).done(function (data) {
            var changeSets = this.mapOpenLibraryDataToChangeSet(data);
            this.setState({changeSets: changeSets});
        });
    },

    render: function () {
        return (<table className='table'>
            <Headings headings={this.props.headings}/>
            <Rows changeSets={this.state.changeSets}/>
        </table>);
    }
});

var headings = ['Updated at ', 'Author', 'Change'];
React.render(<App headings={headings} />, document.body);

"这就是我们的最终产品!"迈克兴奋地说。

"太棒了,我迫不及待想看看我们接下来会构建什么!"肖恩补充道。

在 Adequate 又一个高效的一天。迈克和肖恩对进度感到满意,于是返回了。

摘要

在本章中,我们探讨了如何在 React 组件中使用 props 和 state 来传递数据。我们还讨论了何时以及如何使用 state 和 props。我们了解了如何使用 propTypes 来验证 props。之后,我们讨论了组件的生命周期。我们讨论了各种生命周期方法及其使用方式。随后,我们利用这些生命周期方法从 Open Library API 获取实时数据。

第四章。组合动态组件和表单

在上一章中,我们看到了 React 组件的各种生命周期方法,React 组件之间的数据流,以及如何在我们的 React 应用程序中管理状态和属性。

在本章中,我们将专注于多个动态组件和构建使用 React 的表单。

我们将涵盖以下要点:

  • 多个动态组件与交互性

  • 受控和非受控组件

  • 表单元素

  • 表单事件和处理程序

在本章结束时,我们将能够创建包含复杂表单的应用程序,这些应用程序使用 React 和动态组件的理解。

迈克和肖恩正在为他们的下一个项目做准备。这个应用程序是一个在线书店的原型,人们可以在这里购买不同的书籍。肖恩很兴奋再次与迈克合作,并学习更多关于 React 的知识。这个项目也是基于 React 的。

React 中的表单

“肖恩,在这个项目中,将有许多与下订单、获取用户的运输和账单信息等相关的事情。我们现在将处理很多表单。”迈克开始说。

“使用 React 的表单吗?”肖恩兴奋地问道。

“是的。这就是为什么今天我们将只关注表单。让我们看看使用 React 使用表单的内在细节。”迈克说。

设置应用程序

“肖恩,我们之前一直在使用 JSBin。但现在我们将本地创建应用程序。我们将使用以下目录结构来存放我们的代码:”

$ tree -L 1
.
├── LICENSE
├── README.md
├── index.html
├── node_modules
├── package.json
├── server.js
├── src
└── webpack.config.js

2 directories, 6 files

  • src目录将包含所有的 React 组件

  • webpack.config.jsserver.js文件将用于使用 webpack 设置本地开发服务器。

  • package.json文件将用于包含我们使用的所有 npm 包的信息

  • index.html文件将是应用程序的起点

“让我们看看我们的index.html文件。”

// index.html
<html>
  <head>
    <title>Forms in React</title>
    <link rel="stylesheet"  href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" />
  </head>
  <body>
    <div id='root' class="container">
    </div>
  </body>
  <script src="img/bundle.js"></script>
</html>

“我们正在使用 bootstrap CSS 来美化我们的应用程序。除此之外,我们还将包含打包后的 JavaScript 作为static/bundle.js。Webpack 将打包我们应用程序中的 JavaScript 代码,并将其放置在static/bundle.js中。”

注意事项

Webpack 是一个模块打包器,它将我们的 JavaScript 代码打包成代表这些模块的静态资源。它还具有其他功能,如热模块替换,我们将在本书中使用这些功能。我们将在第十一章(第十一章。React 工具)中更深入地介绍 webpack 及其配置。

“让我们看看我们的index.js文件,它将是 JavaScript 代码的入口点。”

// src/index.js

import ReactDOM from 'react-dom';
import BookStore from './BookStore';

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

“它将在index.html中的根容器中渲染我们的BookStore组件。现在,唯一剩下的事情就是实际编写我们的BookStore组件的代码。让我们开始吧。”

“迈克,在 JSBin 中,我们的 ES6 代码会自动转换为普通 JavaScript。现在会如何转换呢?”肖恩问道。

"非常好的问题。我忘记提到使用 Babel 了。Babel 是一个 JavaScript 转换器,它将我们的 ES6 和 JSX 代码转换为普通 JavaScript。我已经配置了这个应用程序使用 Babel。你可以检查package.jsonwebpack.config.js来查看我们如何配置 Babel 将 ES6 代码转换为 ES5 代码。但现在我们不必过于担心这个问题。我们稍后会回来,看看整个设置是如何工作的。"

注意

我们将在第十一章 React Tools 中介绍我们如何使用 Babel 和 Webpack。目前,读者不需要担心这个问题,可以直接使用本章源代码中的说明来设置应用程序。在完成本章之前,你也可以查看第十一章 React Tools 以获取更多详细信息。

开始使用表单

"在 React 的世界里,表单的行为与正常 HTML 世界略有不同。它们在 React 世界中比其他 DOM 组件要特殊一些。《inputtextareaoption`标签是 React 提供的一些常见输入组件。" 迈克解释道。

"这些表单组件在用户与之交互时会被修改,比如在输入框中添加一些文本,或者选择一个选项。因此,我们需要确保我们正确地管理这些用户交互。" 迈克进一步解释道。

"让我们从一个简单的输入标签开始,了解用户交互。" 迈克说道。

// src/index.js

import React from 'react';
import ReactDOM from 'react-dom';

var InputExample = React.createClass({
  render() {
    return (
      <input type="text" value="Shawn" />
    );
  }
});
ReactDOM.render(<InputExample />,  
                 document.getElementById('root'));

"迈克,这里的import是什么意思?" 肖恩问道。

"很好,这是我们将在项目中使用的一个 ES6 特性。它用于从其他模块或外部脚本中导入定义的函数。"

注意

更多关于导入的信息可以在developer.mozilla.org/en/docs/web/javascript/reference/statements/import找到。

"我们还在使用 ES6 中定义函数的新方法。"

// Old way of defining function
render: function() {
  return (
    <input type="text" value="Shwn" />
  );
}

// New way of defining function
render() {
    return (
      <input type="text" value="Shawn" />
    );
  }

"使用这种新语法,我们每次定义函数时都不必写function这个词。"

"肖恩,通过运行npm start来启动我们的应用程序。"

$ npm start

> reactjs-by-example-react-forms@0.0.1 start /Users/prathamesh/Projects/sources/reactjs-by-example/chapter4
> node server.js

Listening at localhost:9000
Hash: 8ec0d12965567260413b
Version: webpack 1.9.11
Time: 1639ms

"太棒了,它工作是因为我们已经为启动应用程序配置了package.json脚本部分。"

  // package.json
  ……
  "scripts": {
    "start": "node server.js",
    "lint": "eslint src"
  },
……

"让我们回到我们的输入框。我们看到一个带有预定义值Shawn的正常 HTML 文本输入。一切正常。" 迈克说。

开始使用表单

"迈克,我无法编辑它。看起来这个字段是只读的。此外,我在控制台中看到了一个警告。" 肖恩报告说。

开始使用表单

交互式属性

"没错。它还说明值是一个属性。与值类似,输入字段还支持一些其他属性。" 迈克说。

  • value

  • defaultValue

  • onChange

"正如 React 所警告的那样,我们需要提供defaultValueonChange属性来使这个字段可变,让我们来处理这个问题并添加一个onChange处理程序。由于我们设置了value属性,这个字段是只读的,因为我们已经渲染了一个受控组件。" 迈克解释道。

受控组件

"迈克,什么是受控组件?" 肖恩问道。

"这是一个由 React 控制的输入组件。通过提供值prop,我们通知 React 这个字段的值是"肖恩"。一旦 React 将其声明为"肖恩",任何用户输入都不会产生影响,因为值已经在."中设置。迈克解释道。

"我想我们不得不求助于state而不是props?" 肖恩问道。

"正是这样。当用户与输入字段交互时,我们需要使用stateonChange事件处理程序来更新值prop。你能试一试吗?" 迈克建议。

// src/index.js

var InputExample = React.createClass({
  getInitialState() {
    return (
      { name: '-'}
    );
  },

  handleChange(event) {
    this.setState({ name: event.target.value });
  },

  render() {
    return (
      <input type="text"
             value={this.state.name}
             onChange={this.handleChange} />
    );
  }
});

"太棒了。这种基于状态制作输入值并根据用户交互更新状态的模式使得对用户交互做出响应变得非常容易。我们还可以做一些家务事,比如验证输入。" 迈克进一步解释道。

"例如,你可以将所有文本的格式改为大写。" 迈克补充道。

handleChange: function(event) {
  this.setState({name: event.target.value.toUpperCase()});
}

不受控组件

"React 还有不受控组件,其中没有将value属性传递给输入"。

render() {
    return (
      <input type="text" />
    );
  }

"在这种情况下,用户输入的值将立即反映在输入中。为了设置一些默认的初始值,我们可以传递默认的value属性,这将作为不受控组件的初始值。"。

render() {
    return (
      <input type="text" defaultValue="Shawn"/>
    );
  }

"太棒了。这就完成了。"

开始使用表单向导

"肖恩,我们今天的任务是构建一个表单向导,模拟用户在使用在线书店时将采取的所有步骤。"

  • 我们将从表单开始,用户将选择他们想要购买的书籍。

  • 在下一步中,用户将输入与账单和配送地址相关的信息。

  • 之后,用户需要选择一个配送方式。

  • 最后,用户将确认交易并下订单。

"我们将设计四个不同的表单吗?" 肖恩问道。

"是的。但所有这些都将由一个父组件控制。父组件将跟踪用户所处的状态,并为这一步渲染一个表单。" 迈克解释道。

// src/BookStore.js

import React from 'react';

var BookStore = React.createClass({
  render() {
    switch (step) {
      case 1:
        return <BookList />;
      case 2:
        return <ShippingDetails />;
      case 3:
        return <DeliveryDetails />;
    }
  }
});

"我们将如何控制步骤?" 肖恩问道。

"我们稍后再讨论这个点。在那之前,让我们先填入一些细节。让我们为所有表单添加占位符。" 迈克说。

// src/BookStore.js

var BookList = React.createClass({
  render() {
    return(
      <h1> 
        Choose from wide variety of books available in our store.
      </h1>
    );
  }
});

var ShippingDetails = React.createClass({
  render() {
    return(
      <h1>Enter your shipping information.</h1>
    );
  }
});

var DeliveryDetails = React.createClass({
  render() {
    return (
      <h1>Choose your delivery options here.</h1>
    );
  }
});

"太好了。现在,让我们确保我们始终从第一步开始。" 迈克解释道。

// src/BookStore.js

……

var BookStore = React.createClass({
  getInitialState() {
    return ({ currentStep: 1 });
  },

  render() {
    switch (this.state.currentStep) {
      case 1:
        return <BookList />;
      case 2:
        return <ShippingDetails />;
      case 3:
        return <DeliveryDetails />;
    }
  }
……

});

"既然我们已经确保用户在开始时始终处于第一步,让我们继续完成书店。" 迈克说。

// src/BookStore.js

var BookList = React.createClass({
  getInitialState() {
    return (
      { books: [
        { name: 'Zero to One', author: 'Peter Thiel' },
        { name: 'Monk who sold his Ferrari', author: 'Robin Sharma' },
        { name: 'Wings of Fire', author: 'A.P.J. Abdul Kalam' }
      ] }
    )
  },

  _renderBook(book) {
    return(
      <div className="checkbox">
        <label>
          <input type="checkbox" /> {book.name} -- {book.author}
        </label>
      </div>
    );
  },

  render() {
    return(
      <div>
        <h3> Choose from wide variety of books available in our store </h3>
        <form>
          {this.state.books.map((book) => { 
             return this._renderBook(book); })
          }

          <input type="submit" className="btn btn-success" />
        </form>
      </div>
    );
  }
});

"这是我们静态的表单。它对用户交互没有任何反应。下一步将是让它对事件做出响应。" 迈克补充道。

"太酷了,我对我们如何进入下一步很感兴趣。" 肖恩说。

"我们很快就会到达那里。我们先完成其他事情。"迈克通知道。

"好的。在那之前,这里发生了什么?"

this.state.books.map((book) => { return (this._renderBook(book)) })

"这被称为箭头函数语法来定义函数。它是 ES6 的另一个特性。它是编写函数的简写方式。"

注意

更多关于箭头函数的详细信息可以在developer.mozilla.org/en/docs/Web/JavaScript/Reference/Functions/Arrow_functions找到。

表单事件

"现在让我们处理表单的提交。React 为此提供了onSubmit事件。"迈克说。

// src/BookStore.js
……
// Updating BookStore component

render() {
    return(
      <div>
        <h3> Choose from wide variety of books available in our store </h3>
        <form onSubmit={this.handleSubmit}>
          {this.state.books.map((book) => { return (this._renderBook(book)) })}

          <input type="submit" className="btn btn-success" />
        </form>
      </div>
    );
  },

handleSubmit(event) {
    console.log(event);
    event.preventDefault();
    console.log("Form submitted");
   }
   ……

"现在,下一个任务是获取用户所选的所有书籍。我们可以使用state来实现这一点。"迈克解释道。

// src/BookStore.js
……

// Updating BookStore component

getInitialState() {
    return (
      { books: [
        { id: 1, name: 'Zero to One', author: 'Peter Thiel' },
        { id: 2, name: 'Monk who sold his Fearrary', author: 'Robin Sharma' },
        { id: 3, name: 'Wings of Fire', author: 'A.P.J. Abdul Kalam' }
      ],
        selectedBooks: []
      }
    );
  },

_renderBook(book) {
    return (
      <div className="checkbox" key={book.id}>
        <label>
          <input type="checkbox" value={book.name}
                 onChange={this.handleSelectedBooks}/>
          {book.name} -- {book.author}
        </label>
      </div>
    );
  },

  handleSelectedBooks(event) {
    var selectedBooks = this.state.selectedBooks;
    var index = selectedBooks.indexOf(event.target.value);

    if (event.target.checked) {
      if (index === -1)
        selectedBooks.push(event.target.value);
    } else {
      selectedBooks.splice(index, 1);
    }

    this.setState({selectedBooks: selectedBooks });
}

"我们为复选框添加了onChange处理程序。我们还向复选框提供了valueprop,它将是书籍的名称。最初,selectedBooks状态将通过getInitialState函数设置为空数组。handleSelectedBooks方法将检查复选框是否被选中。我们使用 React 为复选框输入提供的 checked prop。与 value 类似,它也会随着用户交互而更新。"

最后,我们使用selectedBooks的新值更新状态。因此,在任何时候,我们都会在this.state.selectedBooks中掌握所选书籍。"迈克解释道。

"太好了!"肖恩说。

父子关系

"现在,接下来的步骤是与父组件通信。目前,我们的子组件没有通过 props 进行通信的方法。我们希望与父组件通信,因为我们想将用户所选的书籍发送到父组件。子组件与父组件通信的最简单方法是使用 props。"迈克解释道。

"但是 props 通常是发送给子组件的属性或属性,对吧?子组件如何使用它们与父组件通信?"肖恩问道。

"记住{}语法。我们可以将任何有效的表达式作为 prop 传递。我们可以将函数回调作为 prop 传递给子组件。子组件可以调用它来更新父组件的状态。现在让我们更新我们的BookStore组件。"迈克解释道。

// src/BookStore.js

……
// Updating BookStore component

  updateFormData(formData) {
    console.log(formData);
  },

  render() {
    switch (this.state.currentStep) {
      case 1:
        return <BookList 
                 updateFormData={this.updateFormData} />;
      case 2:
        return <ShippingDetails  
                 updateFormData={this.updateFormData} />;
      case 3:
        return <DeliveryDetails 
                 updateFormData={this.updateFormData} />;
    }
  }
……

});

"我们将updateFormData函数作为 prop 传递给所有子组件。这个函数将负责更新表单数据。我们还需要更新BookList以便使用它。"

// src/BookStore.js
// Updating BookList component

……
  handleSubmit(event) {
    event.preventDefault();

    this.props.updateFormData({ selectedBooks: 
                                this.state.selectedBooks });
  }
  ……
}); 

"每当用户提交第一个表单时,BookList组件现在会调用updateFormData函数并将当前所选书籍传递给它。"迈克解释道。

"因此,每个表单都会将其数据发送到父组件,我们将使用完整的数据进行最终提交,对吧?"肖恩问道。

"确实如此。我们还需要在父组件中存储传入的数据。"

// src/BookStore.js
// Updating BookStore component

var BookStore = React.createClass({
  getInitialState() {
    return ({ currentStep: 1, formValues: {} });
  },

  updateFormData(formData) {
    var formValues = Object.assign({}, this.state.formValues, formData);
    this.setState({formValues: formValues});  
  },

  render() {
    switch (this.state.currentStep) {
      case 1:
        return <BookList updateFormData={this.updateFormData} />;
      case 2:
        return <ShippingDetails updateFormData={this.updateFormData} />;
      case 3:
        return <DeliveryDetails updateFormData={this.updateFormData} />;
    }
  }
});

"我们添加了状态来存储formValues。每当用户提交表单时,子表单将调用父级的updateFormData函数。这个函数将合并存储在父级formValues中的当前数据与传入的formData,并将状态重置为新formValues。在这种情况下,我们将在formValues对象中获得selectedBooks,如下所示:"迈克说。

 { selectedBooks: ['Zero to One', 'Monk who sold his Ferrary'] }

"请注意,我们正在使用另一个 ES6 方法——Object.assignObject.assign()方法用于将一个或多个源对象的所有可枚举属性值复制到目标对象中。"

"在我们的情况下,使用Object.assign将合并表单值的当前状态与用户交互后更改的新表单值。然后我们将使用这些更新后的数据来更新组件的状态。我们使用Object.assign而不是直接修改组件的状态。我们将在接下来的章节中解释为什么这样做比直接修改组件的状态更好。"说迈克。

var formValues = Object.assign({}, this.state.formValues, formData);

"有道理。这处理了表单数据的更新。现在,我们如何进入下一个步骤呢?"肖恩问道。

"很简单。每次我们更新表单数据时,我们还需要更新BookStore组件的currentStep方法。你能试一下吗?"迈克问道。

// src/BookStore.js
// Updating BookStore component

var BookStore = React.createClass({
  updateFormData(formData) {
    var formValues = Object.assign({}, this.state.formValues, formData);
    var nextStep = this.state.currentStep + 1;
    this.setState({currentStep: nextStep, formValues: formValues});
    console.log(formData);
  },

  render() {
    switch (this.state.currentStep) {
      case 1:
        return <BookList updateFormData={this.updateFormData} />;
      case 2:
        return <ShippingDetails updateFormData={this.updateFormData} />;
      case 3:
        return <DeliveryDetails updateFormData={this.updateFormData} />;
    }
  }
});

"太好了。你在updateFormData回调中更新了步骤1。这将把用户带到下一个步骤。"迈克说。

表单验证

"肖恩,我认为我们还应该给BookList组件添加基本的验证,这样用户就不能在选择一本书之前进入下一个步骤了。"迈克说。

"同意。让我试试。"肖恩回答。

// src/BookStore.js
// Updating BookList component

var BookList = React.createClass({
  getInitialState() {
    return (
      { books: [
        { id: 1, name: 'Zero to One', author: 'Peter Thiel' },
        { id: 2, name: 'Monk who sold his Fearrary', author: 'Robin Sharma' },
        { id: 3, name: 'Wings of Fire', author: 'A.P.J. Abdul Kalam' }
      ],
        selectedBooks: [],
        error: false
      }
    );
  },

  _renderError() {
    if (this.state.error) {
      return (
        <div className="alert alert-danger">
          {this.state.error}
        </div>
      );
    }
  },

  handleSubmit(event) {
    event.preventDefault();

    if(this.state.selectedBooks.length === 0) {
      this.setState({error: 'Please choose at least one book to continue'});
    } else {
      this.setState({error: false});
      this.props.updateFormData({ selectedBooks: this.state.selectedBooks });
    }
  },

  render() {
    var errorMessage = this._renderError();

    return (
      <div>
        <h3> Choose from wide variety of books available in our store </h3>
        {errorMessage}
        <form onSubmit={this.handleSubmit}>
          { this.state.books.map((book) => { return (this._renderBook(book)); })}
          <input type="submit" className="btn btn-success" />
        </form>
      </div>
    );
  }
});

"我为验证错误添加了状态。最初,它将被设置为false。当用户提交表单后,我们将检查用户是否没有选择任何内容,并设置适当的错误信息。状态将被更新,并相应地显示错误信息。如果用户至少选择了一本书,那么错误状态将被设置为false,错误信息将不会显示。我们在 Bootstrap 类的帮助下得到了一个很好的错误信息。"肖恩说。

表单验证

"太棒了。现在让我们转到第二个表单。我们希望接受用户的发货详情,如地址和联系信息。"迈克说。

发货详情步骤

"肖恩,在这个步骤中,我们想要获取用户的发货偏好。它将包含发货地址和客户姓名。"迈克解释说。

"我们也应该添加一个电话号码。"肖恩补充说。

"当然。这就是我们的发货详情表单看起来像。"迈克告知。

// src/BookStore.js
// Adding ShippingDetails component

var ShippingDetails = React.createClass({
  getInitialState() {
    return (
      { fullName: '', contactNumber: '', shippingAddress: '', error: false }
    );
  },

  _renderError() {
    if (this.state.error) {
      return (
        <div className="alert alert-danger">
          {this.state.error}
        </div>
      );
    }
  },

  _validateInput() {
    if (this.state.fullName === '') {
      this.setState({error: "Please enter full name"});
    } else if (this.state.contactNumber === '') {
      this.setState({error: "Please enter contact number"});
    } else if (this.state.shippingAddress === '') {
      this.setState({error: "Please enter shipping address"});
    } else {
      this.setState({error: false});
      return true;
    }

  },

  handleSubmit(event) {
    event.preventDefault();

    var formData = { fullName: this.state.fullName,
                     contactNumber: this.state.contactNumber,
                     shippingAddress: this.state.shippingAddress };

    if (this._validateInput()) {
      this.props.updateFormData(formData);
    }
  },

  handleChange(event, attribute) {
    var newState = this.state;
    newState[attribute] = event.target.value;
    this.setState(newState);
    console.log(this.state);
  },

  render() {
    var errorMessage = this._renderError();

    return (
      <div>
        <h1>Enter your shipping information.</h1>
        {errorMessage}
        <div style={{width: 200}}>
          <form onSubmit={this.handleSubmit}>
            <div className="form-group">
              <input className="form-control"
                     type="text"
                     placeholder="Full Name"
                     value={this.state.fullName}
                     onChange={(event) => this.handleChange(event, 'fullName')} />
            </div>

            <div className="form-group">
              <input className="form-control"
                     type="text"
                     placeholder="Contact number"
                     value={this.state.contactNumber}
                     onChange={(event) => this.handleChange(event, 'contactNumber')}/>
            </div>

            <div className="form-group">
              <input className="form-control"
                     type="text"
                     placeholder="Shipping Address"
                     value={this.state.shippingAddress}
                     onChange={(event) => this.handleChange(event, 'shippingAddress')} />
            </div>

            <div className="form-group">
              <button type="submit"
                      ref="submit"
                      className="btn btn-success">
                Submit
              </button>
            </div>
          </form>
        </div>
      </div>
    );
  }
});

"这个组件几乎使用了与我们的第一个表单相同的代码。我们向用户显示用于配送详情的文本框。有验证,所有字段都是必填项。我们使用组件状态中的onChange处理程序同步用户输入的数据,并在最后将此状态传递给父组件的updateFormData函数。" 迈克解释道。

"现在,在第二步的结尾,我们已经收集了用户选择的书籍列表和配送信息。" 迈克说。

{ selectedBooks: ["Zero to One", "Wings of Fire"],
  fullName: "John Smith",
  contactNumber: "1234567890",
  shippingAddress: "10th Cross, NY" }

"我可以看到我们是如何将输入字段的value属性分配给它对应的状态的。" 肖恩。

  value={this.state.shippingAddress}

"是的。正如我们之前讨论的受控和非受控组件,我们正在确保 UI 反映了基于用户交互的最新状态。" 迈克说。

配送详情步骤

"肖恩,下一步是提供各种配送选项。目前,让我们假设用户可以在Primary配送(意味着次日送达)和Normal配送(意味着 3-4 天送达)之间进行选择。默认情况下,必须选择Primary选项。用户也可以选择Normal配送选项。你能尝试构建这个最后一步吗?" 迈克问道。

// src/BookStore.js
// Adding DeliveryDetails component

var DeliveryDetails = React.createClass({
  getInitialState() {
    return (
      { deliveryOption: 'Primary' }
    );
  },

  handleChange(event) {
    this.setState({ deliveryOption: event.target.value});
  },

  handleSubmit(event) {
    event.preventDefault();
    this.props.updateFormData(this.state);
  },

  render() {
    return (
      <div>
        <h1>Choose your delivery options here.</h1>
        <div style={{width:200}}>
          <form onSubmit={this.handleSubmit}>
            <div className="radio">
              <label>
                <input type="radio"
                       checked={this.state.deliveryOption === "Primary"}
                       value="Primary"
                       onChange={this.handleChange} />
                Primary -- Next day delivery
              </label>
            </div>
            <div className="radio">
              <label>
                <input type="radio"
                       checked={this.state.deliveryOption === "Normal"}
                       value="Normal"
                       onChange={this.handleChange} />
                Normal -- 3-4 days
              </label>
            </div>

            <button className="btn btn-success">
              Submit
            </button>
          </form>
        </div>
      </div>
    );
  }
});

"迈克,根据需要,我通过状态添加了Primary作为默认选项。我使用了单选按钮及其checked属性,以确保在任何时候只能选择一个单选按钮。此外,使用onChange回调更新状态以反映所选选项。最后,调用updateFormData函数来更新父表单数据。" 肖恩解释道。

"太好了,肖恩。看起来不错。我想我们可能还需要对我们的BookStore组件做一些修改,因为我们现在想在用户完成选择配送选项后显示一个确认页面。" 迈克说。

// src/BookStore.js
// Updating BookStore component

var BookStore = React.createClass({
 render() {
    switch (this.state.currentStep) {
      case 1:
        return <BookList updateFormData={this.updateFormData} />;
      case 2:
        return <ShippingDetails updateFormData={this.updateFormData} />;
      case 3:
        return <DeliveryDetails updateFormData={this.updateFormData} />;
      case 4:
        return <Confirmation data={this.state.formValues}/>;}/>;}/>;
      default:
        return <BookList updateFormData={this.updateFormData} />;
    }
  }
});

// Adding Conformation step

var Confirmation = React.createClass({
  render() {
    return (
      <div>
        <h1>Are you sure you want to submit the data?</h1>
      </div>
    );
  }
});

"我们已经将Confirmation组件作为最后一步添加,并且将默认表单设置为BookList表单,用户可以在其中选择书籍。现在,我们只需要在确认页面上显示到最后一步所捕获的所有信息,并将一切实际提交到后端。" 迈克说。

"让我更新一下确认页面,以显示用户输入的数据。" 肖恩说。

var Confirmation = React.createClass({
  handleSubmit(event) {
    event.preventDefault();
    this.props.updateFormData(this.props.data);
  },

  render() {
    return (
      <div>
        <h1>Are you sure you want to submit the data?</h1>
        <form onSubmit={this.handleSubmit}>
          <div>
            <strong>Full Name</strong> : { this.props.data.fullName }
          </div><br/>
          <div>
            <strong>Contact Number</strong> : { this.props.data.contactNumber }
          </div><br/>
          <div>
            <strong>Shipping Address</strong> : { this.props.data.shippingAddress }
          </div><br/>
          <div>
            <strong>Selected books</strong> : { this.props.data.selectedBooks.join(", ") }
          </div><br/>
          <button className="btn btn-success">
            Place order
          </button>
        </form>
      </div>
    );
  }
});

"迈克,我已经列出了用户选择的所有数据,并提供了一个下单按钮。看起来是这样的。" 肖恩。

配送详情步骤

"太好了。我已经准备好了成功页面。让我们试试吧。" 迈克。

// src/BookStore.js
// Adding Success step

var Success = React.createClass({
  render() {
    var numberOfDays = "1 to 2 ";

    if (this.props.data.deliveryOption === 'Normal') {
      numberOfDays = "3 to 4 ";
    }
    return (
      <div>
        <h2>
          Thank you for shopping with us {this.props.data.fullName}.
        </h2>
        <h4>
          You will soon get {this.props.data.selectedBooks.join(", ")} at {this.props.data.shippingAddress} in approrximately {numberOfDays} days.
        </h4>
      </div>
    );
  }
});

"我们还需要更新BookStore以显示成功页面。" 迈克补充说。

// Updating render method of BookStore component

  render() {
    switch (this.state.currentStep) {
      case 1:
        return <BookList updateFormData={this.updateFormData} />;
      case 2:
        return <ShippingDetails updateFormData={this.updateFormData} />;
      case 3:
        return <DeliveryDetails updateFormData={this.updateFormData} />;
      case 4:
        return <Confirmation data={this.state.formValues} updateFormData={this.updateFormData}/>;
      case 5:
        return <Success data={this.state.formValues}/>;
      default:
        return <BookList updateFormData={this.updateFormData} />;
    }
  }

"现在,用户确认并下单后,将显示成功页面作为最后一页。我们的表单向导的第一个版本已经完成。" 迈克通知。

配送详情步骤

摘要

在本章中,我们讨论了如何在 React 中开发表单。我们看到了如何使用不同的输入类型和事件处理器来响应用户对这些输入的交互。我们使用状态和属性来管理表单数据从一个步骤流向另一个步骤的流程。我们还看到了如何使用动态组件根据用户当前所在的步骤向用户显示特定的表单。

在下一章中,我们将继续介绍表单向导,并了解混入(mixins)如何帮助我们更好地组织代码。

第五章:Chapter 5. Mixins and the DOM

In the previous chapter, we took a deep dive into React Forms. We took a look at building multiple components and interactivity between them, Controller and Uncontrolled Components, building Forms and Form elements, and Form events and handlers for the events. We build a form to capture cart-checkout flow and orders being placed in a multi-step form.

In this chapter, we will focus on abstracting content using mixins and touch upon DOM handling.

Here, we will cover the following points:

  • Mixins

  • PureRender mixin

  • React and the DOM

  • Refs

At the end of this chapter, we will be able to abstract and reuse logic across our components and learn how to handle DOM from within the components.

Back at the office

The duo was back at work. Mike entered with a cup of coffee. It was morning and the office had just started to buzz.

"So Shawn, we did a lot of complex forms stuff last time. Our cart flow is now complete. However, now we have been asked to add a timeout to the cart. We need to show a timer to the user that they need to checkout and complete the order in 15 minutes."

"Any idea how we can do this?"

"Umm, maintain a state for timer and keep updating every second? Take some action when the timer hits zero."

"Right! We will use intervals to reduce the timeout values and keep updating our views to display the timer. As we have been storing the form data in a single place, our Bookstore component, let's go ahead and add a state value that will track this timeout value. Let's change our initial state to something similar to the following:"

getInitialState() {
    return ({currentStep: 1, formValues: {}, cartTimeout: 60 * 15});
  }

"60 X 15, that's 15 minutes in seconds value. We will also need to add a method to keep updating this state so that we can use it freely from here as well as the child components."

updateCartTimeout(timeout){
    this.setState({cartTimeout: timeout});
}

"Cool."

"Now, what we will do is define what are called as mixins."

"Mixins?"

"Yeah, mixins allow us to share a code across components. Let's take a look at how we are going to use it before moving ahead."

var SetIntervalMixin = {

  componentWillMount: function() {
    this.intervals = [];
  },

  setInterval: function() {
    this.intervals.push(setInterval.apply(null, arguments));
  },

  componentWillUnmount: function() {
    this.intervals.map(clearInterval);
  }
};

module.exports = SetIntervalMixin;

"所以我们在这里做的 nothing much but defining an object. We will see how we use it in our components."

"As you can see, what we are trying to achieve here is add a way to track all our interval handlers, as follows:"

componentWillMount: function() {
    this.intervals = [];
  }

"Here, we are first initializing an array to hold instances to intervals that we will be creating. Next, we will define a method that can be used to define new intervals, as follows:"

  setInterval: function() {
    this.intervals.push(setInterval.apply(null, arguments));
  }

"Got it. I see the last bit is defining the componentWillUnmount method and we have already defined componentWillMount; but this isn't a React component. Why do we have these method here?"

"Oh right. Let's take a look at the following method first:"

  componentWillUnmount: function() {
    this.intervals.map(clearInterval);
  }

"What this method does is clean up the intervals, which we might have created, before we unmount our component."

"Got it."

"Now, as you mentioned, we have two life cycle methods here—componentWillMount and componentWillUnmount."

"当我们开始在组件中使用这个功能时,它们就像我们组件中其他类似的生命周期方法一样被调用。"

"哦,很好。这两个方法都会被调用吗?”肖恩问。

"没错。现在我们已经定义了 mixin,让我们开始使用它!"

"我们首先想开始使用这个功能的地方是在交付详情页。这就像做以下事情一样简单:"

var DeliveryDetails = React.createClass({
…
mixins: [SetIntervalMixin]
…

"太棒了,接下来我们希望开始使用这个功能来存储cartTimeout值并更新它们。你能定义一个 mixin 来完成这个任务吗?”迈克问道。

"好的,我将首先定义一个方法来递减购物车计时器,这将保持更新状态。接下来,我们需要实际设置超时,以便每隔一段时间调用该方法,使其每秒调用一次以递减时间?"

"没错,让我们看看你会怎么做。"

var CartTimeoutMixin = {
  componentWillMount: function () {
    this.setInterval(this.decrementCartTimer, 1000);
  },

  decrementCartTimer(){
    if (this.state.cartTimeout == 0) {
      this.props.alertCartTimeout();
      return;
    }
    this.setState({cartTimeout: this.state.cartTimeout - 1});
  },

};

"太好了,这正是我们需要的。但我们遗漏了一部分;我们需要能够将这个信息发送回父组件以存储我们在这里更新的计时器值。"

"我们还将注意从父组件传递当前计时器的状态到子组件。"

"哦,对了。"

"让我们回到父组件,开始传递购物车计时器值给子组件。现在我们的渲染方法应该看起来像这样:"

……
render() {
    switch (this.state.currentStep) {

      case 1:
        return <BookList updateFormData={this.updateFormData}/>;

      case 2:
        return <ShippingDetails updateFormData={this.updateFormData}
                                cartTimeout={this.state.cartTimeout}
                                updateCartTimeout={this.updateCartTimeout} />;

      case 3:
        return <DeliveryDetails updateFormData={this.updateFormData}
                                cartTimeout={this.state.cartTimeout}
                                updateCartTimeout={this.updateCartTimeout} />;

……

"请注意,我们在这里传递了updateCartTimeout方法。这是我们将在 mixin 中开始使用的东西。"

"接下来,我们将更新DeliveryDetails组件以开始存储cartTimeout值。"

getInitialState() {
    return { deliveryOption: 'Primary', cartTimeout: this.props.cartTimeout };
 } 

"有了这个设置,我们现在可以设置交付选项页的渲染方法,现在应该看起来像以下这样:"

  render() {

    var minutes = Math.floor(this.state.cartTimeout / 60);
    var seconds = this.state.cartTimeout - minutes * 60;

    return (
      <div>
        <h1>Choose your delivery options here.</h1>
        <div style={{width:200}}>
          <form onSubmit={this.handleSubmit}>
            <div className="radio">
              <label>

                <input type="radio"
                       checked={this.state.deliveryOption === "Primary"}
                       value="Primary"
                       onChange={this.handleChange} />
                Primary -- Next day delivery
              </label>
            </div>

            <div className="radio">
              <label>
                <input type=e"radio"
                       checked={this.state.deliveryOption === "Normal"}
                       value="Normal"
                       onChange={this.handleChange} />
                Normal -- 3-4 days
              </label>
            </div>

            <button className="btn btn-success">
              Submit
            </button>

          </form>
        </div>

        <div className="well">
          <span className="glyphicon glyphicon-time" aria-hidden="true"></span> You have {minutes} Minutes, {seconds} Seconds, before confirming order
        </div>

      </div>
    );
  }

"我们还需要开始使用CartMixin,所以我们的mixins导入应该看起来像以下这样:"

…
mixins: [SetIntervalMixin, CartTimeoutMixin],
…

"很好,让我看看现在的运输信息看起来怎么样。"

回到办公室

"它工作了!”肖恩兴奋地说。

"太棒了。记住,肖恩,现在我们需要在切换到其他页面时将信息传递回父组件。"

"是的,我们应该将其添加到使用了 mixin 的组件中?"

"更好的是,让我们将以下代码添加到 mixin 中:"

….
componentWillUnmount(){
    this.props.updateCartTimeout(this.state.cartTimeout);
  }
….

"现在我们的 mixin 应该看起来像以下这样:"

var CartTimeoutMixin = {
  componentWillMount: function () {
    this.setInterval(this.decrementCartTimer, 1000);
  },

  decrementCartTimer(){
    if (this.state.cartTimeout == 0) {
      this.props.alertCartTimeout();
      return;
    }
    this.setState({cartTimeout: this.state.cartTimeout - 1});
  },

  componentWillUnmount(){
    this.props.updateCartTimeout(this.state.cartTimeout);
  }

};

module.exports = CartTimeoutMixin;

"我们的 mixin 现在将在组件卸载时更新当前的购物车值。"

"我们遗漏了一件事,它是这个 mixin 的一部分。当计时器达到零时,我们调用this.props.alertCartTimeout()。"

"我们将在父组件上定义这个,并传递它以便从子组件调用,如下所示:"

  alertCartTimeout(){
    this.setState({currentStep: 10});
  },

"然后更新我们的渲染方法,以便在达到超时步骤时进行处理,如下所示:"

render() {
    switch (this.state.currentStep) {
      case 1:
        return <BookList updateFormData={this.updateFormData}/>;
      case 2:
        return <ShippingDetails updateFormData={this.updateFormData}
                                cartTimeout={this.state.cartTimeout}
                                updateCartTimeout={this.updateCartTimeout}
                                alertCartTimeout={this.alertCartTimeout}/>;
      case 3:
        return <DeliveryDetails updateFormData={this.updateFormData}
                                cartTimeout={this.state.cartTimeout}
                                updateCartTimeout={this.updateCartTimeout}
                                alertCartTimeout={this.alertCartTimeout}/>;
      case 4:
        return <Confirmation data={this.state.formValues}
                             updateFormData={this.updateFormData}
                             cartTimeout={this.state.cartTimeout}/>;
      case 5:
        return <Success data={this.state.formValues} cartTimeout={this.state.cartTimeout}/>;

      case 10:
       /* Handle the case of Cart timeout */
        return <div><h2>Your cart timed out, Please try again!</h2></div>;
      default:
        return <BookList updateFormData={this.updateFormData}/>;
    }
  }

"让我们看看完成它后DeliveryDetails组件看起来怎么样:"

import React from 'react';
import SetIntervalMixin from './mixins/set_interval_mixin'
import CartTimeoutMixin from './mixins/cart_timeout_mixin'

var DeliveryDetails = React.createClass({
  propTypes: {
    alertCartTimeout: React.PropTypes.func.isRequired,
    updateCartTimeout: React.PropTypes.func.isRequired,
    cartTimeout: React.PropTypes.number.isRequired
  },

  mixins: [SetIntervalMixin, CartTimeoutMixin],

  getInitialState() {
    return { deliveryOption: 'Primary', cartTimeout: this.props.cartTimeout };
  },

  componentWillReceiveProps(newProps){
    this.setState({cartTimeout: newProps.cartTimeout});
  },

  handleChange(event) {
    this.setState({ deliveryOption: event.target.value});
  },

  handleSubmit(event) {
    event.preventDefault();
    this.props.updateFormData(this.state);
  },

  render() {
    var minutes = Math.floor(this.state.cartTimeout / 60);
    var seconds = this.state.cartTimeout - minutes * 60;
    return (
      <div>
        <h1>Choose your delivery options here.</h1>
        <div style={{width:200}}>
          <form onSubmit={this.handleSubmit}>
            <div className="radio">
              <label>
                <input type="radio"
                       checked={this.state.deliveryOption === "Primary"}
                       value="Primary"
                       onChange={this.handleChange} />
                Primary -- Next day delivery
              </label>
            </div>
            <div className="radio">
              <label>
                <input type="radio"
                       checked={this.state.deliveryOption === "Normal"}
                       value="Normal"
                       onChange={this.handleChange} />
                Normal -- 3-4 days
              </label>
            </div>

            <button className="btn btn-success">
              Submit
            </button>
          </form>
        </div>
        <div className='well'>
          <span className="glyphicon glyphicon-time" aria-hidden="true"></span> You have {minutes} Minutes, {seconds} Seconds, before confirming order
        </div>
      </div>
    );
  }
});

module.exports = DeliveryDetails;

"我们还将更新我们的ShippingDetails组件,使其看起来像以下这样:"

import React from 'react';
import SetIntervalMixin from './mixins/set_interval_mixine'
import CartTimeoutMixin from './mixins/cart_timeout_mixin'

var ShippingDetails = React.createClass({
  propTypes: {
    alertCartTimeout:React.PropTypes.func.isRequired,
    updateCartTimeout: React.PropTypes.func.isRequired,
    cartTimeout: React.PropTypes.number.isRequired
  },

  mixins: [SetIntervalMixin, CartTimeoutMixin],

  getInitialState() {
    return {fullName: '', contactNumber: '', shippingAddress: '', error: false, cartTimeout: this.props.cartTimeout};
  },
  _renderError() {
    if (this.state.error) {
      return (
        <div className=e"alert alert-danger">
          {this.state.error}
        </div>
      );
    }
  },

  _validateInput() { 
  …..
  },

  handleSubmit(event) {
  ….
  },

  handleChange(event, attribute) {
    var newState = this.state;
    newState[attribute] = event.target.value;
    this.setState(newState);
    console.log(this.state);
  },

  render() {
    var errorMessage = this._renderError();
    var minutes = Math.floor(this.state.cartTimeout / 60);
    var seconds = this.state.cartTimeout - minutes * 60;

    return (
      <div>
        <h1>Enter your shipping information.</h1>
           …..

        <div className='well'>
          <span className="glyphicon glyphicon-time" aria-hidden="true"></span> You have {minutes} Minutes, {seconds} Seconds, before confirming order
        </div>
      </div>
    );
  }
});

module.exports = ShippingDetails;

"现在它应该看起来像以下截图所示:"

回到办公室

"太棒了,”肖恩兴奋地说。

"在超时的情况下,我们有简单的显示:"

回到办公室

添加模态框

"好吧,这工作得很好,”迈克继续说。

"然而,目前这有点笨拙。超时后,用户无法进行任何操作。我们可以添加一个弹出窗口来通知用户。而不是显示错误页面,让我们显示一个带有警告的模态框,并将用户重定向到第一页,这样用户就可以重新开始流程。我们可以使用 Bootstrap 模态框来实现这一点。"

"明白了。你想让我试一试吗?"肖恩问道。

"继续!"

"让我先从设置模态框开始。我将使用一个简单的 Bootstrap 模态框来显示它。完成之后,我需要从alertCartTimeout调用模态框的显示。我想我还需要设置显示第一页和重置表单数据。"

"正确。"

"这就是模态框将呈现的样子"

import React from 'react';

var ModalAlertTimeout = React.createClass({
  render() {
    return (

      <div className="modal fade" ref='timeoutModal'>
        <div className="modal-dialog">
          <div className="modal-content">
            <div className="modal-header">
              <button type="button" className="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
              <h4 className="modal-title">Timeout</h4>
            </div>
            <div className="modal-body">
              <p>The cart has timed-out. Please try again!</p>
            </div>
          </div>
        </div>
      </div>
    );
  }
});
module.exports = ModalAlertTimeout;

"很好。接下来,你将更新Bookstore组件的alertCartTimeout方法。"

"是的,我在 body 中添加了一个新的空 HTML 元素,ID 为modalAlertTimeout。这将用于显示新的模态框并在其上方挂载组件。我还将更改警告超时方法如下:"

alertCartTimeout(){
    React.render(<ModalAlertTimeout />, document.getElementById('modalAlertTimeout'));
    this.setState({currentStep: 1, formValues: {}, cartTimeout: 1});
  }

"啊,让我们看看这个做了什么"迈克继续说,检查肖恩所做的更改。

"肖恩,看起来超时将我们带到了第一页,但没有显示模态警告"

"哦,对了。我们仍然需要从 Bootstrap 调用模态框的显示。"

"正确。让我来处理这个问题,肖恩。在我们的ModalAlertTimeout中,我们将在组件成功挂载后添加一个方法调用以显示模态框,如下所示:"

componentDidMount(){
    setTimeout(()=> {
      let timeoutModal = this.refs.timeoutModal.getDOMNode();
      $(timeoutModal).modal('show');
    }, 100);
  }

"啊,我明白了我们在这里做了一些 DOM 操作。"

"是的,让我过一遍它们。"

引用

"我想我们之前用过这个,”肖恩问道。"

"是的。引用的作用是给我们一个引用组件某部分的句柄。我们在表单中做过这个。在这里,我们使用它来获取对模态框的引用,这样我们就可以在上面调用modal()方法。"

"这将反过来显示模态框。"

"现在,注意我们是如何使用getDOMNode()方法的。"

"是的。它做什么?"

"getDOMNode()方法帮助我们获取渲染 React 元素的底层 DOM 节点。在我们的例子中,我们想在 DOM 节点上调用一个方法。"

"当我们调用this.refs.timeoutModal时,它返回给我们一个组件的引用对象。"

"这与实际的 DOM 组件不同。它实际上是一个 React 包装的对象。为了获取底层的 DOM 对象,我们调用了getDOMNode()。"

"明白了。"

"接下来,我们将所有这些包裹在一个setTimeout调用中,这样我们就可以在 React 组件成功渲染并且模态框内容存在于页面上时调用它。"

"最后,我们调用了$(timeoutModal).modal('show')来调用模态框!"

"让我们看看我们的模态框现在看起来怎么样。"

import React from 'react';

var ModalAlertTimeout = React.createClass({
  componentDidMount(){
    setTimeout(()=> {
      let timeoutModal = this.refs.timeoutModal.getDOMNode();
      $(timeoutModal).modal('show');
    }, 100);
  },

  render() {
    return (

      <div className="modal fade" ref='timeoutModal'>
        <div className="modal-dialog">
          <div className="modal-content">
            <div className="modal-header">
              <button type="button" className="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
              <h4 className="modal-title">Timeout</h4>
            </div>
            <div className="modal-body">
              <p>The cart has timed-out. Please try again!</p>
            </div>
          </div>
        </div>
      </div>
    );
  }
});
module.exports = ModalAlertTimeout;

"让我们看看现在看起来怎么样。"

引用

"既然我们正在讨论这个,还有一个关于 DOM 的问题。我们可以调用getDOMNode()来获取当前组件的节点。因此,我们可以简单地调用this.getDOMNode(),这将返回给我们一个元素!"

"好的,让我们这么做。当有人关闭模态时,我们将卸载模态,以便在第二次渲染时重新调用它。"

"让我们定义一个回调方法来完成这个任务,如下:"

unMountComponent () {
    React.unmountComponentAtNode(this.getDOMNode().parentNode);
  }

"最后,我们将设置一个回调函数在模态关闭时执行,如下:"

$(timeoutModal).on('hidden.bs.modal', this.unMountComponent);

"这样一来,我们就完成了!当模态隐藏时,组件将会卸载。"

"注意我们是如何在 DOM 节点上使用parentNode属性来隐藏模态的。这有助于我们获取包含 React 元素的容器,并使用它来移除模态。"

"太好了。这真是一次很好的复习。谢谢 Mike!"

随着这些,这对搭档回到了检查他们刚刚所做的各种更改。

摘要

在本章中,我们探讨了重构组件。我们学习了如何利用混入(mixins)提取相似的功能,以便在组件间无缝使用。我们还研究了 DOM 交互,使用 refs 以及从组件中执行相关的 DOM 操作。

在下一章中,我们将探讨 React 在服务器端的运行方式。我们将看到 React 如何允许我们在服务器上渲染和处理组件,以预渲染 HTML,这对于多个原因来说是有用的。我们还将研究这如何影响 React 组件的生命周期。

第六章。React 在服务器端

在上一章中,我们查看了对组件的重构。我们看到了如何使用混入(mixins)提取相似的功能,以便在组件之间无缝使用。我们还查看了一下 DOM 交互,使用 refs 以及从组件中相关的 DOM 操作。

在本章中,我们将探讨 React 在服务器端的运行方式。React 允许我们在服务器上渲染和处理组件,以预渲染 HTML,这有几个原因很有用。我们还将看看这如何影响 React 组件的生命周期。

在本章中,我们将涵盖以下要点:

  • 服务器端渲染

  • 渲染函数

  • 服务器端组件生命周期

在本章结束时,我们将能够开始在服务器端使用 React 组件,并理解其与服务器端的交互和影响。

让 React 在服务器上渲染

"嘿,肖恩!" 迈克带着一杯咖啡闯入他们的工作场所,吓了肖恩一跳。

"早上好,迈克,”肖恩回答道。

阳光在肖恩的桌子上闪耀,他们开始讨论即将开始的新项目。

"肖恩,我从卡拉那里刚刚得知,我们有一个新的项目需要承担。客户要求我们为我们的 Open Library 项目构建一个简单的搜索页面。"

肖恩和迈克之前构建了一个应用程序,用于显示来自openlibrary.com API 的最新更改。他们现在将基于 Open Library 的搜索 API 构建一个搜索应用程序。

"太棒了,”迈克对此兴奋不已。他已经非常喜欢在 React 上工作了。

"肖恩,对于这个项目,我们将探讨如何在服务器上使用 React 的选项。"

到目前为止,我们一直在页面加载后手动挂载我们的组件。直到组件被渲染,页面都没有任何来自组件的 HTML 内容。

"让我们看看我们如何在服务器上完成这项工作,以便在页面完全加载后预先生成 HTML。"

"明白了。那么,服务器端组件的渲染有什么好处呢?"

"这有几个有用的原因。其中一个原因是我们在服务器上生成内容。这对于 SEO 目的和更好的搜索引擎索引很有用。"

"由于内容是在服务器上生成的,第一次渲染将立即显示页面,而不是等待页面加载完全完成后才正确渲染组件。"

"这也帮助我们避免了页面加载时的闪烁效果。还有其他这样的优点我们可以利用,我们将在稍后探讨,”迈克解释道。

"好。那我们就开始吧。"

"好的!对于这个项目,让我们从一个入门 Webpack 项目开始,以管理我们的代码。对于服务器端元素,我们将使用 Express JS。我们在这里不会做任何复杂的事情,我们只是简单地从 Express JS 中暴露一个路由,并渲染一个包含我们的组件的.ejs视图。"

"这样的入门级项目示例可以在webpack.github.io/网站上找到,"迈克告知。

"很好,我想我们也会在客户端/服务器端划分代码?"

"是的。让我们将它们放在/app目录下以组成我们的组件,/client用于客户端特定的代码,/server用于我们/src目录中的服务器端代码,"迈克继续说道。

"接下来,我们将在/app/server目录中设置server.js文件。"

import path from 'path';
import Express from 'express';

var app = Express();
var server;

const PATH_STYLES = path.resolve(__dirname, '../client/styles');
const PATH_DIST = path.resolve(__dirname, '../../dist');

app.use('/styles', Express.static(PATH_STYLES));
app.use(Express.static(PATH_DIST));

app.get('/', (req, res) => {
  res.sendFile(path.resolve(__dirname, '../client/index.html'));
});

server = app.listen(process.env.PORT || 3000, () => {
  var port = server.address().port;

  console.log('Server is listening at %s', port);
});

"这是一个相当标准的 Express 应用程序设置。我们指定了要使用的样式,静态资源路径等等。"

"对于我们的路由,我们通过这样做来简单地暴露根/:"

app.get('/', (req, res) => {
  res.sendFile(path.resolve(__dirname, '../client/index.html'));
});

"我们要求 Express 在请求根目录时服务index.html文件。在我们的index.js文件中,我们将将其传递给 node 以运行应用程序,我们只需简单地暴露我们刚刚编写的服务器模块。"

require('babel/register');

module.exports = require('./server');

"迈克,为什么在这里需要babel/register?"

"哦,对了。在这里,我们引入 Babel(babeljs.io/)将我们的文件转换为浏览器兼容的格式。我们正在使用一些 JavaScript ES2015 语法的好处。Babel 通过语法转换器帮助我们添加对 JavaScript 最新版本的支持。这允许我们使用目前浏览器不支持的最新的 JavaScript 语法。"

"有了这个设置,我们将定义我们的index.html如下:"

<!DOCTYPE html>
<html>
<head lang="en">
  <meta charset="UTF-8">
  <title>Search</title>

  <link href="styles/main.css" rel="stylesheet" />

  <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
  <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap-theme.min.css">
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">

</head>
<body>
  <div id="app"></div>
  <script src="img/bundle.js"></script>
</body>
</html>

"这里没有什么特别的。我们只是在顶部定义一个 div,我们将在其上渲染 React 组件。"

"此外,请注意,我们已经包含了添加 Bootstrap 和 Font Awesome 支持到我们的应用程序的文件链接。"

"接下来,在客户端渲染处理方面,我们将做"

// file: scr/client/scripts/client.js
import App from '../../app';

var attachElement = document.getElementById('app');
var state = {};
var app;

// Create new app and attach to element
app = new App({ state: state});

app.renderToDOM(attachElement);

"最后,让我们在移动到我们的实际组件之前看看这里定义的App类是如何使用的。"

import React from 'react/addons';
import AppRoot from './components/AppRoot';

class App {
  constructor(options) {
    this.state = options.state;
 }

  render(element) {
    var appRootElement = React.createElement(AppRoot, {
      state: this.state
    });

    // render to DOM
    if (element) {
      React.render(appRootElement, element);
      return;
    }

    // render to string
    return React.renderToString(appRootElement);
  }

  renderToDOM(element) {
    if (!element) {
      new Error('App.renderToDOM: element is required');
    }

    this.render(element);
  }

  renderToString() {
    return this.render();
  }
}

export default App;

"哇,这需要消化很多东西,"肖恩叹了口气。

"哈哈!给它一些时间。我们在这里所做的只是处理我们的渲染逻辑。如果我们向这个类传递一个元素,内容将被渲染到它上面;否则,我们将返回渲染后的字符串版本。注意我们是如何使用React.renderToString来实现相同功能的。让我们先完成这个,然后我们将在使用它来在服务器请求上渲染内容时再次回顾它。"

"简而言之,我们只是要求 React 接收一个组件的状态,渲染它,并将render()方法将渲染的内容作为字符串返回。"

"然后我们将开始定义我们的根容器组件。"

require("jquery");
import React from 'react/addons';
import SearchPage from './SearchPage'

var AppRoot = React.createClass({
    propTypes: {
      state: React.PropTypes.object.isRequired
    },
    render()
    {
      return <SearchPage/>;
    }
  })
  ;

export default AppRoot;

"在这里,我们简单地定义一个容器来存放我们的主组件,并引入所有依赖。让我们接下来构建我们的搜索组件。"

"太棒了。我想我可以处理这个。看起来这只是一个简单的组件?"

"是的。继续吧,"迈克回应道。

"好的,我明白了我们需要从 Open Library API 端点获取数据。"

openlibrary.org/search.json?page=1&q=searchTerm

"在这里,q 查询参数将是搜索词。一个示例响应看起来像:"

{
 "start": 0,
 "num_found": 6,
 "numFound": 6,
 "docs": [
  {
   "title_suggest": "Automatic search term variant generation for document retrieval",
   "edition_key": [
    ..
   ],
…
   ],
   "author_name": [
..
..}]
}

"没错," Mike 补充说。

"我想我会从根据 startnum_founddocs 字段定义初始状态开始," Shawn 说

"好的。"

  getInitialState(){
    return {docs: [], numFound: 0, num_found: 0, start: 0, searchCompleted: false, searching: false}
  }

"我还添加了两个其他状态,我将保持它们:searchCompleted 以知道当前搜索操作是否已完成,以及 searching 以知道我们目前正在搜索某物。"

"酷。让我们看看下一个渲染方法," Mike 继续说。

"让我先在 render 方法中添加搜索框。"

render() {
    let tabStyles = {paddingTop: '5%'};
    return (
        <div className='container'>
          <div className="row" style={tabStyles}>
            <div className="col-lg-8 col-lg-offset-2">
              <div className="input-group">
                <input type="text" className="form-control" placeholder="Search for Projects..." ref='searchInput'/>
            <span className="input-group-btn">
              <button className="btn btn-default" type="button" onClick={this.performSearch}>Go!</button>
            </span>
              </div>
            </div>
          </div>
       </div>
    );
  },

"我们现在应该有一个搜索框的显示。"

在服务器上使 React 渲染

"接下来,我们将添加 performSearch 方法,它基于用户输入的搜索词启动搜索。"

  performSearch(){
    let searchTerm = $(this.refs.searchInput.getDOMNode()).val();
    this.openLibrarySearch(searchTerm);
    this.setState({searchCompleted: false, searching: true});
  },

"在这里,我们只是获取用户输入的搜索词,并将其传递给 openLibrarySearch 方法,它将实际执行搜索。然后,我们更新状态,表明我们现在正在积极执行搜索。"

"现在让我们完成搜索功能。"

 openLibrarySearch(searchTerm){
    let openlibraryURI = `https://openlibrary.org/search.json?page=1&q=${searchTerm}}`;
    fetch(openlibraryURI)
        .then(this.parseJSON)
        .then(this.updateState)
        .catch(function (ex) {
          console.log('Parsing failed', ex)
        })
  }

"啊,好,Shawn,你正在使用 fetch 而不是常规的 Ajax!"

"嗯,是的。我一直在使用 github.com/github/fetch 作为 window.fetch 规范的 polyfill。"

"不错,不是吗?它支持简单且干净的 API,如 Ajax,以及统一的获取 API。"

在获取某些资源或请求完成后,回调会通过 then 方法执行。注意,我们还在构建 URI 时使用了 ES2015 字符串字面量," Shawn 补充说。

"酷。看起来你正在获取资源,然后将其传递给 parseJSON 以解析并从响应体中返回 JSON 结果。然后,我们是否在它之上更新状态?"

"是的,让我定义那些"

  parseJSON(response) {
    return response.json();
  },

// response.json() is returning the JSON content from the response. 

updateState(json){
    this.setState({
      ...json,
      searchCompleted: true,
      searching: false
    });
  },

"在获取最终响应后,我正在更新和设置返回的结果状态,以及更新我们的 searchCompletedsearching 状态,以表明搜索工作已完成。"

"啊,好,Shawn,我看到你已经开始采用并使用 JS Next! 的新特性,比如 spread 操作符。"

"哈哈,是的。我已经爱上了这些。我正在使用这个来合并 JSON 结果的属性和我想添加的新键,并构建一个新的对象。这也会使用我们之前看到的 Object.assign 以类似的方式完成。"

Object.assign({}, json, {searchCompleted: true, searching: false} )

"这样,我们是在构建一个新的对象,而不是修改先前的对象。"

"Shawn 很棒,Mike 愉快地知道 Shawn 正在掌握新事物。"

"最后,让我添加加载动作显示,以显示加载器图标和实际结果的显示。现在渲染方法将看起来像这样。"

render() {
    let tabStyles = {paddingTop: '5%'};
    return (
        <div className='container'>
          <div className="row" style={tabStyles}>
            <div className="col-lg-8 col-lg-offset-2">
              <div className="input-group">
                <input type="text" className="form-control" placeholder="Search for Projects..." ref='searchInput'/>
            <span className="input-group-btn">
              <button className="btn btn-default" type="button" onClick={this.performSearch}>Go!</button>
            </span>
              </div>
            </div>
          </div>
          { (() => {
            if (this.state.searching) {
              return this.renderSearching();
            }
            return this.state.searchCompleted ? this.renderSearchElements() : <div/>
          })()}
        </div>
    );
  },

"在这里,我们正在检查搜索操作当前的状态。基于此,我们正在显示实际内容、结果或空 div 元素的加载器。"

"让我定义元素的加载和渲染。"

renderSearching(){
    return <div className="row">
      <div className="col-lg-8 col-lg-offset-2">
        <div className='text-center'><i className="fa fa-spinner fa-pulse fa-5x"></i></div>
      </div>
    </div>;
  },

"这将定义旋转按钮的显示,以指示正在加载。"

renderSearchElements(){
    return (

        <div className="row">
          <div className="col-lg-8 col-lg-offset-2">
            <span className='text-center'>Total Results: {this.state.numFound}</span>

            <table className="table table-stripped">
              <thead>
              <th>Title</th>
              <th>Title suggest</th>
              <th>Author</th>
              <th>Edition</th>
              </thead>
              <tbody>
              {this.renderDocs(this.state.docs)}
              </tbody>
            </table>

          </div>
        </div>

    );
  },

  renderDocs(docs){
    return docs.map((doc) => {
      console.log(doc);
      return <tr key={doc.cover_edition_key}>
        <td>{doc.title}</td>
        <td>{doc.title_suggest}</td>
        <td>{(doc.author_name || []).join(', ')}</td>
        <td>{doc.edition_count}</td>
      </tr>
    })
  },

"添加这个之后,搜索操作应该会显示一个类似这样的加载器。"

在服务器上使 React 渲染

显示的结果将如下所示:

在服务器上使 React 渲染

"完成的SearchPage组件如下:"

import React from 'react';
var SearchPage = React.createClass({
  getInitialState(){
    return {docs: [], numFound: 0, num_found: 0, start: 0, searchCompleted: false, searching: false}
  },
  render() {
    let tabStyles = {paddingTop: '5%'};
    return (
      <div className='container'>
        <div className="row" style={tabStyles}>
          <div className="col-lg-8 col-lg-offset-2">
            <div className="input-group">
              <input type="text" className="form-control" placeholder="Search for Projects..." ref='searchInput'/>
            <span className="input-group-btn">
              <button className="btn btn-default" type="button" onClick={this.performSearch}>Go!</button>
            </span>
            </div>
          </div>
        </div>
        { (() => {
          if (this.state.searching) {
            return this.renderSearching();
          }
          return this.state.searchCompleted ? this.renderSearchElements() : <div/>
        })()}
      </div>
    );
  },
  renderSearching(){
    return <div className="row">
      <div className="col-lg-8 col-lg-offset-2">
        <div className='text-center'><i className="fa fa-spinner fa-pulse fa-5x"></i></div>
      </div>
    </div>;
  },
  renderSearchElements(){
    return (
      <div className="row">
        <div className="col-lg-8 col-lg-offset-2">
          <span className='text-center'>Total Results: {this.state.numFound}</span>
          <table className="table table-stripped">
            <thead>
            <th>Title</th>
            <th>Title suggest</th>
            <th>Author</th>
            <th>Edition</th>
            </thead>
            <tbody>
            {this.renderDocs(this.state.docs)}
            </tbody>
          </table>
        </div>
      </div>
    );
  },
  renderDocs(docs){
    return docs.map((doc) => {
      console.log(doc);
      return <tr key={doc.cover_edition_key}>
        <td>{doc.title}</td>
        <td>{doc.title_suggest}</td>
        <td>{(doc.author_name || []).join(', ')}</td>
        <td>{doc.edition_count}</td>
      </tr>
    })
  },

  performSearch(){
    let searchTerm = $(this.refs.searchInput.getDOMNode()).val();
    this.openLibrarySearch(searchTerm);
    this.setState({searchCompleted: false, searching: true});
  },

  parseJSON(response) {   return response.json();  },

  updateState(json){
    this.setState({
      ...json,
      searchCompleted: true,
      searching: false
    });
  },
  openLibrarySearch(searchTerm){
    let openlibraryURI = `https://openlibrary.org/search.json?page=1&q=${searchTerm}}`;
    fetch(openlibraryURI)
      .then(this.parseJSON)
      .then(this.updateState)
      .catch(function (ex) {
        console.log('Parsing failed', ex)
      })
  }
});
module.exports = SearchPage;

"如果你注意到,我使用立即调用的函数添加了一个if语句来显示搜索图标渲染,如下所示:"

          { (() => {
            if (this.state.searching) {
              return this.renderSearching();
            }
            return this.state.searchCompleted ? this.renderSearchElements() : <div/>
          })()}

"在这里,我们使用了()=>{}语法首先定义函数,然后立即调用它(()=>{}))(),返回我们在渲染期间需要显示的内容。"

"干得好,肖恩!" 迈克对肖恩取得的进展感到高兴。

"这很方便,当我们想在渲染本身中添加简单的逻辑开关时,而不是定义新的方法,"迈克继续说。

在服务器上

"现在肖恩,让我们在服务器上预渲染组件。这意味着从 React 组件创建一个 HTML 元素,并在第一次页面加载时本身渲染其内容。目前,元素的加载由客户端代码处理。"

app.renderToDOM(attachElement);

"而不是这样,我们将在 Express 动作本身中渲染 React 元素。"

"首先,让我们设置一个.ejs视图来显示我们的 HTML 内容以及动态生成的 React 内容。"

<!DOCTYPE html>
<html>
<head lang="en">
  <meta charset="UTF-8">
  <title>Search</title>

  <link href="styles/main.css" rel="stylesheet" />
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap-theme.min.css">
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">
</head>
<body>
  <div id="app">
  <%- reactOutput %>
  </div>
  <script src="img/bundle.js"></script>
</body>
</html>

"在这里,我们将reactOutput作为变量传递给视图进行渲染。"

"我们现在将修改server.js文件,以包含所需的组件和 React 进行渲染。"

import AppRoot from '../app/components/AppRoot'
import React from 'react/addons';

"我们的动作将变为:"

app.get('/', (req, res) => {
  var reactAppContent = React.renderToString(<AppRoot state={{} }/>);
  console.log(reactAppContent);
  res.render(path.resolve(__dirname, '../client/index.ejs'), {reactOutput: reactAppContent});
});

"我们的最终服务器代码将如下所示。"

import path from 'path';
import Express from 'express';

import AppRoot from '../app/components/AppRoot'
import React from 'react/addons';

var app = Express();
var server;

const PATH_STYLES = path.resolve(__dirname, '../client/styles');
const PATH_DIST = path.resolve(__dirname, '../../dist');

app.use('/styles', Express.static(PATH_STYLES));
app.use(Express.static(PATH_DIST));

app.get('/', (req, res) => {
  var reactAppContent = React.renderToString(<AppRoot state={{} }/>);
  console.log(reactAppContent);
  res.render(path.resolve(__dirname, '../client/index.ejs'), {reactOutput: reactAppContent});
});

server = app.listen(process.env.PORT || 3000, () => {
  var port = server.address().port;

  console.log('Server is listening at %s', port);
});

"这里就是了!我们正在使用 React 的renderToString方法来渲染一个组件,如果需要,可以传递任何状态,以伴随它。"

摘要

在本章中,我们探讨了如何使用Express.js的帮助,将服务器端渲染与 React 结合使用。我们从一个客户端 React 组件开始,最后使用 React API 提供的方法将其替换为服务器端渲染。

在下一章中,我们将探讨 React 插件,用于执行双向绑定、类名操作、组件克隆、不可变辅助工具和 PureRenderMixin,同时继续在本章中构建的搜索项目。

第七章。React 插件

在上一章中,我们学习了如何在服务器端使用 React。我们了解了在服务器端使用 React 时 React 组件的预渲染以及组件生命周期的变化。我们还看到了如何使用 Express.js 调用 React 的服务器端 API。

在本章中,我们将探讨 React 插件——这些不是 React 核心库的一部分的实用程序包,但它们使开发过程变得有趣和愉快。我们将学习使用不可变辅助工具、组件克隆和测试实用工具。我们不会涵盖其他插件,如AnimationPerfPureRenderMixin。这些插件将在下一章中介绍。

在本章中,我们将涵盖以下主题:

  • 开始学习 React 插件

  • 不可变辅助工具

  • 克隆 React 组件

  • 测试辅助工具

开始学习插件

在完成关于在服务器端使用 React 的上一项目之后,迈克的团队在开始下一个项目之前有一些空闲时间。迈克决定利用这段时间来学习 React 插件。

"肖恩,我们有了一些空闲时间。让我们利用这段时间开始学习 React 插件。"

"什么是 React 插件?它们与 React 核心库有关吗?"肖恩问道。

"React 插件是 React 核心库之外的实用模块。然而,它们得到了 React 团队的认可。在未来,其中一些可能会被包含在 React 核心中。这些库提供了编写不可变代码的辅助工具、测试 React 应用的实用工具以及衡量和改进 React 应用性能的方法。"迈克解释道。

"每个插件都有自己的 npm 包,这使得使用变得非常简单。例如,要使用 Update 插件,我们需要安装并引入其 npm 包。"

 $ npm install  react-addons-update --save

 // src/App.js
 import Update from 'react-addons-update';

不可变辅助工具

"肖恩,随着我们学习插件,让我们给我们的应用添加排序功能,这样用户就可以按标题排序书籍。我已经添加了所需的标记。"

不可变辅助工具

"你能尝试编写当用户点击标题时进行排序的代码吗?"马克问道。

"这就是它。我引入了排序状态来指示排序方向——升序或降序。"

  // Updated getInitialState function of App component
  // src/App.js

  getInitialState(){
    return { books: [],
             totalBooks: 0,
             searchCompleted: false,
             searching: false,
             sorting: 'asc' };
  }

"当用户点击标题时,它将使用 sort-by npm 包按升序或降序对现有状态中存储的书籍进行排序,并使用排序后的书籍更新状态。"

import sortBy from 'sort-by';

_sortByTitle() {
    let sortByAttribute = this.state.sorting === 'asc' ? "title" : "-title";
    let unsortedBooks = this.state.books;
    let sortedBooks = unsortedBooks.sort(sortBy(sortByAttribute));
    this.setState({ books: sortedBooks, 
                    sorting: this._toggleSorting() });
  },

  _toggleSorting() {
    return this.state.sorting === 'asc' ? 'desc' : 'asc';
  }

"肖恩,这是功能性的;然而,它并不遵循 React 的方式。React 假设状态对象是不可变的。当我们为unsortedBooks赋值时,我们正在使用现有状态中对书籍的引用。"

let unsortedBooks = this.state.books;

"稍后,我们将unsortedBooks转换为sortedBooks;然而,作为副作用,我们也在修改this.state的当前值。"

_sortByTitle() {
    let sortByAttribute = this.state.sorting === 'asc' ? "title" : "-title";
    let unsortedBooks = this.state.books;
    console.log("Before sorting :");
    console.log(this.state.books[0].title);
    let sortedBooks = unsortedBooks.sort(sortBy(sortByAttribute));
    console.log("After sorting :");
    console.log(this.state.books[0].title);
    // this.setState({ books: sortedBooks, 
                       sorting: this._toggleSorting() });

  },

不可变辅助工具

"正如你所见,即使我们注释掉了对 this.setState的调用,我们的当前状态仍然被修改了。"马克解释道。

"这可以通过使用 ES6 中的 Object.assign 轻易地修复。我们可以简单地创建一个新的数组,并将 this.state.books 的当前值复制到其中。然后我们可以对新的数组进行排序,并使用新的排序数组调用 setState。" 肖恩告知。

注意

Object.assign 方法将多个源对象的所有可枚举属性值复制到目标对象。更多详细信息可以在以下链接找到:developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign

_sortByTitle() {
    let sortByAttribute = this.state.sorting === 'asc' ? "title" : "-title";
    let unsortedBooks = Object.assign([], this.state.books);
    console.log("Before sorting :");
    console.log(this.state.books[0].title);
    let sortedBooks = unsortedBooks.sort(sortBy(sortByAttribute));
    console.log("After sorting :");
    console.log(this.state.books[0].title);
    this.setState({ books: sortedBooks, 
                    sorting: this._toggleSorting() });
  }

不可变辅助工具

"是的。这行得通。但是 Object.assign 方法将创建 this.state.books 的浅拷贝。它将创建一个新的 unsortedBooks 对象,然而,它仍然会在 unsortedBooks 中使用 this.state.books 的相同引用。假设,由于某种原因,我们想要将所有书籍的标题转换为大写字母,那么我们可能会意外地变异 this.state," 迈克解释道。

_sortByTitle() {
    let sortByAttribute = this.state.sorting === 'asc' ? "title" : "-title";
    let unsortedBooks = Object.assign([], this.state.books);
    unsortedBooks.map((book) => book.title = book.title.toUpperCase());
    console.log("unsortedBooks");
    console.log(unsortedBooks[0].title);
    console.log("this.state.books");
    console.log(this.state.books[0].title); 
  }

不可变辅助工具

"正如您所看到的,即使使用了 Object.assignthis.state.books 仍然发生了变异。实际上,这与 React 本身无关。这是由于 JavaScript 传递数组和对象引用的方式造成的。然而,由于这个原因,如果我们深层次的状态中有数组和对象,就很难防止变异。" 迈克进一步解释道。

"我们是否总是必须执行深拷贝状态对象以确保安全?" 肖恩问道。

"嗯,深拷贝可能很昂贵,有时在深层次嵌套的状态中很难实现。幸运的是,React 提供了带有不可变辅助工具的 Update 插件,我们可以使用它来解决这个问题的。" 迈克补充道。

"在使用不可变辅助工具时,我们需要回答以下三个问题:"

  • 需要更改什么?

  • 需要在哪里更改?

  • 需要如何更改?

"在这种情况下,我们需要更改 this.state 以显示排序后的书籍。"

"第二个问题是,this.state 内部应该在哪个位置发生变异?变异应该发生在 this.state.books 上。"

"第三个问题是变异应该如何发生?我们是打算删除某些内容,还是添加新的元素,或者重构现有元素?在这种情况下,我们想要根据某些标准对元素进行排序。"

"Update 插件接受两个参数。第一个参数是我们想要变异的对象。第二个参数告诉我们第一个参数中变异应该发生在哪里以及如何进行。在这种情况下,我们想要变异 this.state。在 this.state 中,我们想要用排序后的书籍更新书籍。因此,我们的代码将类似于以下内容:"

Update(this.state, { books: { sortedBooks }})

可用命令

Update 插件提供了不同的命令来执行数组和对象的变异。这些命令的语法受到了 MongoDB 查询语言的启发。

"大多数这些命令都操作数组对象,允许我们向数组中推入一个数组或从数组中 unshift 一个元素。它还支持使用 set 命令完全替换目标。除此之外,它还提供了 merge 命令来合并新键与现有对象。" 迈克解释道。

"肖恩,这个插件提供的我最喜欢的命令是 apply。它接受一个函数,并将该函数应用于我们想要修改的目标的当前值。它为在复杂数据结构中做出更改提供了更多的灵活性。这也是你下一个任务的提示。尝试使用它来按标题对书籍进行排序,而不进行修改。" 迈克提出了挑战。

"当然。给你," 肖恩说。

    // src/App.js

import Update from 'react-addons-update';

_sortByTitle() {
  let sortByAttribute = this.state.sorting === 'asc' ? "title" : "-title";
  console.log("Before sorting");
  console.log(this.state.books[0].title);
  let newState = Update(this.state,
                        { books: { $apply: (books) => { return books.sort(sortBy(sortByAttribute)) } },
                          sorting: { $apply: (sorting) => { return sorting === 'asc' ? 'desc' : 'asc' } } });
  console.log("After sorting");
  console.log(this.state.books[0].title);
  this.setState(newState);
  }

可用命令

"太好了,肖恩。使用 Update 插件使得管理复杂状态变得非常容易,而实际上并没有修改它。"

小贴士

查阅facebook.github.io/immutable-js/docs/以获取在 JavaScript 中使用不可变数据结构的完整解决方案。

克隆组件

"肖恩,在 React 中,props 也是不可变的。在大多数情况下,子组件只是使用父组件传递的 props。然而,有时我们在渲染子组件之前想要扩展传入的 props 以添加一些新数据。这是一个典型的用例,用于更新样式和 CSS 类。React 提供了一个插件来克隆组件并扩展其 props。它被称为cloneWithProps插件。" 迈克说。

"迈克,这个插件已经过时了。我以前看过它,React 已经将其弃用。它还与子组件的 refs 未传递给新克隆的子组件等问题有关。" 肖恩通知说。

"确实如此。然而,React 还有一个顶级的React.cloneElement API 方法,它允许我们克隆并扩展一个组件。它有一个非常简单的 API,并且可以用作 cloneWithProps 插件的替代品。" 迈克解释道。

React.cloneElement(element, props, …children);

"这个方法可以用来克隆给定的元素,并将新属性与现有属性合并。它用新子元素替换现有子元素。因此,在处理子组件时,我们必须记住这一点。"

"cloneElement函数还会将旧子组件的 ref 传递给新克隆的组件。因此,如果我们有任何基于 refs 的回调,它们在克隆后仍然会继续工作。"

"肖恩,你的下一个挑战来了。我们在我们的应用中显示书籍列表。实际上,在我们的所有应用中,我们都显示其他事物的列表,如产品、项目等。我们希望在所有这些列表中显示带有交替颜色的行,而不是只有白色背景。因为这个功能的代码将在所有应用中都是相同的,所以我正在考虑创建一个单独的组件,该组件将接受行作为输入,并以交替颜色渲染它们。这样的组件可以用于我们的所有应用。我认为你可以使用React.cloneElement来完成这个任务。" 迈克解释了下一个任务。

"将这个功能提取为一个单独的组件听起来是个好主意。我们几乎在所有应用中都需要它。昨天我们的 QA 团队抱怨我们的搜索应用缺少颜色。" 肖恩回忆道。

"让我们添加一些交替颜色吧。" 迈克轻声笑了。

"首先,让我们看看我们目前是如何显示书籍的。"

// src/App.js

render() {
    let tabStyles = {paddingTop: '5%'};
    return (
      <div className='container'>
        <div className="row" style={tabStyles}>
          <div className="col-lg-8 col-lg-offset-2">
            <h4>Open Library | Search any book you want!</h4>
            <div className="input-group">
              <input type="text" className="form-control" placeholder="Search books..." ref='searchInput'/>
              <span className="input-group-btn">
                <button className="btn btn-default" type="button" onClick={this._performSearch}>Go!</button>
              </span>
            </div>
          </div>
        </div>
        {this._displaySearchResults()}
      </div>
    );
  },

_displaySearchResults() {
    if(this.state.searching) {
      return <Spinner />;
    } else if(this.state.searchCompleted) {
      return (
        <BookList
            searchCount={this.state.totalBooks}
            _sortByTitle={this._sortByTitle}>
          {this._renderBooks()}
        </BookList>
      );
    }
  } 

_renderBooks() {
    return this.state.books.map((book, idx) => {
      return (
        <BookRow key={idx}
                 title={book.title}
                 author_name={book.author_name}
                 edition_count={book.edition_count} />
      );
    })
  },

})
  }

"BookList 组件只是按照原样渲染传递给它的行,因为它使用了 this.props.children。"

// BookList component

var BookList = React.createClass({
  render() {
    return (
      <div className="row">
        <div className="col-lg-8 col-lg-offset-2">
          <span className='text-center'>
            Total Results: {this.props.searchCount}
          </span>
          <table className="table table-stripped">
            <thead>
              <tr>
                <th><a href="#" onClick={this.props._sortByTitle}>Title</a></th>
                <th>Author</th>
                <th>No. of Editions</th>
              </tr>
            </thead>
            <tbody>
              {this.props.children}
            </tbody>
          </table>
        </div>
      </div>
    );
  }
});

"迈克,我打算将这个组件命名为 RowAlternatorRowAlternator 组件将获取动态的子行数组,并以交替颜色渲染它们。我们也可以向 RowAlternator 传递多个颜色。这样,使用这个组件的客户端代码就可以控制它们想要使用的交替颜色。"

"听起来不错,肖恩。我认为现在这个 API 已经足够了。"

// RowAlternator component

import React from 'react';

var RowAlternator = React.createClass({
  propTypes: {
    firstColor: React.PropTypes.string,
    secondColor: React.PropTypes.string
  },

  render() {
    return (
      <tbody>
        { this.props.children.map((row, idx) => {
            if (idx %2 == 0) {
              return React.cloneElement(row, { style: { background: this.props.firstColor }});
            } else {
              return React.cloneElement(row, { style: { background: this.props.secondColor }});
            }
          })
        }
      </tbody>
    )
  }
});

module.exports = RowAlternator;

"由于我们不知道在 RowAlternator 中我们会得到多少子元素,所以我们只是遍历所有元素并使用交替颜色设置样式。我们在这里也使用了 React.cloneElement 来克隆传递的子元素,并使用适当的背景颜色扩展其样式属性。"

"现在让我们更改 BookList 组件,以便使用 RowAlternator。"

// BookList component

import RowAlternator from '../src/RowAlternator';

var BookList = React.createClass({
  render() {
    return (
      <div className="row">
        <div className="col-lg-8 col-lg-offset-2">
          <span className='text-center'>
            Total Results: {this.props.searchCount}
          </span>
          <table className="table table-stripped">
            <thead>
              <tr>
                <th><a href="#" onClick={this.props._sortByTitle}>Title</a></th>
                <th>Author</th>
                <th>No. of Editions</th>
              </tr>
            </thead>
            <RowAlternator firstColor="white" secondColor="lightgrey">
              {this.props.children}
            </RowAlternator>
          </table>
        </div>
      </div>
    );
  }
});

"我们已经准备好了。现在列表显示了我们所想要的交替颜色,如下面的图片所示:"

克隆组件

"太好了,肖恩。正如你已经注意到的,当我们构建一个具有动态子元素的组件时,使用 React.cloneElement 是有意义的,在这些子元素的渲染方法上我们没有控制权,但基于某些标准想要扩展它们的属性。" 迈克很高兴。

测试 React 应用程序的帮助器

"肖恩,我们还没有为我们的应用添加任何测试,但是现在是时候开始慢慢添加测试覆盖率了。有了 Jest 库和测试实用工具插件,设置和开始测试 React 应用程序变得非常简单。" 马克解释了下一个任务。

"我听说过 Jest。它不是 Facebook 的一个测试库吗?" 肖恩问道。

设置 Jest

"是的。它是建立在 Jasmine 之上的。设置起来非常简单。首先,我们必须安装 jest-clibabel-jest 包。"

npm install jest-cli --save-dev
npm install babel-jest –-save-dev

"之后,我们需要配置 package.json,如下所示:"

{
 ...
 "scripts": {
   "test": "jest"
 },

 "jest": {
   "scriptPreprocessor": "<rootDir>/node_modules/babel-jest",
    "unmockedModulePathPatterns": [
         "<rootDir>/node_modules/react",
         "<rootDir>/node_modules/react-dom",
         "<rootDir>/node_modules/react-addons-test-utils",
         "<rootDir>/node_modules/fbjs"
     ],
   "testFileExtensions": ["es6", "js", "jsx"],
   "moduleFileExtensions": ["js", "json", "es6"]
 }
 ...
}

"默认情况下,Jest 模拟所有模块,但是在这里我们告诉 Jest 不要模拟 React 和相关库。我们还指定了 Jest 将识别为测试文件的测试文件扩展名。"

"创建一个 __test__ 文件夹,我们将在这里添加我们的测试。Jest 将从这个文件夹中的文件运行测试。让我们也添加一个空文件。我们必须确保文件以 -test.js 结尾,这样 Jest 才能将其识别为测试文件。" 迈克解释道。

mkdir __test__
touch __test__/app-test.js

"现在让我们验证我们是否可以从命令行运行测试。"

$ npm test

> react-addons-examples@0.0.1 test /Users/prathamesh/Projects/sources/reactjs-by-example/chapter7
> jest

Using Jest CLI v0.7.1
PASS __tests__/app-test.js (0.007s)

备注

你应该看到一个与前面输出类似的输出。它可能会根据 Jest 版本而变化。如有任何问题,请咨询facebook.github.io/jest/docs/getting-started.html以设置 Jest。

"肖恩,我们已经准备好使用 Jest 了。现在是时候开始编写测试了。我们将测试顶层App组件是否正确挂载。然而,首先,我们需要更多地了解 Jest 的使用方法,"迈克说。

"默认情况下,Jest 模拟了测试文件中需要的所有模块。Jest 这样做是为了隔离正在测试的模块与其他所有模块。在这种情况下,我们想测试App组件。如果我们只是导入它,那么 Jest 将提供App的模拟版本。"

// app-test.js

   const App = require('App'); // Mocked by Jest

"因为我们想测试App组件本身,所以我们需要指定 Jest 不要模拟它。Jest 提供了jest.dontmock()函数来实现这个目的。"

// app-test.js

jest.dontmock('./../src/App'); // Tells Jest not to mock App.
const App = require('App');

注意

查阅facebook.github.io/jest/docs/automatic-mocking.html以获取 Jest 自动模拟功能的更多详细信息。

"接下来,我们将添加 React 和 TestUtils 插件的导入语句。"

// app-test.js

jest.dontMock('../src/App');
const App = require('../src/App');

import React from 'react';
import ReactDOM from 'react-dom';
import TestUtils from 'react-addons-test-utils';

"TestUtils 插件提供了渲染组件、在渲染组件中查找子组件以及模拟渲染组件上的事件的实用函数。它有助于测试 React 组件的结构和行为。"迈克补充道。

测试 React 组件的结构

"我们将从renderIntoDocument函数开始。这个函数将给定的组件渲染到文档中的一个分离的 DOM 节点中。它返回ReactComponent,可以用于进一步的测试。"

// app-test.js

describe('App', () => {
  it('mounts successfully', () => {
    let app = TestUtils.renderIntoDocument(<App />);
    expect(app.state.books).toEqual([]);
    expect(app.state.searching).toEqual(false);
  })
});

"我们在 DOM 中渲染了App组件,并断言初始书籍和搜索状态被正确设置。"迈克解释道。

"迈克,这太棒了。我们不是在测试真实的 DOM,而是在测试 React 组件。"

"是的。TestUtils 插件附带了一些查找方法,可以用来在给定的组件树中查找子组件。它们对于查找子组件,如输入框和提交按钮,并模拟点击事件或更改事件很有用。"

  • findAllInRenderedTree(tree, predicate function):这对于在给定的树中找到返回谓词函数真值的所有组件很有用。

  • scryRenderedDOMComponentsWithClass(tree, className):这对于找到具有给定类名的所有 DOM 组件很有用。

  • findRenderedDOMComponentWithClass(tree, className):与找到具有给定类的所有 DOM 组件不同,此方法期望只有一个这样的组件存在。如果有多个具有给定类名的组件,它将抛出异常。

  • scryRenderedDOMComponentsWithTag(tree, tagName):它与根据类名查找 DOM 组件类似,但是,它基于给定的标签名来查找组件。

  • findRenderedDOMComponentWithTag(tree, tagName): 与查找具有给定标签的所有组件不同,它期望只有一个这样的组件存在。如果存在多个此类组件,它也会抛出异常。

  • scryRenderedComponentsWithType(tree, componentType): 此方法查找所有具有给定类型的组件。这对于查找用户创建的所有复合组件非常有用。

  • findRenderedComponentWithType (tree, componentType): 这与所有之前的查找方法类似。如果存在具有给定类型的多个组件,它将引发异常。

测试 React 组件的行为

"让我们使用这些函数来断言,当用户输入搜索词并点击提交按钮时,搜索书籍开始。我们将通过模拟用户输入搜索词的事件来实现这一点。"迈克说。

// app-test.js

it('starts searching when user enters search term and clicks submit', () => {
    let app = TestUtils.renderIntoDocument(<App />);

    let inputNode = TestUtils.findRenderedDOMComponentWithTag(app, 'input');
    inputNode.value = "Dan Brown";
    TestUtils.Simulate.change(inputNode);
    let submitButton = TestUtils.findRenderedDOMComponentWithTag(app, 'button');
    TestUtils.Simulate.click(submitButton);
    expect(app.state.searching).toEqual(true);
    expect(app.state.searchCompleted).toEqual(false);
  })

"我们渲染App组件,找到输入节点,将输入节点的值设置为搜索词,并使用TestUtils.Simulate()模拟更改事件。这个函数模拟在 DOM 节点上给定事件的派发。Simulate函数为 React 理解的所有事件都有一个方法。因此,我们可以模拟所有事件,如更改、点击等。我们可以使用这种方法来测试用户行为。"迈克解释道。

"明白了。因此,在更改搜索词后,我们点击提交按钮并验证状态是否按预期更新。"肖恩说道。

"是的,肖恩。现在,你能检查一下用户点击提交按钮后是否显示 Spinner 吗?你可以使用我们之前讨论过的查找方法之一。"迈克解释了下一个任务。

"是的。在点击提交按钮后,组件状态发生变化后,我们可以搜索渲染的组件树,以查看 Spinner 组件是否存在。"。

// app-test.js

// __tests__/app-test.js

import Spinner from './../src/Spinner';

it('starts searching when user enters search term and clicks submit', () => {
    let app = TestUtils.renderIntoDocument(<App />);
    let inputNode = TestUtils.findRenderedDOMComponentWithTag(app, 'input');
    inputNode.value = "Dan Brown";
    TestUtils.Simulate.change(inputNode);
    let submitButton = TestUtils.findRenderedDOMComponentWithTag(app, 'button');
    TestUtils.Simulate.click(submitButton);
    expect(app.state.searching).toEqual(true);
    expect(app.state.searchCompleted).toEqual(false);
 let spinner = TestUtils.findRenderedComponentWithType(app, Spinner);
 expect(spinner).toBeTruthy();
  }),

"我们在这里使用TestUtils.findRenderedComponentWithType来检查 Spinner 是否存在于由App组件渲染的树中。然而,在添加此断言之前,我们需要在测试文件顶部导入 Spinner 组件,因为findRenderedComponentWithType期望第二个参数是一个 React 组件。"。

"非常好,肖恩。正如你所看到的,使用TestUtils.Simulatefinder方法,React 组件的测试行为变得非常简单。"迈克解释道。

注意

注意,我们没有添加测试来异步从 Open Library API 加载书籍,因为这超出了本章的范围。

浅渲染

“肖恩,TestUtils 还提供了一种使用浅渲染以隔离方式测试组件的方法。浅渲染允许我们渲染顶级组件并断言其渲染方法的返回值。它不会渲染子组件或实例化它们。因此,我们的测试不受子组件行为的影响。与之前的方法不同,浅渲染不需要 DOM,”迈克解释道。

let renderer = TestUtils.createRenderer();

“这创建了一个浅渲染器对象,我们将在这个对象中渲染我们想要测试的组件。浅渲染器有一个类似于ReactDOM.renderrender方法,可以用来渲染组件。”

let renderer = TestUtils.createRenderer();
let result = renderer.render(<App />);

“在调用渲染方法之后,我们应该调用renderer.getRenderedOutput,它返回渲染组件的浅渲染输出。我们可以在getRenderedOutput的输出上开始断言关于组件的事实。”

“让我们看看从getRenderedOutput得到的输出。”

let renderer = TestUtils.createRenderer();
let result = renderer.render(<App />);
result = renderer.getRenderOutput();
console.log(result);

// Output of console.log(result)

Object {
  '$$typeof': Symbol(react.element),
  type: 'div',
  key: null,
  ref: null,
  props: 
   Object {
     className: 'container',
     children: Array [ [Object], undefined ] },
  _owner: null,
  _store: Object {} }

“正如你所见,基于渲染输出,我们可以断言关于当前组件 props 的事实。然而,如果我们想测试关于子组件的任何事情,我们需要通过this.props.children[0].props.children[1].props.children显式地访问它们。”

“这使得使用浅渲染测试子组件的行为变得困难。然而,由于浅渲染不受子组件的影响,它对于以隔离方式测试小型组件是有用的,”迈克说。

摘要

在本章中,我们首先了解了 React 插件及其使用方法。我们使用了插件提供的不可变辅助工具和测试实用函数。我们还探讨了如何克隆组件。

在下一章中,我们将使我们的 React 应用更加高效。你将学习到可以提升 React 应用性能的插件。具体来说,你将学习如何测量我们应用的性能以及 React 如何在不改变大部分 UI 的情况下进行更快的更新。

让我们的 React 应用更快吧!

第八章. React 应用的性能

在上一章中,我们学习了如何使用各种 React 插件。我们看到了从不可变辅助工具到测试工具的各种插件。

在本章中,我们将探讨可以提高我们 React 应用性能的 React 性能工具。特别是,我们将使用 PERF 插件、PureRenderMixinshouldComponentUpdate。我们还将探讨在使用 React 提供的性能工具时需要考虑的一些陷阱。

React 应用的性能

"嗨,迈克,我今天有几个问题想问你。周末我一直在思考我们的搜索应用。你有时间讨论一下吗?"肖恩问道。

"当然,但让我先喝点咖啡。好的,我现在准备好了。开始吧!"迈克说。

"我对 React 应用的性能有几个问题。我知道 React 在状态变化时重新渲染组件树方面做得非常好。React 使我能很容易地理解和推理我的代码。然而,这不会影响性能吗?重新渲染看起来是一个非常昂贵的操作,尤其是在重新渲染大型组件树时。"肖恩问道。

"肖恩,重新渲染可能会很昂贵。然而,React 在这方面很聪明。它只渲染发生变化的部分。它不需要重新渲染页面上的一切。它也在尽量减少 DOM 操作方面很聪明。"

"这是怎么可能的?它是怎么知道页面哪一部分发生了变化?它不是依赖于用户交互或传入的状态和属性吗?"肖恩质疑道。

"虚拟 DOM 在这里发挥了作用。它完成了跟踪变化和帮助 React 对真实 DOM 进行最小更改的所有繁重工作。"迈克解释道。

虚拟 DOM

"肖恩,React 使用虚拟 DOM 来跟踪真实 DOM 中的变化。它的概念非常容易理解。React 始终在内存中保留实际 DOM 表示的副本。每当某些状态操作发生变化时,它都会计算一个新的 DOM 副本,该副本将使用新的状态和属性生成。然后它计算原始虚拟 DOM 副本和新虚拟 DOM 副本之间的差异。这个差异导致对真实 DOM 进行最小操作,可以将当前 DOM 带到新的阶段。这样,React 在发生变化时不需要进行重大更改。"迈克解释道。

"但是,差异计算不是很昂贵吗?"肖恩问道。

"与实际的 DOM 操作相比,它并不昂贵。DOM 操作总是昂贵的。虚拟 DOM 的比较发生在 JavaScript 代码中,所以它总是比手动 DOM 操作快。"迈克说。

"这种方法的优势之一是,一旦 React 知道需要在 DOM 上执行哪些操作,它就会一次性完成。因此,当我们渲染 100 个元素的列表时,而不是逐个添加元素,React 将执行最少的 DOM 操作来在页面上插入这 100 个元素。"迈克解释道。

"我印象深刻!"肖恩惊呼。

"你们会更加印象深刻。让我实际展示一下我的意思。让我们使用来自 React 的 PERF 插件,并实际看到我们讨论的内容。"

The PERF 插件

"让我们从安装 PERF 插件开始。"

$ npm install react-addons-perf --save-dev

"我们只需要在开发模式下使用这个插件。这是一个需要记住的重要点,因为在生产中,我们不需要调试信息,因为它可能会使我们的应用程序变慢。"迈克告知。

"肖恩,PERF 插件可以用来查看 React 对 DOM 做了哪些更改,它在渲染我们的应用程序时在哪里花费时间,它是否在渲染过程中浪费了一些时间,等等。然后,我们可以使用这些信息来提高应用程序的性能。"迈克说。

"让我们首先将 PERF 插件暴露为一个全局对象。当我们的应用程序运行时,我们可以使用它在浏览器控制台中查看 React 根据用户交互所做的更改。"迈克解释道。

// index.js
import ReactDOM from 'react-dom';
import React from 'react';
import App from './App';
import Perf from 'react-addons-perf';

window.Perf = Perf;

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

"我们已经将 PERF 插件导入到我们的 index.js 文件中,这是应用程序的起点。我们可以在浏览器控制台中访问 Perf 对象,因为我们已经将其附加到 window.Perf。"迈克补充道。

The PERF 插件

"PERF 插件附带了一些方法,可以帮助我们了解当某些内容发生变化时 React 如何处理 DOM。让我们测量一些性能统计数据。我们将通过在浏览器控制台中调用 Perf.start() 来开始测量过程。之后,我们将与应用程序进行交互。我们将输入一个查询以搜索一本书,点击提交,搜索结果将显示出来。我们将通过在浏览器控制台中调用 Perf.stop() 来停止性能测量。之后,让我们分析我们收集到的信息。"迈克解释了整个过程。

The PERF 插件

"让我们搜索丹·布朗写的书籍。"

The PERF 插件

"一旦结果显示出来,我们就停止性能测量。"

The PERF 插件

React 执行的 DOM 操作

"肖恩,PERF 插件可以显示 React 执行了哪些 DOM 操作。让我们看看 React 对丹·布朗的书籍列表进行了哪些操作。"迈克说。

React 执行的 DOM 操作

"Perf.printDOM() 方法告诉我们 React 执行的 DOM 操作。它只做了两次设置 innerHTML 调用。第一次是渲染加载指示器,第二次是渲染行列表。在这之间,我们看到一个移除调用,这应该是加载指示器从页面上移除的时候。"

"哇,这个方法看起来非常实用,因为它可以告诉我们 React 是否在某种程度上做了额外的 DOM 操作。" 肖恩说。

"是的,但还有更多工具可以用来分析性能。让我们看看 React 渲染每个组件需要多少时间。这可以通过使用Perf.printInclusive()来实现。" 迈克解释道。

渲染所有组件所需的时间

渲染所有组件所需的时间

"这个方法打印了渲染所有组件所需的总时间。这还包括处理属性、设置初始状态以及调用componentDidMountcomponentWillMount所需的时间。"

"因此,如果我们在这其中的某个钩子中有一些耗时操作,它将影响printInclusive函数显示的输出,对吧?" 肖恩问道。

"正是如此。尽管 PERF 插件提供了另一种方法——printExclusive()——它可以在不使用这些钩子的情况下打印渲染所需的时间,这些钩子用于安装应用程序。"

渲染所有组件所需的时间

"但是迈克,这些方法对于检测 React 的性能并不那么有帮助。我了解了所有发生的事情的总体情况,但它并没有告诉我如何优化哪个部分。" 肖恩问道。

React 浪费的时间

"肖恩,PERF 插件还可以告诉我们 React 浪费了多少时间以及在哪里。这有助于确定我们可以进一步优化的应用程序的部分。" 迈克说。

"什么是浪费时间?"

"当 React 重新渲染组件树时,一些组件可能不会从它们的前一个表示形式中改变。然而,如果它们再次渲染,那么 React 在渲染相同的输出上就浪费了时间。PERF 插件可以跟踪所有这些时间,并给我们一个 React 浪费了时间渲染相同输出的总结。让我们看看这个实际操作。" 迈克说。

React 浪费的时间

"PERF 插件告诉我们,它浪费了时间在两次重新渲染Form组件上,但Form组件中没有任何变化,因此,它只是按照原样重新渲染了一切。" 迈克解释道。

"让我们看看Form组件,了解为什么会发生这种情况。"

// src/Form.js

import React from 'react';

export default React.createClass({
  getInitialState() {
    return { searchTerm: '' };
  },

  _submitForm() {
    this.props.performSearch(this.state.searchTerm);
  },

  render() {
    return (
      <div className="row" style={this.props.style}>
        <div>
          <div className="input-group">
            <input type="text"
                   className="form-control input-lg"
                   placeholder="Search books..."
                   onChange={(event) => { this.setState({searchTerm: event.target.value}) }}/>
            <span className="input-group-btn">
              <button className="btn btn-primary btn-lg"
                      type="button"
                      onClick={this._submitForm}>
                Go!
              </button>
            </span>
          </div>
        </div>
      </div>
    )
  }
})

"肖恩,Form组件的渲染不依赖于状态或属性。无论状态和属性如何,它都会渲染相同的输出。然而,当用户在输入框中输入字符时,我们会更新其状态。因此,React 会重新渲染它。实际上,重新渲染的输出并没有任何变化。因此,PERF 插件正在抱怨浪费了时间。" 迈克解释道。

"这是有用的信息,但这看起来像是一种微不足道的浪费,对吧?" 肖恩问道。

"同意。让我们做一些更改,这样我就可以向您展示 React 如何在实际上不应该的情况下浪费大量时间重新渲染相同的输出。" 迈克说。

"目前,我们只显示 Open Library API 返回的前 100 个搜索结果。让我们更改我们的代码,在同一页面上显示所有结果。"

// src/App.js
getInitialState() {
    return { books: [],
             totalBooks: 0,
             offset: 100,
             searching: false,
             sorting: 'asc',
             page: 1,
             searchTerm: '',
             totalPages: 1
    };
  }

"我引入了一个新的状态来保存搜索词、从开放图书馆获取的总页数以及当前正在获取的页码。"

"现在,我们想要从 API 默认获取同一页的所有结果。API 通过numFounds属性返回查询找到的书籍总数。基于这个,我们需要找到需要从 API 获取的总页数。"

"此外,每次最多返回 100 条记录,这些记录我们已经存储在state.offset中了。"

totalPages = response.numFound / this.state.offset + 1;

"一旦我们得到总页数,我们需要继续请求下一页的搜索结果,直到所有页面都被获取。你想要尝试让它工作吗?" 迈克问道。

"当然。" 肖恩说。

 // src/App.js  

 // Called when user hits "Go" button.
 _performSearch(searchTerm) {
    this.setState({searching: true, searchTerm: searchTerm});
    this._searchOpenLibrary(searchTerm);
  },

  _searchOpenLibrary(searchTerm) {
    let openlibraryURI = `https://openlibrary.org/search.json?q=${searchTerm}&page=${this.state.page}`;
    this._fetchData(openlibraryURI).then(this._updateState);
  },

  // called with the response received from open library API
  _updateState(response) {
    let jsonResponse = response;
    let newBooks = this.state.books.concat(jsonResponse.docs);
    let totalPages = jsonResponse.numFound / this.state.offset + 1;
    let nextPage = this.state.page + 1;

    this.setState({
      books: newBooks,
      totalBooks: jsonResponse.numFound,
      page: nextPage,
      totalPages: totalPages
    } this._searchAgain);
  },

     // Keep searching until all pages are fetched.
  _searchAgain() {
    if (this.state.page > this.state.totalPages) {
      this.setState({searching: false});
    } else {
      this._searchOpenLibrary(this.state.searchTerm);
    }
  }

"我将 API URL 更改为包含页面参数。每次从 API 收到响应时,我们都会用新的页面更新状态。我们还更新this.state.books以包括新获取的书籍。然后,在this.setState调用的回调中调用_searchAgain函数,以确保它是setState调用设置的下一页的正确值。" 肖恩解释说。

"很好,这是一个重要的要点,要记住不要在this.setState()调用之外调用_searchAgain函数,因为它可能会在setState()完成之前执行。"

"因为如果我们在外面调用它,_searchAgain函数可能会使用错误的this.state.page值。然而,由于你已经在回调中将_searchAgain函数传递给了setState,所以这种情况不可能发生。" 迈克解释道。

"_searchAgain函数会一直获取结果,直到所有页面都完成。这样,我们将在页面上显示所有搜索结果,而不仅仅是前 100 条。" 肖恩通知说。

"这正是我想要的。做得好。让我清理一下渲染方法,这样旋转器就会始终显示在底部。" 迈克说。

// src/App.js
render() {
    let style = {paddingTop: '5%'};
    return (
      <div className='container'>
        <Header style={style}></Header>
        <Form style={style}
              performSearch={this._performSearch}>
        </Form>

        {this.state.totalBooks > 0 ?
         <BookList
             searchCount={this.state.totalBooks}
             _sortByTitle={this._sortByTitle}>
           {this._renderBooks()}
         </BookList>
       : null }

        { this.state.searching ? <Spinner /> : null }
      </div>
    );
  }

"这将确保在所有结果都显示之前,旋转器会一直显示。好的,都完成了。现在让我们再次测量性能。" 迈克说。

React 浪费的时间

"哇,浪费的时间增加了很多!丹·布朗发布了新书吗?比我们上次看到的时间多了这么多?" 肖恩说。

"哈哈,我认为他刚才并没有发布新书。每次从下一页获取书籍时,我们都将它们添加到现有的书籍中,并从下一页开始获取书籍。然而,之前页面的书籍渲染并没有任何变化。因为我们把所有状态都保存在顶层的App组件中,所以每当它的状态发生变化时,App下的整个组件树都会重新渲染。因此,BookList会再次渲染。反过来,所有的BookRows也会再次渲染。这导致在重复渲染之前页面的相同BookRow组件上浪费了大量的时间。" 迈克说道。

"所以每次我们从新页面获取书籍时,包括已经在页面上存在的所有书籍都会再次重新渲染?我认为在这种情况下,仅仅将新的书籍行添加到现有列表中会更好。"肖恩说。

"别担心。我们可以轻松地消除这种不必要的浪费时间。React 为我们提供了一个用于短路重新渲染过程的钩子。它是shouldComponentUpdate。"

应该使用shouldComponentUpdate钩子

"肖恩,shouldComponentUpdate是一个钩子,它告诉 React 是否重新渲染组件。它不会在组件的初始渲染时被调用。然而,每当组件即将接收新的状态或属性时,shouldComponentUpdate都会在那时被调用。如果这个函数的返回值是true,那么 React 将重新渲染组件。然而,如果返回值是false,那么 React 将不会在下次调用之前重新渲染组件。在这种情况下,componentWillUpdatecomponentDidUpdate钩子也不会被调用。"迈克解释道。

"很好。那么我们的代码为什么浪费了这么多时间?React 不应该使用这个钩子来优化它,并且不应该反复重新渲染相同的BookRow组件吗?"肖恩问道。

"默认情况下,shouldComponentUpdate总是返回true。React 这样做是为了避免微小的错误。我们的代码中可能有可变的状态或属性,这会使shouldComponentUpdate返回假阳性。它可能在应该返回true时返回false,导致组件在应该重新渲染时没有重新渲染。因此,React 将实现shouldComponentUpdate的责任放在了开发者的手中。"迈克说。

"让我们尝试自己使用shouldComponentUpdate来减少在重新渲染相同的BookRow组件上浪费的时间。"迈克补充道。

"这是我们目前的BookRow组件:"

// src/BookRow.js

import React from 'react';

export default React.createClass({
  render() {
    return(
      <tr style={this.props.style}>
        <td><h4>#{this.props.index}</h4></td>
        <td><h4>{this.props.title}</h4></td>
        <td><h4>{(this.props.author_name || []).join(', ')}</h4></td>
        <td><h4>{this.props.edition_count}</h4></td>
      </tr>
    );
  }
});

"让我们添加shouldComponentUpdate以减少不必要的重新渲染。"

// src/BookRow.js

import React from 'react';

export default React.createClass({
  shouldComponentUpdate(nextProps, nextState) {
    return nextProps.title !== this.props.title ||
           nextProps.author_name !== this.props.author_name ||
           nextProps.edition_count !== this.props.edition_count;
  },

  render() {
    return(
      <tr style={this.props.style}>
        <td><h4>#{this.props.index}</h4></td>
        <td><h4>{this.props.title}</h4></td>
        <td><h4>{(this.props.author_name || []).join(', ')}</h4></td>
        <td><h4>{this.props.edition_count}</h4></td>
      </tr>
    );
  }
});

shouldComponentUpdate钩子接收nextPropsnextState作为参数,并且我们可以将它们与当前的状态或属性进行比较,以做出是否返回truefalse的决定。

"在这里,我们正在检查标题、作者姓名或版次是否已更改。如果这些属性中的任何一个已更改,那么我们将返回true。然而,如果所有这些都没有更改,那么我们将返回false。因此,如果没有任何属性更改,组件将不会重新渲染。由于BookRow组件只依赖于属性,我们根本不必担心状态变化。"迈克补充道。

"现在,再次测量性能,看看我们是否有所改进。"

shouldComponentUpdate 钩子

"太棒了,我们完全消除了BookRow组件重新渲染所花费的时间。然而,我们还可以做得更好。看起来我们也可以消除重新渲染FormHeader组件的时间,根据前面的结果。它们是静态组件。因此,它们根本不应该重新渲染。肖恩,这是你的下一个挑战。"

"知道了。"

// src/Header.js

import React from 'react';

export default React.createClass({
  shouldComponentUpdate(nextProps, nextState) {
    return false;
  },

  render() {
    return (
      <div className="row" style={this.props.style}>
        <div className="col-lg-8 col-lg-offset-2">
          <h1>Open Library | Search any book you want!</h1>
        </div>
      </div>
    )
  }
})

// src/Form.js

import React from 'react';

export default React.createClass({
  getInitialState() {
    return { searchTerm: '' };
  },

  shouldComponentUpdate(nextProps, nextState) {
    return false;
  },

  _submitForm() {
    this.props.performSearch(this.state.searchTerm);
  },

  render() {
    return (
      <div className="row" style={this.props.style}>
        <div>
          <div className="input-group">
            <input type="text"
                   className="form-control input-lg"
                   placeholder="Search books..."
                   onChange={(event) => { this.setState({searchTerm: event.target.value}) }}/>
            <span className="input-group-btn">
              <button className="btn btn-primary btn-lg"
                      type="button"
                      onClick={this._submitForm}>
                Go!
              </button>
            </span>
          </div>
        </div>
      </div>
    )
  }
})

"迈克,我们可以简单地从shouldComponentUpdate中返回false,对于HeaderForm组件,因为它们在渲染时根本不依赖于状态或 props!"

"完美的发现,肖恩。记下这些不依赖于任何东西的静态组件。它们是告诉 React 不比较它们的虚拟 DOM 的完美目标,因为它们根本不需要重新渲染。"迈克通知说。

"没错。我会密切关注 UI 中可以提取成更小组件的这些静态部分。"肖恩说。

"现在让我们看看在进行这些改进之后,是否消除了更多浪费的时间。"

The shouldComponentUpdate hook

"酷!我们消除了重新渲染相同的HeaderForm组件所浪费的时间。"迈克说。

"太棒了!让我也尝试一下消除BookListRowAlternator上花费的时间。"肖恩通知道。

"等等,肖恩。在我们做这件事之前,我想讨论一下shouldComponentUpdate的一个替代方案。"

PureRenderMixin

"肖恩,PureRenderMixin是一个可以作为shouldComponentUpdate替代品使用的附加组件。在底层,它使用shouldComponentUpdate并比较当前和下一个 props 和 state。让我们在我们的代码中试试它。当然,首先我们需要安装这个附加组件。"迈克说。

$ npm install react-addons-pure-render-mixin

// src/Header.js

import React from 'react';
import PureRenderMixin from 'react-addons-pure-render-mixin';

export default React.createClass({
 mixins: [PureRenderMixin],
 ..
 ..
})

// src/Form.js

import React from 'react';
import PureRenderMixin from 'react-addons-pure-render-mixin';

export default React.createClass({
 mixins: [PureRenderMixin],
 ..
 ..
 }
})

"肖恩,现在我们用过了PureRenderMixin,来看看浪费的时间吧。"迈克说。

PureRenderMixin

"哦,情况变得更糟了。PureRenderMixin函数又把重新渲染FormHeader组件所浪费的时间加回去了。怎么了,迈克?"肖恩问道。

"冷静点!我要解释一下为什么会这样。PureRenderMixin会将当前的 props 和 state 与下一个 props 和 state 进行比较,但它进行的是浅比较。因此,如果我们传递包含对象和数组的 state 或 props,即使它们内容相同,浅比较也不会返回 true。"迈克解释道。

"然而,我们在哪里将任何复杂对象或数组传递给HeaderForm组件呢?我们只是传递了书籍数据,如作者的名字、版次等。我们没有向Header传递任何东西,PureRenderMixin怎么会失败呢?"肖恩问道。

"你忘了从App组件传递给HeaderForm组件的样式属性。"迈克提醒说。

// src/App.js

render() {
    let style = {paddingTop: '5%'};
    return (
      <div className='container'>
        <Header style={style}></Header>
        <Form style={style}
              performSearch={this._performSearch}>
        </Form>
         ..
         ..
      </div>
)}

"每次App重新渲染时,都会创建一个新的样式对象,并通过 props 发送给HeaderForm。"

PureRenderMixin

The PureRenderMixin anti pattern

PureRenderMixin内部实现了shouldComponentUpdate,如下所示:

var ReactComponentWithPureRenderMixin = {
  shouldComponentUpdate: function(nextProps, nextState) {
    return shallowCompare(this, nextProps, nextState);
  },
};

shallowCompare函数也是 React 提供的一个附加功能,是一个比较当前状态和 props 与下一个状态和 props 的辅助函数。它基本上实现了与PureRenderMixin相同的功能,但由于它是一个函数,可以直接使用,而不是使用PureRenderMixin。当我们使用 ES6 类与 React 一起使用时,这尤其必要。" 迈克解释说。

"迈克,所以浅比较是PureRenderMixin未能检测到下一个 props 没有变化的原因吗?" 肖恩问。

"是的。shallowCompare只是遍历正在比较的对象的键,当每个对象中键的值不是严格相等时返回false。因此,如果我们传递简单的 props,如下所示,那么shallowCompare将正确地确定不需要重新渲染:"

// shallowCompare will detect correctly that props are not changed.
{ author_name: "Dan Brown", 
  edition_count: "20", 
  title: "Angels and Demons" }

"然而,如果道具是一个对象或数组,它将立即失败。"

{ author_name: "Dan Brown", 
  edition_count: "20", 
  title: "Angels and Demons", 
  style: { paddingTop: '%5' } }

"尽管PureRenderMixin为我们节省了一些代码行,但它可能不会像我们预期的那样始终有效。特别是当我们有可变状态、对象或数组作为 props 时。" 迈克说。

"明白了。所以当我们有嵌套状态或 props 时,我们可以编写自己的shouldComponentUpdate函数吗?" 肖恩问道。

"是的。PureRenderMixinshallowCompare对于具有简单 props 和 states 的简单组件来说很好,但我们在使用它时应该小心。" 迈克说。

注意

由于各种原因,在 React 世界中不建议使用混入。在此处查看PureRenderMixin模式的替代方法 - github.com/gaearon/react-pure-render

不可变数据

"迈克,不过我有一个问题。尽管如此,为什么PureRenderMixin最初要执行浅比较呢?它不应该执行深度比较,以便我们始终有更好的性能吗?" 肖恩对PureRenderMixin不太满意。

"嗯,这里有一个原因。浅比较非常便宜。它不花太多时间。深度比较总是昂贵的。因此,PureRenderMixin执行浅比较,这对于大多数简单用例来说已经足够好了。" 迈克说。

"然而,React 确实为我们提供了一个选项,可以定义我们自己的shouldComponentUpdate版本,就像我们之前看到的。我们只需从shouldComponentUpdate返回false就可以完全短路重新渲染过程,或者我们只需比较我们组件所需的那部分 props。"

"没错,就像我们为BookRow组件编写shouldComponentUpdate一样?" 肖恩问。

// src/BookRow.js

export default React.createClass({
  shouldComponentUpdate(nextProps, nextState) {
    return nextProps.title !== this.props.title ||
           nextProps.author_name !== this.props.author_name ||
           nextProps.edition_count !== this.props.edition_count;
  },

  render() {
    return(
      <tr style={this.props.style}>
        ..
      </tr>
    );
  }
});

"确实如此,肖恩。如果需要,你还可以根据组件的需求进行深度比较。"

// custom deep comparison as per requirement
shouldComponentUpdate(nextProps, nextState) {
    return nextProps.book.review === props.book.review;
}

"肖恩,我们还有另一个选择,那就是使用不可变数据。比较不可变数据非常简单,因为它总是会创建新的数据或对象,而不是修改现有的对象。"

// pseudo code 
book_ids = [1, 2, 3]
new_book_ids = book_ids.push(4)
book_ids === new_book_ids # false

"因此,我们只需要比较新对象的引用与旧对象的引用,当值相等时它们总是相同的,当值不相等时它们总是不同的。因此,如果我们使用不可变数据作为我们的 props 和 state,那么PureRenderMixin将按预期工作。" 迈克说道。

注意

检查facebook.github.io/immutable-js/,作为使用不可变数据作为 state 和 props 的选项。

摘要

在本章中,你了解了 React 提供的性能工具以及如何使用它们。我们使用了 PERF 插件:shouldComponentUpdatePureRenderMixin。我们还看到了在尝试提高我们应用性能时需要关注的区域。我们还研究了在提高性能时可能遇到的陷阱,特别是与PureRenderMixin相关。最后,我们讨论了不可变数据的重要性和优势。

在下一章中,我们将使用 React Router 和 Flux 详细查看 React 的数据模型。你将学习如何使用 React 与其他框架如 Backbone 一起使用。

第九章:React Router 和数据模型

在上一章中,我们探讨了可以提高 React 应用性能的 React 性能工具。我们探讨了使用 PERF 插件、PureRenderMixin 等,并查看了一些与 React 提供的性能工具相关的问题。

在本章中,我们将更深入地了解 react-router,并在不同级别执行路由。我们将探讨嵌套路由和传递参数,以及 react-router 在执行路由任务时如何维护历史记录。我们还将探讨传递和使用上下文来渲染 React 组件。最后,我们将探索数据模型,并将它们与其他框架混合匹配,用作 React 中的数据模型,例如 Backbone。

在本章中,我们将涵盖以下主题:

  • 在你的应用中使用 React

  • 使用 react-router 进行路由

  • 不同的路由机制

  • 设置路由和传递路由上下文

  • React 和数据存储/模型

  • 使用 Backbone 模型/集合作为数据存储

在本章结束时,我们将能够开始使用 react-router 和不同的路由模式,并在路由中传递上下文数据。我们还将能够用 Backbone.js 等类似的东西替换纯数据模型的部分。

新的冒险

"嗨,Shawn 和 Mike!"Carla 大声说道。

Shawn 和 Mike 吃了一惊。他们刚刚到达,正准备开始新的一天。过去几天他们一直在探索 React。

"我有一些好消息要告诉你们。我们得到了一个新的项目,我们需要构建一个基于猫的兴趣网站。就像说——Pinterest?用户可以喜欢猫的图片和资料。然后他们可以看到并喜欢相关的销售文章,”Carla 继续说。

"哦,不错,”Shawn 回应道。

Shawn 和 Mike 重新集结,开始讨论他们刚刚从 Carla 那里听说的新项目。

"这很好。所以,我想,我们想在面板形状中显示一个小型的 Pinterest 风格的图片画廊吗?"Shawn 询问道。

"正确,”Mike 接着说,“我们还想以大尺寸显示图片,也许在用户点击图片后显示模态窗口。Carla 说她想在页脚中展示随机的猫,这将带我们到一个完整的猫展示页面。”

"你知道什么,我知道我们将要使用的完美东西。让我们今天看看 react-router!我也知道一个完美的示例开始。我们将查看 react-router 中的 Pinterest 示例,github.com/rackt/react-router/tree/master/examples/pinterest。然后我们将在其基础上构建我们的应用。”

"不错,”Shawn 说,“我可以看到现有的示例中包含了一些我们讨论过的事情,比如模态显示。让我看看这个示例的样子。”

Shawn 看了以下示例:

import React from 'react'
import { render } from 'react-dom'
import { browserHistory, Router, Route, IndexRoute, Link } from 'react-router'
…
const PICTURES = [
  { id: 0, src: 'http://placekitten.com/601/601' },
  { id: 1, src: 'http://placekitten.com/610/610' },
  { id: 2, src: 'http://placekitten.com/620/620' }
]
const Modal = React.createClass({
… // Modal Class implementation
 })

const App = React.createClass({
  componentWillReceiveProps(nextProps) {
// takes care of context in case of Modals
  },
  render() {
// Main render for Modal or Cat Pages
 }
})

const Index = React.createClass({
  render() {
// Index page render 
..
        <div>
          {PICTURES.map(picture => (
            <Link key={picture.id}
              to={{
                pathname: `/pictures/${picture.id}`,
                state: { modal: true, returnTo: this.props.location.pathname }
              }}
            >
              <img style={{ margin: 10 }} src={picture.src} height="100" />
            </Link>
          ))}
        </div>
.. // Usage of React Router Links
        <p><Link to="/some/123/deep/456/route">Go to some deep route</Link></p>
      </div>
    )
  }
})

const Deep = React.createClass({
  render() {
// Render handler for some deep link
   )
  }
})

const Picture = React.createClass({
  render() {
    return (
      <div>
// Pictures display
        <img src={PICTURES[this.props.params.id].src} style={{ height: '80%' }} />
      </div>
    )
  }
})

// The actual Routing logic using Router Library.
render((
  <Router history={browserHistory}>
    <Route path="/" component={App}>
      <IndexRoute component={Index}/>
      <Route path="/pictures/:id" component={Picture}/>
      <Route path="/some/:one/deep/:two/route" component={Deep}/>
    </Route>
  </Router>
), document.getElementById('example'))

"看起来很有趣,”Shawn 说。

"是的,让我们逐一查看我们需要创建的组件。首先,让我们看看我们将如何存储我们的数据并在整个系统中显示猫的数据。目前,图片存储在 PICTURES 常量中。我们希望存储更多内容。"

创建 Backbone 模型

"所以,肖恩,让我们继续构建我们想要展示的猫的收藏。为了开发目的,我们将使用 lorempixel 服务提供的猫图片,例如,lorempixel.com/600/600/cats/。这将给我们一个 600 x 600 像素的随机猫图片。"

"接下来,我们将创建一个使用不同于常规对象的数据存储。我们想探索如何将不同的模型流程嵌入到我们的 React 应用中。在我们的例子中,让我们使用 Backbone 模型,而不是 PICTURES 常量。我知道你已经使用过 Backbone。"

"是的,我在我的前一个项目中使用过它。"

"那么,让我们定义我们的 Cat 模型。"

const PictureModel = Backbone.Model.extend({
  defaults: {
    src: 'http://lorempixel.com/601/600/cats/',
    name: 'Pusheen',
    details: 'Pusheen is a Cat'
  }
});

"在这里,我们存储猫图片的 src、它的名字以及一些关于它的细节。正如你所见,我们为这些属性提供了一些默认值。"

"接下来,让我们定义我们的 Cats 集合,包含所有的 Cat 记录。"

const Cats = new Backbone.Collection;
Cats.add(new PictureModel({src: "http://lorempixel.com/601/600/cats/", 
                                              name: Faker.Name.findName(), 
                                              details: Faker.Lorem.paragraph()}));

Cats.add(new PictureModel({src: "http://lorempixel.com/602/600/cats/", 
                                              name: Faker.Name.findName(), 
                                              details: Faker.Lorem.paragraph()}));
…

"在这里,我们使用 Faker 模块通过 Faker.Name.findName() 创建猫的随机名字,使用 Faker.Lorem.paragraph() 添加随机描述,并按需传递源信息。"

"酷,肖恩说。让我看看现在看起来怎么样。"

//models.js
import Backbone from 'backbone';
import Faker from 'faker';

const PictureModel = Backbone.Model.extend({
  defaults: {
    src: 'http://lorempixel.com/601/600/cats/',
    name: 'Pusheen',
    details: 'Pusheen is a Cat'
  }
});

const Cats = new Backbone.Collection;
Cats.add(new PictureModel({src: "http://lorempixel.com/601/600/cats/", name: Faker.Name.findName(), details: Faker.Lorem.paragraph()}));
…
Cats.add(new PictureModel({src: "http://lorempixel.com/606/600/cats/", name: Faker.Name.findName(), details: Faker.Lorem.paragraph()}));

module.exports = {Cats, PictureModel};

集成定义的 Backbone 模型

"接下来,让我们定义我们的索引,以及我们需要路由如何工作以及路由应该响应哪些路径。从那里,我们将继续构建我们的组件。"

"明白了。"

import React from 'react'
import { render } from 'react-dom'
import { createHistory, useBasename } from 'history'
import { Router, Route, IndexRoute, Link } from 'react-router'
import Backbone from 'backbone';
import Modal from './Modal'
import App from './App'
import { Cats, PictureModel } from './models';
import Picture from './Picture'
import Sample from './Sample'
import Home from './Home'

const history = useBasename(createHistory)({
  basename: '/pinterest'
});

render((
  <Router history={history}>
    <Route path="/" component={App}>
      <IndexRoute component={Home}/>
      <Route path="/pictures/:id" component={Picture}/>
      <Route path="/this/:cid/is/:randomId/sampleroute" component={Sample}/>
    </Route>
  </Router>
), document.getElementById('rootElement'));

"所以,我看到的第一件事是我们正在创建一个会话历史记录?"

"正确,我们在这里创建了一个会话历史记录。我们将使用它作为我们的路由器。"

"在这里,我们使用历史模块的 useBasename 方法,它提供了在 base URL 下运行应用的支撑,在我们的例子中是 /pinterest。"

"明白了。"

"接下来,我们将展示我们实际上希望路由如何工作。我们将我们的路由器包装进 <Router/> 组件中,并指定不同的 <Route/> 作为路径。"

"这被称为路由配置,它基本上是一组规则或指令,用于将 URL 匹配到某些 React 组件以便显示。"

"哦,我们可以讨论一下这个配置吗?它看起来很有趣。"

"确实如此。首先,让我们看看 <IndexRoute component={Home}/> 做了什么。当我们到达应用的 / 页面时,在我们的例子中将是 /pinterest,由 IndexRoute 定义的组件将被渲染。正如你可能猜到的,要渲染的组件是通过路由的组件参数传递的。请注意,这是在 App 组件中显示的,它是所有基础组件。"

IndexRoute 类似,我们有不同的 <Route/> 定义。在我们的示例中,如果你看到 <Route path="/pictures/:id" component={Picture}/>,它显示了路由是如何被使用的,以及我们是如何传递相同属性的。在这里,路径属性是一个匹配表达式,组件属性指定了在匹配路由后要显示的组件。

"注意这里的路径是如何定义的,它被指定为一个表达式。"

基于 URL 对路由进行匹配是基于三个组件:

  • 路由嵌套

  • 路径属性

  • 路由优先级

肖恩开始说,“我明白了嵌套的部分。我看到我们已经以嵌套的方式安排了我们的路由,就像一棵树。路由匹配和构建是基于这个树状匹配结构的。"

"对。其次,我们有路径属性。我们可以看到这些示例:"

      <Route path="/pictures/:id" component={Picture}/>
      <Route path="/this/:cid/is/:randomId/sampleroute" component={Sample}/>

"路径值是一个字符串,它作为一个正则表达式,可以由以下部分组成:"

  • :paramName: 例如,ID,这是在 URL 中传递的参数,如 /pictures/1212 被解析为 param id

  • (): 这可以用来指定一个可选的路径,例如 /pictures(/:id),这将匹配 /pictures 以及 /pictures/12

  • *: 就像正则表达式的情况一样,* 可以用来匹配表达式的任何部分,直到下一个 /?# 出现。例如,为了匹配所有 JPEG 图像,我们可以使用 /pictures/*.jpg

  • **: 贪婪匹配,类似于 *,但它贪婪地匹配。例如,/**/*.jpg 将匹配 /pictures/8.jpg 以及 /photos/10.jpg

"明白了。最后,什么是优先级?最可能的是,它应该使用文件中定义的第一个路由,并满足用于匹配路径的条件?"

"没错," 迈克大声说道。

"哦,在我忘记之前,我们还有一个 <Redirect> 路由。这可以用来将一些路由匹配到其他路由操作。例如,我们希望 /photos/12 匹配 /pictures/12 而不是,我们可以将其定义为代码。"

<Redirect from="/photos/:id" to="/pictures/:id" />

"太棒了。"

"接下来,让我们看看我们正在导入和使用的一切,我们将它们定义为组件。"

import React from 'react'
…
import Modal from './Modal'
import App from './App'
import { Cats, PictureModel } from './models';
import Picture from './Picture'
import Sample from './Sample'
import Home from './Home'

"让我们首先定义我们的 App 组件,它将作为容器:"

..
import { Router, Route, IndexRoute, Link } from 'react-router'
import Modal from './Modal'

const App = React.createClass({
  componentWillReceiveProps(nextProps) {
    if ((
            nextProps.location.key !== this.props.location.key &&
            nextProps.location.state &&
            nextProps.location.state.modal
        )) {
      this.previousChildren = this.props.children
    }
  },

  render() {
    let { location } = this.props;
    let isModal = ( location.state && location.state.modal && this.previousChildren );
    return (
        <div>
          <h1>Cats Pinterest</h1>
          <div>
            {isModal ?
                this.previousChildren :
                this.props.children
            }
            {isModal && (
                <Modal isOpen={true} returnTo={location.state.returnTo}>
                  {this.props.children}
                </Modal>
            )}
          </div>
        </div>
    )
  }
});

export {App as default}

"我们这里不会改变太多,这是从我们已经看到的示例中来的。"

"我看到了这里的位置使用。这是来自 react-router 吗?"

"正如我们所看到的,我们的 App 被包裹在路由器中。路由器通过 props 传递位置对象。位置对象实际上类似于 window.location,这是我们使用的 history 模块定义的。Location 对象在其上定义了各种特殊属性,我们将利用这些属性,如下所示:"

  • pathname: URL 的实际路径名

  • search: 查询字符串

  • state: 从 react-router 传递并绑定到位置的对象

  • action: PUSHREPLACEPOP 操作之一

  • key: 位置的唯一标识符

"明白了。我看到我们正在使用之前看到的props.children。"

  componentWillReceiveProps(nextProps) {
    if ((
            nextProps.location.key !== this.props.location.key &&
            nextProps.location.state &&
            nextProps.location.state.modal
        )) {
      this.previousChildren = this.props.children
    }
  }

"我想,当 Modal 显示时,我们将子元素和上一个屏幕存储到App对象上。"

"是的。我们首先检查是否显示了一个不同的组件,通过匹配 location 的 key 属性。然后我们检查是否在 location 上传递了状态属性,以及状态中的 modal 是否设置为 true。我们将在 Modal 显示的情况下做这件事。这是我们将状态传递给链接的方式:"

<Link … state={{ modal: true .. }}.. />

"当我们使用它来显示图片时,我们将查看Link对象。"

"明白了,肖恩说。"

"然后我看到我们正在传递子 props 或渲染上一个布局,然后,如果点击 modal,就在其上方显示Modal:"

           {isModal ?
                this.previousChildren :
                this.props.children
            }
            {isModal && (
                <Modal isOpen={true} returnTo={location.state.returnTo}>
                  {this.props.children}
                </Modal>
            )}

"没错!你在这一方面做得越来越好了,迈克兴奋地说。"

"现在,让我们看看我们的主要索引页面组件,好吗?"

// home.js
import React from 'react'
import { Cats, PictureModel } from './models';
import { createHistory, useBasename } from 'history'
import { Router, Route, IndexRoute, Link } from 'react-router'

const Home = React.createClass({
  render() {
    let sampleCat = Cats.sample();
    return (
        <div>
          <div>
            {Cats.map(cat => (
                <Link key={cat.cid} to={`/pictures/${cat.cid}`} state={{ modal: true, returnTo: this.props.location.pathname }}>
                  <img style={{ margin: 10 }} src={cat.get('src')} height="100" />
                </Link>
            ))}
          </div>
          <p><Link to={`/this/${sampleCat.cid}/is/456/sampleroute`}>{`Interesting Details about ${sampleCat.get('name')}`}</Link></p>
        </div>
    )
  }
});

export {Home as default}

"所以肖恩,我们首先导入在Cats集合中生成的所有数据。我们将遍历它们并显示带有链接到 Modals 的图片。你可以在这里看到这个过程:"

            {Cats.map(cat => (
                <Link key={cat.cid} to={`/pictures/${cat.cid}`} state={{ modal: true, returnTo: this.props.location.pathname }}>
                  <img style={{ margin: 10 }} src={cat.get('src')} height="100" />
                </Link>
            ))}

"是的,我看到我们正在使用cat对象的cid从 backbone 对象设置键。我们必须为链接指定路径,即它应该链接到的位置,我想?"

"没错。对于每只显示的猫,我们都生成一个唯一的动态路由,例如/pictures/121等等。现在,当我们点击它以显示放大后的猫时,我们正在将modal: true传递到<Link/>的状态中。"

"我们还传递了一个returnTo属性,它与从当前location.pathname获取的当前路径相关。我们将使用这个returnTo属性从状态中设置组件上的回链。我们将在 Modal 上显示一个,这样当点击时我们可以回到主页,并且 Modal 将被关闭。"

"明白了。我看到我们还在这里定义了一个用于样本猫展示页面的链接:"

    let sampleCat = Cats.sample();
…
render(
…
          <p><Link to={`/this/${sampleCat.cid}/is/456/sampleroute`}>{`Interesting Details about ${sampleCat.get('name')}`}</Link></p>
…
);

"是的,我们打算在这里随机展示一只猫。我们将在样本页面上显示关于猫的详细信息。现在,我想向你展示我们是如何在这里创建链接的:"

`/this/${sampleCat.cid}/is/456/sampleroute`

"在这里,我们正在创建一个嵌套的随机路由,例如,这可以匹配以下 URL:"

/this/123/is/456/sampleroute

"123456作为位置的参数。"

"很好,肖恩接着说。让我定义 Modal?让我重用示例中的那个。"

import React from 'react'
import { Router, Route, IndexRoute, Link } from 'react-router'

const Modal = React.createClass({
  styles: {
    position: 'fixed',
    top: '20%',
    right: '20%',
    bottom: '20%',
    left: '20%',
    padding: 20,
    boxShadow: '0px 0px 150px 130px rgba(0, 0, 0, 0.5)',
    overflow: 'auto',
    background: '#fff'
  },

  render() {
    return (
      <div style={this.styles}>
        <p><Link to={this.props.returnTo}>Back</Link></p>
        {this.props.children}
      </div>
    )
  }
})

export {Modal as default}

"很简单,肖恩。我们还需要定义如何显示图片。让我们定义一下。"

import React from 'react'
import { Cats, PictureModel } from './models';

const Picture = React.createClass({
  render() {
    return (
        <div>
          <img src={Cats.get(this.props.params.id).get('src')} style={{ height: '80%' }} />
        </div>
    )
  }
});

export {Picture as default}

"为了显示猫并获取它的详细信息,我们使用从 params 接收到的 ID。这些是通过params属性发送给我们的。然后我们从Cats集合中获取 ID。"

Cats.get(this.props.params.id)

"使用id属性,回忆一下我们是如何在定义如下链接时发送 ID 的:"

<Route path="/pictures/:id" component={Picture}/>

"最后,让我们看看如何使用示例组件来显示猫的信息:"

import React from 'react'
import { Cats, PictureModel } from './models';
import { createHistory, useBasename } from 'history'
import { Router, Route, IndexRoute, Link } from 'react-router'

const Sample = React.createClass({
  render() {
    let cat = Cats.get(this.props.params.cid);
    return (
        <div>
          <p>CID for the Cat: {this.props.params.cid}, and Random ID: {this.props.params.randomId}</p>
          <p>Name of this Cat is: {cat.get('name')}</p>
          <p>Some interesting details about this Cat:</p>
          <p> {cat.get('details')} </p>
          </p>
        </div>
    )
  }
});

export {Sample as default};

"有了这个,看起来我们已经完成了!让我们看看它看起来怎么样,好吗?"

"首页看起来很整洁。"

将定义的 Backbone 模型整合

"接下来,让我们看看 Modal 和链接与 URL 的样子。"

"这只猫看起来真不错。" 肖恩笑着说。

将定义的 Backbone 模型整合

"哈哈,是的。"

注意

注意 URL。点击时,模态链接变成了锚标签上的链接。我们处于同一页面,并且模态被显示。

"最后,我们有样本页面,在这里我们显示猫的详细信息。让我们看看它的样子:"

将定义的 Backbone 模型整合

"太棒了!"

数据模型和 Backbone

"肖恩,我想讨论一下我们在这里如何使用 Backbone 模型,或者我们如何存储数据。我们从以下代码迁移到使用 Backbone 集合。这帮助我们更好地定义我们的数据:"

PICTURES =[{array of objects}]

"然而,如果你注意到,我们最终定义了一个静态的对象集合。此外,这个集合是全局的,需要传递给其他部分。"

"这是真的。我也注意到我们为数据在全局范围内有一个固定的 state。我相信,我们在这里可能没有做什么。如果我们更新了,Views 仍然会保持不变吗?"

"没错!在我们这个案例中,我们以固定的方式发送、使用/修改数据,全局范围内。对这部分应用中的数据进行的任何更新都不会影响我们视图的显示方式,甚至不同组件中已经访问的数据也不会改变。例如,考虑一下 Home 组件改变了 Cats 常量。首先,它不会与 Sample、Modal 或其他组件同步更改。"

"其次,Home 组件对 Cats 集合的更改甚至不会改变 Home 组件的显示!"

"啊,这相当棘手。我想,我们最终会将所有这些集合状态存储在一个全局组件状态中,比如 App 组件,它只渲染一次。" 肖恩接着说。

"是的,我们可以这样做。但问题在于,在这种情况下,我们需要手动维护状态,并将子组件的状态更新到 App 组件,等等。想象一下,比如有人点击了一个猫的图片,需要改变猫的状态。事件会在 Picture 组件上发生,我们需要手动将事件传播到 HomeModal 组件,然后再传播到 App 组件,以便真正更新全局集合。"

"这不会很好。我相信这将很难跟踪和调试。"

"没错。在我们接下来的重构中,我们将尝试改变这种做法,将其限制在 App 中。从长远来看,我们将尝试使用 Flux。"

"哦,对了,我听说过它。它是用于传递或访问数据,以及通过事件或其他方式管理数据变化的吗?"

“嗯,不是完全如此,它帮助我们简化了单向数据流中的数据流。维护的状态会传播到组件中,并按需更新。例如,拥有一只猫的事件可能会改变数据存储,进而改变组件。”

“无论如何,我只是想给你一个关于这个的想法,以及为什么我们稍后会探索 Flux。现在,我们的解决方案按预期工作。”

天色渐晚。在 Adequate LLC 公司又度过了一个有趣的一天。肖恩和迈克合作,使用 react-router 并与之混合 Backbone 模型构建了一个简单的应用程序。

摘要

在本章中,我们构建了一个简单的类似 Pinterest 的应用程序,利用 react-router 并对其在不同级别的路由执行时进行了更深入的研究。我们还探讨了嵌套路由、传递参数、react-router 如何维护历史记录等问题,在执行路由任务时。我们还研究了如何传递和使用上下文来渲染 React 组件,以及如何将 Backbone 模型与它混合以维护 Cats 显示数据。

在下一章中,我们将探讨在现有应用程序的基础上添加动画和一些其他显示功能。

第十章。动画

在上一章中,我们了解了 react-router 并在不同级别执行路由。我们还探讨了嵌套路由、传递参数以及 react-router 在执行路由任务时如何维护历史记录。我们学习了传递上下文和使用上下文来渲染 React 组件。我们探讨了数据模型,并将它们与其他框架混合匹配,用作 React-like Backbone 中的数据模型,并介绍了 Flux。

在本章中,我们将探索一个有趣的 React 插件,动画。我们将从继续我们的猫 Pinterest 应用开始,并增强它以支持星标和共享数据来更新视图。然后我们将探索添加动画处理器。我们将看到组件是如何被包装进行动画的,以及 React 是如何为不同事件添加处理器的。我们还将探索不同的事件,以及我们如何轻松增强我们的应用程序以创建惊人的效果。

在本章中,我们将涵盖以下主题:

  • 修改数据流并从 react-router 链接传递数据

  • React 中的动画

  • CSS 过渡

  • 转换组

  • 转换处理器

  • 动画我们的动态组件

在本章末尾,我们将能够开始为不同的动作如添加新内容、更改数据和位置等对 React 组件进行动画处理。我们还将能够添加不同类型事件的处理器,并探索除了核心动画插件之外的不同动画选项。

在 Adequate LLC 有很多有趣的事情!

"嗨,肖恩和迈克!" 卡拉加入了迈克和肖恩的对话。

在前一天,卡拉要求他们为他们的一个客户构建一个 Pinterest 风格的猫应用。

"今天怎么样?"她询问道。

"一切顺利,卡拉。肖恩,你想向卡拉展示我们昨天建造的东西吗?"

"当然。"

Adequate LLC 的有趣事情

"看起来不错!我们接下来要添加点赞/星标猫的按钮吗?"

"是的,我们正准备做这件事。"

"酷。昨天客户打电话来了。他们除了显示猫之外还想显示屏幕上猫的更新流。这将在有人点赞猫时发生,这样我们就可以向其他用户展示它。"

"明白了。我们将开始工作,并模拟添加猫到屏幕上以开始。"

"太棒了,我就让你们俩处理吧。"

模型更新

"所以肖恩,我们不如将 Backbone 集合移动到一个类中,以独立的方式使用它,让它随机添加新的猫并提供一些其他工具,如下所示:"

const PictureModel = Backbone.Model.extend({
  defaults: {
    src: 'http://lorempixel.com/601/600/cats/',
    name: 'Pusheen',
    details: 'Pusheen is a Cat',
    faved: false
  }
});

"我们的PictureModel保持不变。我们在这里添加一个新的faved属性来维护用户是否喜欢这只猫的状态。

"我们将把这个新类命名为CatGenerator,它将提供我们用来显示猫的组件,以及显示、获取和添加新猫的数据。"

"明白了。需要我试一试吗?"

"当然。"

import Backbone from 'backbone';
import Faker from 'faker';
import _ from 'underscore';
…

class CatGenerator {
  constructor() {
    this.Cats = new Backbone.Collection;
    [600, 601, 602, 603, 604, 605].map( (height)=>{
      this.createCat(height, 600);
    })
  }

  createCat(height = _.random(600, 650), width = 600) {
    console.log('Adding new cat');
    this.Cats.add(new PictureModel({
      src: `http://lorempixel.com/${height}/${width}/cats/`,
      name: Faker.Name.findName(),
      details: Faker.Lorem.paragraph()
    }));
  }
}

"做得好,肖恩。"

"谢谢。我已经将createCat作为一个独立的方法移动,这样我们就可以在运行时向集合中添加猫。我现在正在添加一个随机的一个,随机高度为 600-650 和随机宽度来创建一个新的PictureModel实例。"

"此外,首先,我在类属性上创建了一个cats集合。接下来,我一开始就添加了六只猫。"

"酷。我们现在将开始更改它在我们的组件中的使用。"

提示

"记住,当新数据到来时,我们将更新组件。这样做的一个简单方法是开始在Home组件上存储CatGenerator作为状态对象。"

"让我们开始定义和更改我们的Home组件,如下所示:"

class Home extends React.Component {
  constructor() {
    super();
    this.timer = null;
    this.state = {catGenerator: new CatGenerator()};
  }

  componentDidMount() {
    this.timer = setInterval(::this.generateCats, 1000);
  }

  generateCats() {
    let catGenerator = this.state.catGenerator;
    catGenerator.createCat();
    clearInterval(this.timer);
    this.timer = setInterval(::this.generateCats, catGenerator.randRange());

    this.setState({catGenerator: catGenerator});
  }
…

"所以,我们在这里做的是创建一个计时器来跟踪时间间隔。我们将使用一个随机的时间间隔来模拟在这里添加新的猫流。"

"明白了," 肖恩接着说。

"为此,我添加了generateCats()方法。在我们的componentDidMount中,我们在第一次创建后添加并设置计时器来调用此方法。"

"在方法本身中,我添加了清除旧间隔,并且我们调用catGenerator.createCat()方法来实际上从我们的CatGenerator类创建猫。"

"然后我们重置计时器并设置一个新的,基于随机的时间间隔。我在CatGenerator类中添加了catGenerator.randRange()方法来生成随机的时间间隔。这就是它在CatGenerator类中的样子:"

randRange() {
    return _.random(5000, 10000);
  }

"明白了。这应该会在 5-10 秒的范围内创建一个新的猫流。"

"接下来,让我们看看我们的渲染方法看起来怎么样。我打算在猫旁边添加一个星号。"

render() {
    let Cats = this.state.catGenerator.Cats;

    return (
        <div>
          <div>

              {Cats.map(cat => (
                  <div key={cat.cid} style={{float: 'left'}}>
                    <Link to={`/pictures/${cat.cid}`}
                          state={{ modal: true, returnTo: this.props.location.pathname, cat: cat }}>
                      <img style={{ margin: 10 }} src={cat.get('src')} height="100"/>
                    </Link>
                    <span key={`${cat.cid}`} className="fa fa-star"></span>
                  </div>
              ))}

          </div>
        </div>
    )
  }

"我在这里做了两个更改。首先,我添加了一个默认未收藏的星号。"

                    <span key={`${cat.cid}`} className="fa fa-star"></span>

"其次,我开始在模态链接的状态中传递猫对象。"

                    <Link to={`/pictures/${cat.cid}`}
                          state={{ modal: true, 
                                       returnTo: this.props.location.pathname,  
                                       cat: cat }}>

"在我们的PictureModel框中,我们之前可以访问全局猫集合。从现在起,情况将不再是这样,我们需要将猫对象传递给Picture组件。"

"这很棒,我们能够将对象传递给从路由<Link/>对象来的组件。"

"是的,让我们继续更改图片组件,以便它能够正确地处理这个新的数据传递变化。我们的Modal保持不变:"

const Modal = React.createClass({
  styles: {
…   
  },

  render() {
     return (
      <div style={this.styles}>
        <p><Link to={this.props.returnTo}>Back</Link></p>
        {this.props.children}
      </div>
    )
  }
})
…
export {Modal as default}

"现在Picture组件开始使用猫对象。"

import React from 'react'
import { PictureModel } from './models';

const Picture = React.createClass({
  render() {
    let { location } = this.props;
    let cat = location.state.cat;
    console.log(this.props);
    return (
        <div>
          <div style={{ float: 'left', width: '40%' }}>
            <img src={cat.get('src')} style={{ height: '80%' }}/>
          </div>
          <div style={{ float: 'left', width: '60%' }}>
            <h3>Name: {cat.get('name')}.</h3>
            <p>Details: {cat.get('details')} </p>
          </div>
        </div>
    )
  }
});

export {Picture as default}

"正如你所看到的,猫对象是通过location.state对象从 props 接收的。"

"我已经扩展了图片以显示有关猫的更多详细信息,例如名称等,而不是在单独的页面上显示。之前它看起来相当空白。"

"酷,让我们看看它看起来怎么样,好吗?"

模型更新

"很好,星星看起来不错。我们很快需要检查我添加的样式。"

"模态看起来也不错,看看所有这些作为流生成的猫!"

"太棒了!" 迈克和肖恩欢呼。

模型更新

动画

"React 允许我们通过其 react-addons-css-transition-group 扩展插件轻松地动画化对象。"

"这为我们提供了对 ReactCSSTransitionGroup 对象的引用,这是我们用来动画化数据变化(如添加猫、点赞/取消点赞等)的。"

"让我们先从动画化新猫添加到流中开始,怎么样?"

render() {
    let Cats = this.state.catGenerator.Cats;

    return (
        <div>
          <div>
            <ReactCSSTransitionGroup transitionName="cats" 
                                     transitionEnterTimeout={500} 
                                     transitionLeaveTimeout={300}
                                     transitionAppear={true} 
                                     transitionAppearTimeout={500}>
              {Cats.map(cat => (
                  <div key={cat.cid} style={{float: 'left'}}>
                    <Link to={`/pictures/${cat.cid}`}
                          state={{ modal: true, returnTo: this.props.location.pathname, cat: cat }}>
                      <img style={{ margin: 10 }} src={cat.get('src')} height="100"/>
                    </Link>
                    <span key={`${cat.cid}`} className="fa fa-star"></span>
                  </div>
              ))}

</ReactCSSTransitionGroup>
          </div>
        </div>
    )
  }

"在这里,我更改了我们的渲染方法,并简单地用 ReactCSSTransitionGroup 元素包裹了猫集合的显示,如下所示。"

            <ReactCSSTransitionGroup transitionName="cats" 
                                     transitionEnterTimeout={500}
                                     transitionLeaveTimeout={300}
                                     transitionAppear={true} 
                                     transitionAppearTimeout={500}>

"让我们逐一在以下内容中查看它们:"

  • transitionName:此属性用于定义应用于不同事件(如元素进入、离开等)的 CSS 类的前缀。

  • transitionEnterTimeout:这是元素在渲染后新鲜显示的超时时间。

  • transitionLeaveTimeout:这与 transitionEnterTimeout 类似,但用于元素从页面移除时。

  • transitionAppear:有时,我们想在元素首次渲染时动画化元素集合的添加,在我们的例子中是猫。我们可以通过将此属性设置为 true 来实现这一点。

    注意

    注意,在第一个元素显示之后添加的元素将应用 transitionEnter 属性。

  • transitionAppearTimeout:这与其他超时值类似,但用于 transitionAppear

  • transitionEnter:默认情况下,此属性设置为 true。如果我们不想动画化元素进入过渡,则可以将其设置为 false

  • transitionLeave:默认情况下,此属性设置为 true。如果我们不想动画化元素离开过渡动画,则可以将其设置为 false

"现在,基于过渡和过渡名称,类被应用到 <ReactCSSTransitionGroup/> 组件内的元素上。例如,对于进入过渡,以及我们的 cats 前缀,cats-enter 将被应用到元素上。"

"在下一个周期中,cats-enter-active 将应用到元素应该处于的最终类。"

"明白了。"

"让我们检查一下我们可以根据这个定义的所有不同过渡。"

.cats-enter {
    opacity: 0.01;
}

.cats-enter.cats-enter-active {
    opacity: 1;
    transition: opacity 1500ms ease-in;
}
.cats-leave {
    opacity: 1;
}

.cats-leave.cats-leave-active {
    opacity: 0.01;
    transition: opacity 300ms ease-in;
}

.cats-appear {
    opacity: 0.01;
}

.cats-appear.cats-appear-active {
    opacity: 1;
    transition: opacity 1.5s ease-in;
}

"这里的动画过渡相当简单。当在开始时添加新元素,从我们初始化的六只猫开始,将应用 .cats-appear 类。在下一次计时器滴答声后,将添加 .cats-appear-active 类到元素上。"

"接下来,在过渡成功后,类将被移除,如下面的截图所示:"

Animate

"肖恩,如果你能看到,你会注意到猫是如何淡入然后以全不透明度显示其最终状态的。"

"不错。看起来很棒。当新元素被添加时,这是一个很好的效果。"

"确实。你想尝试动画化星星吗?"

"当然!"

"让我首先检查一下我们为星星设置的类。我看到你已经使用了 font-beautiful 星星并为他们添加了样式。"

.fa {
  transition: all .1s ease-in-out;
  color: #666;
}
.star{
    display: inline-block;
    width: 20px;
    position: relative;
}

.star span{
    position: absolute;
    left: 0;
    top: 0;
}

.fa-star{
  color: #fa0017;
}

.fa-star-o{
    color: #fa0017;
}

.fa-star-o:active:before {
  content: "\f005"!important;
}

"是的,就在那里。"

"首先,让我处理星星的点赞和取消点赞功能。"

faveUnfave(event){
    let catCid = event.target.dataset;
    let catGenerator = this.state.catGenerator;
    let Cats = catGenerator.Cats;
    let cat = Cats.get(catCid);
    cat.set('faved', !cat.get('faved'));
    catGenerator.Cats = Cats;
    this.setState({catGenerator: catGenerator});
  }

"将元素改为添加 data-cidhandler,如下所示:"

<span key={`${cat.cid}`}  className="fa fa-star" onClick={::this.faveUnfave} data-cid={cat.cid}></span>  

"首先,我将 faveUnfave 作为 onClick 事件传递,这个事件在这里绑定到了类上下文中。接下来,我为 data-cid 传递了 cat.cid 的值。"

"在 faveUnfave 方法中,我将拉取点赞元素的猫 ID。基于此,我将从猫生成器的猫集合中拉取猫对象。稍后,我将切换当前点赞值的状态并重置集合的状态。"

"看起来不错。"

"接下来,我将根据当前的点赞状态显示点赞或取消点赞的星星,并将其包装为 CSS 过渡,这样我们就可以开始显示显示和隐藏星星、更改颜色等动画。"

<ReactCSSTransitionGroup transitionName="faved"
                                             transitionEnterTimeout={500}
                                             transitionLeaveTimeout={300}
                                             transitionAppear={true}
                                             transitionAppearTimeout={500}
                                             className="star">
                    {()=>{
                      if(cat.get('faved') === true){
                        return <span key={`${cat.cid}`}  className="fa fa-star" onClick={::this.faveUnfave} data-cid={cat.cid}></span>;
                      } else {
                        return <span key={`${cat.cid}`}  className="fa fa-star-o" onClick={::this.faveUnfave} data-cid={cat.cid}></span>;
                      }
                    }()}
                    </ReactCSSTransitionGroup>

"太完美了,”迈克接着说。

"现在让我们为这个点赞添加样式。"

.faved-enter {
    transform: scale(1.5);
}

.faved-enter.faved-enter-active {
    transform: scale(3);
    transition: all .5s ease-in-out;
}

.faved-leave {
    transform: translateX(-100%);
    transform: scale(0);
}

.faved-leave.faved-leave-active {
    transform: scale(0);
    transition: all .1s ease-in-out;
}

"在这里,我添加了动画,当点击星星时,它会放大,类似于 Twitter 的点赞功能。然后,它会恢复缩放并保持在点赞状态。"

"同样,在取消点赞时,它将放大并恢复到原始大小。"

"看起来不错,让我们检查一下,”迈克接着说。

"嗯,我觉得所有元素都在这里,但它似乎不起作用,迈克?"

"让我看看。啊,所以罪魁祸首是这个:"

                       {()=>{
                      if(cat.get('faved') === true){
                        return <span key={`${cat.cid}`}  className="fa fa-star" onClick={::this.faveUnfave} data-cid={cat.cid}></span>;
                      } else {
                        return <span key={`${cat.cid}`}  className="fa fa-star-o" onClick={::this.faveUnfave} data-cid={cat.cid}></span>;
                      }
                    }()}

注意

注意我们在这里使用的关键值?它是相同的。TransitionGroup 会跟踪元素的变化,并根据键值执行动画任务。TransitionGroup 需要知道元素中发生了什么变化,以便执行动画任务,它还需要键来识别元素。

"在这种情况下,点赞或取消点赞时,键将保持为 cat.cid,因此元素保持不变。"

"让我们给键添加一个后缀或前缀,以及点赞状态。"

{()=>{
                      if(cat.get('faved') === true){
                        return <span key={`${cat.cid}_${cat.get('faved')}`} className="fa fa-star" onClick={::this.faveUnfave} data-cid={cat.cid}></span>;
                      } else {
                        return <span key={`${cat.cid}_${cat.get('faved')}`} className="fa fa-star-o" onClick={::this.faveUnfave} data-cid={cat.cid}></span>;
                      }
}()}

"太完美了。现在它工作了,迈克。"

"是的。肖恩,你在 CSS 动画方面做得很好。星星看起来不错。让我们看看现在是什么样子。"

"这就是我们给猫点赞时的样子:"

动画

"这个是在点赞过渡完成后。"

动画

"最后,当我们尝试取消点赞猫时,也会发生相同的动画。"

动画

"太完美了,卡拉会喜欢的!"

在 Adequate LLC 度过了一个愉快的一天。肖恩和迈克致力于重构他们的应用程序,以便数据更改能够反映视图更改,并对添加和删除的猫进行动画处理。他们还研究了如何点赞/取消点赞星星。

摘要

在本章中,我们围绕改变数据流和直接从 react-router 链接传递数据进行了操作。我们查看了对添加/删除或出现的对象集合进行动画处理。我们看到了 ReactCSSTransitionGroup 支持的不同过渡事件以及如何使用相关类来动画化我们的对象。

在下一章中,我们将学习如何使用 Jest 和 React TestUtils 测试我们的应用程序。

第十一章。React 工具

在上一章中,我们学习了如何使用动画插件和 CSS 过渡。我们还探索了不同的事件,并研究了如何通过动画轻松增强我们的应用程序,以创建令人惊叹的效果。

在本章中,我们将探讨 React 生态系统中的各种工具,这些工具在整个应用程序的生命周期中都有用——开发、调试和构建工具。我们将看到这些工具如何使开发 React 应用程序成为一种美好的体验。

在本章中,我们将研究以下工具:

  • Babel

  • ESLint

  • React 开发者工具

  • Webpack

  • 使用 Webpack 进行热重载

迈克和肖恩在开始他们的下一个项目之前有一些空闲时间。他们决定利用这段时间学习更多关于他们在 React 项目中迄今为止使用的各种工具,这些工具用于开发、测试和打包应用程序。

开发工具

"肖恩,今天我想讨论一下我们在今天构建 React 应用程序过程中所使用的工具。React 是一个非常小的库,它做了一件事情做得很好——渲染 UI。然而,在我们迄今为止的旅程中,我们不得不使用很多其他工具与 React 一起使用。今天是我们讨论所有这些工具的一天。" 迈克说。

"太棒了,迈克!我总是准备好了。让我们开始吧。" 肖恩兴奋地说。

使用 Babel 进行 ES6 和 JSX

"肖恩,我们从一开始就使用了 ES6 或 ES2015 代码。我们也非常看好使用 JSX。有时,我们也使用了 ES7 代码,比如我们最新的 Cats Pinterest 项目中函数bind操作符。"

// src/Home.js

class Home extends React.Component {
  componentDidMount() {
    this.timer = setInterval(::this.generateCats, 1000);
  }
}

"是的,迈克。我喜欢这些新功能的简洁性。" 肖恩说。

"然而,当前的浏览器仍然不理解我们编写的 ES6 或 ES7 代码。我们使用 Babel 将这些代码转换为 ES5 JavaScript,这样当前的浏览器就可以运行。它允许我们今天使用未来的 JavaScript 语法。Babel 还支持 JSX,因此它与 React 一起使用非常方便。" 迈克解释说。

"Babel 非常模块化,并带有插件架构。它为不同的 ES6/ES7 语法提供了插件。通常,我们希望使用特定于 React 和特定于 ES6 的插件。Babel 将这些常见插件组合成称为预设的东西。Babel 为 ES6、React 以及未来的语言提案的不同阶段提供了各种插件。"

"我们主要对使用 ES2015 和 React 预设感兴趣,这些预设包含所有与 ES6 和 React 相关的插件。偶尔,我们确实需要一些高级功能,例如 ES7 函数绑定语法,因此我们需要单独配置它。在这种情况下,我们直接使用单个插件,就像我们使用transform-function-bind来处理bind函数语法一样。" 迈克解释说。

注意

所有这些预设和插件都有自己的 npm 包。Babel 就是这样构建的——一个小型核心和庞大的插件架构,周围有许多配置选项。

"因此,我们将不得不分别安装所有这些包。"

npm install babel-core --save
npm install babel-loader --save
npm install babel-preset-react --save
npm install babel-preset-es2015 --save
npm install babel-plugin-transform-function-bind –save

"明白了。我也看到了我们 Webpack 配置中的一些 Babel 相关配置。" 肖恩说。

"是的。虽然 Babel 允许我们从命令行转换文件,但我们不想手动转换每个文件。因此,我们已经以这种方式配置了 Webpack,它将在启动应用程序之前使用 Babel 转换我们的 ES6/ES7 代码。它使用 babel-loader 包。等我们今天晚些时候讨论 Webpack 时再详细讨论这个问题吧。" 迈克说。

注意

我们在这本书中一直使用 Babel 版本 6。查看有关 Babel 的更多详细信息,请访问babeljs.io/

ESLint

"肖恩,你看到我们项目中关于 linting 的提交了吗?"

"是的。最初我对这些小小的变化感到很烦恼,但后来我习惯了。"

"Linting非常重要,尤其是如果我们想在不同的项目中保持代码质量。幸运的是,使用 ESLint 来 lint React 项目非常容易。它还支持 ES6 语法和 JSX,这样我们也可以 lint 我们的下一代代码。" 迈克说道。

"我们使用 eslint-plugin-react 和 babel-eslint npm 包来 lint ES6 和 React 代码。我们还全局安装了 ESLint npm 包。"

注意

查看有关如何开始使用 ESLint 的详细信息,请访问eslint.org/docs/user-guide/getting-started

"迈克,我也看到你在package.jsonscripts下添加了 lint 命令。" 肖恩补充道。

// package.json  

"scripts": {
"lint": "eslint src"
}

"是的,肖恩。在大项目中,这里那里可能会遗漏一些事情是很常见的。有一个命令来 lint 项目有助于我们找到这些事情。我们需要 eslint、eslint-babel 和 eslint-plugin-react 包来在我们的代码中使用 ESLint。因此,在尝试运行此命令之前,我们需要安装它。"

npm install eslint --save
npm install babel-eslint --save
npm install eslint-plugin-react –save

"我们也在使用一些标准的 ESLint 配置选项。这些选项存在于我们项目的.eslintrc文件中。我们在这个文件中定义了 ESLint 要检查的规则。我们还启用了 ES6 功能,让 ESLint 将其列入白名单。否则,它会对原生只支持 ES5 的此类代码引发 linting 错误。我们还指定 ESLint 应使用 babel-eslint 作为解析器,以便 ES6 代码能够被 ESLint 正确解析。"

// .eslintrc
{
  "parser": "babel-eslint",
  "env": {
    "browser": true,
    "es6": true,
    "node": true,
    "jquery": true 
  },
  "plugins": [
    "react" 
  ],
  "ecmaFeatures": {
    "arrowFunctions": true,
    "blockBindings": true,
    "classes": true,
    "defaultParams": true,
    "destructuring": true,
    "forOf": true,
    "generators": true,
    "modules": true,
    "spread": true,
    "templateStrings": true,
    "jsx": true
  },
  "rules": {
    "consistent-return": [0],
    "key-spacing": [0],
    "quotes": [0],
    "new-cap": [0],
    "no-multi-spaces": [0],
    "no-shadow": [0],
    "no-alert": [0],
    "no-unused-vars": [0],
    "no-underscore-dangle": [0],
    "no-use-before-define": [0, "nofunc"],
    "comma-dangle": [0],
    "space-after-keywords": [2],
    "space-before-blocks": [2],
    "camelcase": [0],
    "eqeqeq": [2]
  }
}

"我们现在已经准备好了。去运行我们的 Pinterest 项目,并修复剩余的 linting 问题。" 迈克说道。

$ npm run lint

> react-router-flux@0.0.1 lint /Users/prathamesh/Projects/sources/reactjs-by-example/chapter11
> eslint src

/reactjs-by-example/chapter11/src/Home.js
 29:20  error  Missing space before opening brace  space-before-blocks

x 1 problem (1 error, 0 warnings)

"啊,它抱怨缺少了一个空格。让我快速修复一下。"

// Before  
faveUnfave(event){
    …
}

// After
faveUnfave(event) {
…
}

"完美,肖恩!"

注意

ESLint 也可以与您的文本编辑器集成。有关更多详细信息,请查看eslint.org/docs/user-guide/integrations.html

React Dev Tools

"肖恩,React 在提升开发者体验方面非常出色。他们发布了 react-dev-tools 来帮助我们调试我们的应用程序。React 开发者工具是 Chrome 和 Firefox 的插件,这使得调试 React 应用程序变得有趣。"

“一旦安装了插件,你将在运行 React 应用的浏览器控制台中看到一个 React 选项卡。有趣的是,这个选项卡也会显示在生产中使用 React 的网站上,例如,Facebook。”

React 开发工具

“一旦我们点击 React 选项卡,它就会显示我们应用中的所有组件。”

React 开发工具

“肖恩,正如你可能注意到的,我们可以在左侧面板中看到所有的组件。在右侧,我们看到左侧面板中选择的组件的属性和状态。因此,我们可以在任何时间点检查 UI 状态。我们不需要添加 console.log 语句来查看我们的组件发生了什么。”

“不仅如此,它还为我们提供了一个临时变量——\(r**。控制台中的所选组件在控制台中可用为 **\)r。”

React 开发工具

“让我们看看控制台中的 $r 能给我们带来什么,这样我们就可以直接在控制台中调试所选组件。”

React 开发工具

“它还允许我们滚动到 UI 中的所选组件,以查看组件的实际源代码。它还可以显示特定类型的所有组件。”

React 开发工具

“肖恩,你对这些开发工具有什么看法?”迈克问道。

“我非常印象深刻。这真的很棒!从现在开始,我将在每一个 React 项目中使用它们。”肖恩对看到 React 开发工具的力量感到非常兴奋。

注意

查看更多关于 React 开发工具的详细信息:github.com/facebook/react-devtools

构建工具

“肖恩,当创建新的 Web 应用程序时,构建系统可能是我们首先应该关心的事情。它不仅是一个运行脚本的工具,在 JavaScript 世界中,它通常塑造我们应用程序的基本结构。”

以下责任应由构建系统执行:

  • 外部依赖以及内部依赖都应该被管理

  • 它应该运行编译器/预处理器

  • 它应该优化生产环境中的资源

  • 开发服务器、浏览器重新加载器和文件监视器应该由它运行

“有很多不同的工具,如 Grunt、Gulp 和 Browserify,可以用作我们构建系统的一部分。每个工具都有其自身的优缺点。然而,我们已经决定在我们的项目中使用 Webpack。”迈克说道。

什么是 Webpack?

“Webpack 是一个模块打包器。它将我们的 JavaScript 及其依赖项打包成一个单独的包。”

"与 Browserify 和其他工具不同,Webpack 还可以捆绑其他资源,如 CSS、字体和图像。它支持 CommonJS 模块语法,这在 node.js 和 npm 包中非常常见。因此,它使事情变得更容易,因为我们不需要使用另一个包管理器来处理前端资源。我们可以只使用 npm,并在服务器端代码和前端代码之间共享依赖项。它还足够智能,能够按正确顺序加载依赖项,这样我们就不需要担心显式和隐式依赖项的顺序。"(内联代码不需要翻译)

"因此,Webpack 本身就可以执行 Browserify 以及其他构建工具如 Grunt 和 Gulp 的任务。"(内联代码不需要翻译)

Note(内联代码不需要翻译)

"本节不会涵盖 Webpack 的所有方面。然而,我们将讨论如何有效地使用 Webpack 与 React 结合。"(内联代码不需要翻译)

Webpack configuration(内联代码不需要翻译)

"肖恩,在一个典型的 React 应用中,我们在组件中使用 ES6 代码和 JSX。我们还在同一个组件中使用前端资源,使其更易于携带。因此,我们的 Webpack 配置必须正确处理所有这些方面。" 迈克解释道。

"让我们以我们的 Pinterest 应用为例,看看 Webpack 是如何配置来运行它的。"(内联代码不需要翻译)

"首先,我们需要通知 Webpack 关于我们应用的入口点。在我们的例子中,它是index.js文件,它将App组件挂载到 DOM 中。"(内联代码不需要翻译)

// src/index.js
render((
  <Router history={history}>
    <Route path="/" component={App}>
      <IndexRoute component={Home}/>
      <Route path="/pictures/:id" component={Picture}/>
    </Route>
  </Router>
), document.getElementById('rootElement'));

"因此,我们在webpack.config.js文件中提到入口点为src/index.js。"(内联代码不需要翻译)

// webpack.config.js
path = require('path');
var webpack = require('webpack');

module.exports = {
  // starting point of the application
  entry: [ './src/index']
};

"其次,我们需要通知 Webpack 将生成的捆绑代码放在哪里。这是通过添加一个输出配置来完成的。"(内联代码不需要翻译)

// webpack.config.js
var path = require('path');
var webpack = require('webpack');

module.exports = {
  entry: ['./src/index'],
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'bundle.js',
    publicPath: '/static/'
  }
}

"输出选项告诉 Webpack 将编译后的文件写入当前目录的 dist 文件夹。文件名将是bundle.js。我们可以通过运行webpack命令来查看bundle.js的输出。"(内联代码不需要翻译)

$ webpack
Hash: f8496f13702a67943730
Version: webpack 1.12.11
Time: 2690ms
 Asset     Size  Chunks             Chunk Names
bundle.js  1.81 MB       0  [emitted]  main
 [0] multi main 52 bytes {0} [built]
 + 330 hidden modules

"这将创建一个包含所有编译代码的dist/bundle.js文件。"(内联代码不需要翻译)

"The publicPath specifies the public URL address of the output files, when referenced in a browser. This is the path that we use in our index.html file, which will be served by the web server to the users."(内联代码不需要翻译)

// index.html
<html>
  <head>
    <title>React Router/ Data Models</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" type="text/css" />
    <link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css" rel="stylesheet">
  </head>
  <body>
    <div id='rootElement' class="container"></div>
  </body>
  <script src="img/jquery-2.1.4.min.js"></script>
  <script src="img/bootstrap.min.js"></script>
 <script src="img/bundle.js"></script>
</html>

Loaders(内联代码不需要翻译)

"之后,我们必须指定不同的加载器来正确转换我们的 JSX、ES6 代码和其他资源。加载器是对你的应用资源文件应用的一种转换。它们是运行在 node.js 中的函数,它们将资源文件的源作为参数,并返回新的源。我们使用babel-loader来处理我们的 ES6 和 JSX 代码。"(内联代码不需要翻译)

// webpack.config.js
module.exports = {
  module: {
    loaders: [
      {
        test: /\.jsx?$/,
        loader: 'babel-loader',
        query: {
          presets: ['es2015', 'react'],
          plugins: ['transform-function-bind']
        },
        include: path.join(__dirname, 'src')
      }]
  }
};

"我们通过 npm 安装了babel-loader包,并将其包含在package.json中。之后,我们在 Webpack 配置中指定了它。测试选项匹配给定的正则表达式。给定的加载器解析这些文件。因此,babel-loader将编译src目录中由include选项指定的源文件中的.jsx.js文件。我们还指定babel-loader应使用 es2015 和 react 预设以及 function-bind 转换器插件,这样 Babel 就能正确解析我们所有的代码。"(内联代码不需要翻译)

"对于其他类型的资产,如 CSS、字体和图像,我们使用它们自己的加载器。"

// webpack.config.js

module.exports = {
module: {
    loaders: [
      {
        test: /\.jsx?$/,
        loader: 'babel-loader',
        query: {
          presets: ['es2015', 'react'],
          plugins: ['transform-function-bind']
        },
        include: path.join(__dirname, 'src')
      },
      { test: /\.css$/, loader: "style-loader!css-loader" },
      { test: /\.woff(\d+)?$/, loader: 'url?prefix=font/&limit=5000&mimetype=application/font-woff' },
      { test: /\.ttf$/, loader: 'file?prefix=font/' },
      { test: /\.eot$/, loader: 'file?prefix=font/' },
      { test: /\.svg$/, loader: 'file?prefix=font/' },
      { test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: "url-loader?limit=10000&minetype=application/font-woff"},
      { test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: "file-loader" }
    ]
  }
};

"所有这些加载器都包含在其自己的 npm 包中。我们必须为style-loadercss-loaderurl-loaderfile-loader安装 npm 包,并更新package.json。"

注意

查阅webpack.github.io/docs/using-loaders.html以获取有关使用和配置加载器的更多详细信息。

热模块替换

"肖恩,Webpack 最酷的特性之一是热模块替换HMR)。这意味着每次我们修改一个组件并保存文件时,Webpack 都会在不重新加载浏览器和丢失组件状态的情况下替换页面上的模块。"迈克告知。

"哇!这听起来非常令人印象深刻。"肖恩惊呼。

"为了使热重载工作,我们必须使用出色的 react-hot-loader 包和 webpack-dev-server。webpack-dev-server 包在启动服务器之前,每次文件更改之前都无需重复运行 Webpack,它会为我们使用webpack.config.js中提供的config选项运行应用。设置 webpack-dev-server 的关键点是配置它以启用热重载。这可以通过添加hot: true配置选项来完成。"

// server.js
var webpack = require('webpack');
var WebpackDevServer = require('webpack-dev-server');
var config = require('./webpack.config');

new WebpackDevServer(webpack(config), {
  publicPath: config.output.publicPath,
  hot: true,
  historyApiFallback: true
}).listen(9000, 'localhost', function (err, result) {
  if (err) {
    console.log(err);
  }

  console.log('Listening at localhost:9000');
});

"这将确保 webpack-dev-server 将在 localhost 端口9000上启动,并启用热重载。它还将使用我们在webpack.config.js中定义的所有配置。"迈克说。

"我们将必须修改我们的package.json以运行server.js脚本。"

// package.json
"scripts": {
    "start": "node server.js",
  }

"这将确保npm start命令将运行webpack-dev-server。"

"我们还需要在我们的 Webpack 配置中进行一些更改,以便使热重载工作。我们必须配置入口选项以包含开发服务器和热重载服务器。"

entry: [
    'webpack-dev-server/client?http://localhost:9000',
    'webpack/hot/only-dev-server',
    './src/index'
]

"接下来,我们需要通知 Webpack 使用 hot-loader 与我们已经添加的其他加载器。"

module: {
    loaders: [
      { test: /\.jsx?$/,
        loader: 'react-hot',
        include: path.join(__dirname, 'src')
      }
     .. .. .. 
    ]
  }

"最后,Webpack 的热模块替换插件必须包含在配置的插件部分。"

plugins: [
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoErrorsPlugin()
]

"最终的 Webpack 配置看起来像这样"

// webpack.config.js
var path = require('path');
var webpack = require('webpack');

module.exports = {
  devtool: 'eval',
  entry: [
    'webpack-dev-server/client?http://localhost:9000',
    'webpack/hot/only-dev-server',
    './src/index'
  ],
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'bundle.js',
    publicPath: '/static/'
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoErrorsPlugin()
  ],
  resolve: {
    extensions: ['', '.js', '.jsx']
  },
  module: {
    loaders: [
      { test: /\.jsx?$/,
        loader: 'react-hot',
        include: path.join(__dirname, 'src')
      },
      {

        test: /\.jsx?$/,
        loader: 'babel-loader',
        query: {
          presets: ['es2015', 'react'],
          plugins: ['transform-function-bind']
        },
        include: path.join(__dirname, 'src')
      },
      { test: /\.css$/, loader: "style-loader!css-loader" },
      { test: /\.woff(\d+)?$/, loader: 'url?prefix=font/&limit=5000&mimetype=application/font-woff' },
      { test: /\.ttf$/, loader: 'file?prefix=font/' },
      { test: /\.eot$/, loader: 'file?prefix=font/' },
      { test: /\.svg$/, loader: 'file?prefix=font/' },
      { test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: "url-loader?limit=10000&minetype=application/font-woff"},
      { test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: "file-loader" }
    ]
  }
};

"现在如果我们使用npm start启动应用,那么它将使用 webpack-dev-server 的热重载器。肖恩,尝试更改一些代码并检查代码是否在浏览器中更新而无需刷新页面。魔法!!"迈克解释说。

"太好了,迈克。是的,它确实有效。万岁 Webpack 和热重载!"

摘要

在本章中,你学习了 React 生态系统中的各种工具——开发、测试和生产工具,我们在应用开发的各个阶段都使用了这些工具。我们讨论了 Babel,JavaScript 转译器,将我们的下一代 JavaScript 代码转换为 ES5。我们还看到了如何使用 ESLint 和 React 开发者工具使 React 开发变得容易。最后,我们看到了如何使用 Webpack 及其强大的加载器和配置选项与 React 一起使用。我们看到了这些工具如何使开发 React 应用成为一种美好的体验。"

在下一章中,我们将深入探讨 Flux 作为架构的应用。我们已了解到在组件间共享数据时会出现问题。我们将学习如何使用 Flux 来克服这些问题。

第十二章。Flux

在上一章中,我们查看了一系列在应用程序整个生命周期中都有用的 React 生态系统中的工具——开发、测试和生产。我们还看到了 React 如何使用开发者工具来提高开发者体验。我们了解到了可以与 React 一起使用的各种测试工具。为了总结,我们看到了如何使用构建工具,如 Webpack 和 Browserify,以及它们如何与 React 一起使用。

在本章中,我们将深入探讨 Flux 作为一种架构。我们已经看到了在组件间数据共享过程中出现的问题。我们将看到如何通过拥有一个单一的数据存储点来克服这些问题。接下来,我们将检查如何使用 React 来克服这个问题。

分发器充当一个中央枢纽来管理这种数据流和通信以及动作如何调用它们。最后,我们将查看在构建我们的社交媒体追踪器应用程序时发生的完整数据流。

在本章中,我们将涵盖以下主题:

  • Flux 架构

  • 存储库

  • 动作

  • 分发器

  • Flux 实现

在本章结束时,我们将能够开始用 Flux 替换应用程序中具有紧密数据耦合的部分。我们将能够为 Flux 搭建必要的基础,并轻松地在我们的 React 视图中开始使用它。

Flux 架构和单向流

“嘿,迈克和肖恩!”卡拉在一个晴朗的早晨说道。

“嗨,卡拉,你今天怎么样?”

“太棒了。你之前构建的应用程序很好,客户喜欢它。他们很快会为它添加更多功能。同时,我们还有一个小型应用程序要构建。”

“哦,不错。我们打算构建什么?”迈克问道。

“我们需要构建一种社交追踪器。首先,我们展示用户的 reddits、tweets 等内容。我们稍后会扩展它以显示其他信息。”

“明白了,”肖恩重复道。

“祝你有美好的一天;我将把它留给你。”

“肖恩,你对这个新项目有什么看法?”

“这应该会很有趣。嗯……我们能否探索 Flux 并在应用程序中使用它?我们在构建上一个应用程序时讨论过它。”

“是的,我们可以。这将是一个了解 Flux 如何工作的完美机会。在我们开始使用它之前,让我们先了解一下 Flux 实际上是什么。”

“Flux 是 React 使用单向流的一个简单架构。我们之前讨论过单向流如何适合 React。当数据有任何更改时,React 遵循始终渲染的模型。数据不会像双向绑定的情况那样有其他方向。”

“这并不完全符合模型-视图-控制器MVC)的工作方式。它由模型(存储库)、动作和分发器组成,最后是视图(React 视图)。”

“目前还没有完整的 Flux 作为框架的模块,因为它不是为此而设计的。Facebook 提供了 Flux 模块,它由分发器组成。其他部分,如视图存储库,可以在没有太多支持的情况下完成。让我们一一过一遍,好吗?”

"当然。我相信我们可以讨论它们是如何相互关联的,以及为什么当应用开始增长时它们特别有用。"

"是的。"

"正如您在下面的图像中可以看到的,各种组件相互连接并独立工作。数据在一个循环中以单一流向流动。"

"正如我之前提到的,分发器充当中央枢纽。每当视图发生事件时,例如用户点击按钮或 Ajax 调用完成,就会调用动作。动作也可能由分发器调用。"

"动作是简单的结构,将有效载荷传递给分发器分发器识别动作以及从动作和数据中获取的更新当前状态所需的其他细节。"

Flux 架构和单向流

"然后分发器将其传播到存储。分发器就像一个回调注册表,所有存储都注册自己。每当发生某些动作时,分发器都会通知并回调存储。无论动作是什么,它都会被发送到所有存储。"

"分发器不做任何复杂的活动,它只是将有效载荷转发给已注册的存储,并且不处理任何数据。"

"执行逻辑和复杂决策以及数据更改的责任委托给了存储。这有助于将数据更改点集中在单一位置,并避免在应用程序周围进行更改,这些更改更难追踪。"

"在接收到分发器的回调后,存储根据动作类型决定是否需要执行任何操作。基于回调,它可以更新当前存储。它也可以等待其他存储更新。在完成更改后,它继续通知视图。在我们的简单 Flux 版本中,可以通过使用从 events 模块可用的EventEmitter模块来实现这一点。"

"与动作类似,视图会注册自己以监听存储中的变化。在某些变化发生时,EventEmitter会发出一个事件。根据事件类型,它将调用一个已注册监听该事件的View方法。"

"接收事件的视图可以根据它可用的任何存储的当前状态更新其自身状态。状态更新然后触发视图更新。"

"这个过程通过视图事件继续,导致对动作分发器等的调用。"

"希望,现在这有点清晰了?" 迈克问道。

"嗯...是的,让我理清思路。我们有动作来执行动作,基于一个事件。然后它通知分发器,然后通知任何已注册监听更改的存储。存储根据动作类型更新自己,并通知 React 视图更新自己。"

"正确!让我们立即深入到应用中。我们将基于官方 Flux 示例构建我们的应用。它将这样结构化。"

js/

├── actions

│ └── SocialActions.js

├── app.js

├── components

│ └── SocialTracker.react.js

├── constants

│ └── SocialConstants.js

├── dispatcher

│ └── AppDispatcher.js

├── stores

│ └── SocialStore.js

└── utils

└── someutil.js

"现在,正如卡拉提到的,我们需要显示来自 Twitter 和 Reddit 的用户数据。对于 Reddit,它可以通过 API 调用公开获取,正如我们很快将看到的。"

"对于 Twitter,我们需要做一些基础设置并创建一个 Twitter 应用。我们可以在apps.twitter.com/上创建一个新的。我已经为我们的应用创建了一个。"

Flux 架构和单向流

"然后我们将使用twitter模块来访问 Twitter 并从用户那里获取 tweets。让我们设置一个config.js文件来存储我们之前创建的访问令牌,如下所示:"

module.exports ={
  twitter_consumer_key: 'xxxx',
  twitter_consumer_secret: 'xxxx',
  twitter_access_token_key: 'xxxx',
  twitter_access_token_secret: 'xxxx'
}

"这些对应于我们在我们的应用中创建的相对键和秘密。接下来,我们将创建一个客户端,使用前面的凭证访问数据。"

var Twitter = require('twitter');
var config = require('./config');

var client = new Twitter({
  consumer_key: config.twitter_consumer_key,
  consumer_secret: config.twitter_consumer_secret,
  access_token_key: config.twitter_access_token_key,
  access_token_secret: config.twitter_access_token_secret
});

"我们将在我们的 express 服务器应用程序中使用这个客户端。正如我说的,对于 Reddit,我们可以直接调用 Reddit API 来访问 reddits。对于 Twitter,它将首先击中我们的 node App并返回 tweets 到我们的 React 组件。"

"你想定义这个吗,肖恩?"

"当然。"

var express= require('express');
var app = new (require('express'))();
var port = 3000

app.get('/tweets.json', function (req, res) {
  var params = {screen_name: req.query.username};
  client.get('statuses/user_timeline', params, function (error, tweets, response) {
    if (!error) {
      res.json(tweets);
    } else {
      res.json({error: error});
    }
  });
});

"我在这里定义了一个名为tweets.json的 JSON 端点。它将调用client.get()方法,这是一个 REST API 包装器,用于调用 Twitter API。我们调用statuses/user_timeline API 来获取用户的用户时间线,这是从请求中传递给我们的。"

在收到响应后,它将把这个信息发送回调用它的 React 组件。"

"看起来不错。现在,让我们从 App 开始。我们将首先定义 Dispatcher。"

// AppDispatcher.js
var Dispatcher = require('flux').Dispatcher;

module.exports = new Dispatcher();

"我们通过从flux.Dispatcher中引入它来定义我们的 dispatcher。然后我们将在各个地方使用它。"

Flux actions

"现在我们需要定义我们将要作为常量在各种地方引用的动作类型,例如从 Actions 发送类型到 store,并在我们的 store 中,决定传递给 store 的动作类型以采取适当的行动。

//SocialConstants.js
var keyMirror = require('keymirror');

module.exports = keyMirror({
  FILTER_BY_TWEETS: null,
  FILTER_BY_REDDITS: null,
  SYNC_TWEETS: null,
  SYNC_REDDITS: null

});

"在这里,我们使用github.com/STRML/keyMirror包根据键创建对象的关键和值。这将转换为类似于以下的对象。"

{
FILTER_BY_TWEETS: 'FILTER_BY_TWEETS', 
…
}

"当添加新键时,这很方便,可以避免再次重复相同的内容。"

"我们现在可以开始使用动作常量了。它们代表我们将要执行的四种动作,如下所示:"

  • SYNC_TWEETS: 这将获取给定用户的 tweets

  • SYNC_REDDITS: 这将获取给定主题的 reddits

  • FILTER_BY_TWEETS: 这仅显示 tweets,而不是 tweets 和 reddits

  • FILTER_BY_REDDITS: 这仅显示 reddits,而不是 tweets 和 reddits

"接下来,让我们定义将在我们的视图的不同地方调用的动作。"

// file: SocialActions.js
var AppDispatcher = require('../dispatcher/AppDispatcher');
var SocialConstants = require('../constants/SocialConstants');
var assign = require('object-assign');
var JSONUtil = require('../utils/jsonutil');

var SocialActions = {

  filterTweets: function (event) {
    AppDispatcher.dispatch({
      type: SocialConstants.FILTER_BY_TWEETS,
      showTweets: event.target.checked
    });
  },

  filterReddits: function (event) {
    AppDispatcher.dispatch({
      type: SocialConstants.FILTER_BY_REDDITS,
      showReddits: event.target.checked
    });
  },

  syncTweets: function (json) {
    AppDispatcher.dispatch({
      type: SocialConstants.SYNC_TWEETS,
      tweets: json.map((tweet) => {
        return assign(tweet, {type: 'tweet'})
      }),
      receivedAt: Date.now()
    });
  },

  syncReddits: function (json) {
    AppDispatcher.dispatch({
      type: SocialConstants.SYNC_REDDITS,
      reddits: json.data.children.map((child) => {
        return assign(child.data, {type: 'reddit'})
      }),
      receivedAt: Date.now()
    });
  },

  fetchTweets: function (username) {
    fetch(`/tweets.json?username=${username}`)
        .then(JSONUtil.parseJSON)
        .then(json => SocialActions.syncTweets(json)).catch(JSONUtil.handleParseException)
  },

  fetchReddits: function (topic) {
    fetch(`https://www.reddit.com/r/${topic}.json`)
        .then(JSONUtil.parseJSON)
        .then(json => SocialActions.syncReddits(json)).catch(JSONUtil.handleParseException)
  }
};

module.exports = SocialActions;

"让我们逐个分析这些动作:"

  fetchTweets: function (username) {
    fetch(`/tweets.json?username=${username}`)
        .then(JSONUtil.parseJSON)
        .then(json => SocialActions.syncTweets(json)).catch(JSONUtil.handleParseException)
  }

"在这里,我们使用 fetch,这与我们之前使用的 Ajax 类似,用于从我们的tweets.json API 获取推文,其中我们传递了需要获取推文的用户名。我们在这里使用我们定义的 JSON 实用方法。"

var JSONUtil = (function () {
  function parseJSON(response){
    return response.json()
  }
  function handleParseException(ex) {
    console.log('parsing failed', ex)
  }
  return {'parseJSON': parseJSON, 'handleParseException': handleParseException}
}());

module.exports = JSONUtil;

"它们帮助我们将响应转换为 JSON,或者在失败的情况下记录它们:"

"在从 API 接收到成功响应后,我们从同一模块调用SocialActions.syncTweets(json)方法。"

  syncTweets: function (json) {
    AppDispatcher.dispatch({
      type: SocialConstants.SYNC_TWEETS,
      tweets: json.map((tweet) => {
        return assign(tweet, {type: 'tweet'})
      }),
      receivedAt: Date.now()
    });
  }

"接下来,syncTweets接受 JSON。然后,它将 JSON 包装成一个对象有效载荷,发送给分发器。在这个对象中,我们创建了一个推文数组,从有效载荷中。我们还为每个对象标记了类型,以表示它是推文,这样我们就可以在同一个数组中混合和匹配推文和 Reddit,并识别它代表的是推文还是 Reddit。"

assign(tweet, {type: 'tweet'})

"我们使用Object.assign,它将两个对象合并在一起。我们在这里使用object-assign包。"

"现在,我们通知分发器关于最终要传递给存储器的有效载荷,如下所示:"

AppDispatcher.dispatch({ payload…});

"同样,我们还有一个syncReddits方法,如下所示:"

  fetchReddits: function (topic) {
    fetch(`https://www.reddit.com/r/${topic}.json`)
        .then(JSONUtil.parseJSON)
        .then(json => SocialActions.syncReddits(json)).catch(JSONUtil.handleParseException)
  }

"这从https://www.reddit.com/r/${topic}.json获取 Reddit,例如www.reddit.com/r/twitter.json。"

"在获取数据后,它将数据传递给SocialActions.syncReddits(json)),从而为分发器创建有效载荷,如下所示:"

  syncReddits: function (json) {
    AppDispatcher.dispatch({
      type: SocialConstants.SYNC_REDDITS,
      reddits: json.data.children.map((child) => {
        return assign(child.data, {type: 'reddit'})
      }),
      receivedAt: Date.now()
    });
  }

"请注意,我们在这里传递了类型属性给动作。这是为了通知存储器在接收到有效载荷时采取什么动作。"

"明白了。看到我们如何基于这个对象进行操作会很有趣。"

"是的。接下来,我们有两个简单的方法,将事件传递到存储器中,如下所示:"

  filterTweets: function (event) {
    AppDispatcher.dispatch({
      type: SocialConstants.FILTER_BY_TWEETS,
      showTweets: event.target.checked
    });
  },

  filterReddits: function (event) {
    AppDispatcher.dispatch({
      type: SocialConstants.FILTER_BY_REDDITS,
      showReddits: event.target.checked
    });
  },

"我们将使用这些方法作为onClick方法。点击复选框时,复选框的值——无论是 Reddit 还是 Twitter——将在event.target.checked中可用。"

"我们将这些包裹在一个简单的对象中,用动作调用的类型标记它们,并将相同的对象发送给分发器。这样,我们将知道我们将要显示推文、Reddit 还是什么都没有。"

Flux 存储

"很好,看起来我们现在都准备好创建我们的存储器了。"

"是的,肖恩。我们将从定义我们将不断更新并用作存储器的状态对象开始。"

var AppDispatcher = require('../dispatcher/AppDispatcher');
var EventEmitter = require('events').EventEmitter;
var SocialConstants = require('../constants/SocialConstants');
var assign = require('object-assign');
var _ = require('underscore');

var CHANGE_EVENT = 'change';

var _state = {
  tweets: [],
  reddits: [],
  feed: [],
  showTweets: true,
  showReddits: true
};

"我们还定义了一个CHANGE_EVENT常量,我们将其用作标识符来监听来自存储器事件发射器的更改类型的事件。"

"然后我们定义一个方法来更新状态,创建一个新的状态。"

function updateState(state) {
  _state = assign({}, _state, state);
}

"这合并了需要更新和合并到现有状态中的新属性,并更新了当前状态。"

"很好,这看起来与 React 的setState方法有些相似,”肖恩说。"

"是的。现在我们将定义我们的存储器,它将更新当前状态。"

var SocialStore = assign({}, EventEmitter.prototype, {

  getState: function () {
    return _state;
  },

  emitChange: function () {
    this.emit(CHANGE_EVENT);
  },

  addChangeListener: function (callback) {
    this.on(CHANGE_EVENT, callback);
  },

  removeChangeListener: function (callback) {
    this.removeListener(CHANGE_EVENT, callback);
  }
});

"在这里,我们通过继承EventEmitter来定义我们的SocialStore。这使它能够被组件用来注册监听事件,在我们的例子中是CHANGE_EVENTaddChangeListenerremoveChangeListener方法接受应该在事件上调用并移除监听器的方法,如下:this.on(CHANGE_EVENT, callback);this.removeListener(CHANGE_EVENT, callback);

"每当我们要通知监听器时,我们调用。"

this.emit(CHANGE_EVENT);

"最后,我们的视图可以使用以下函数从存储中获取当前状态:"

getState: function () {
    return _state;
  }

"最后,Shawn,让我们用我们的单个分发器将这些全部结合起来,如下:"

AppDispatcher.register(function (action) {

  switch (action.type) {

    case SocialConstants.FILTER_BY_TWEETS:
      updateState({
        showTweets: action.showTweets,
        feed: mergeFeed(_state.tweets, _state.reddits, action.showTweets, _state.showReddits)
      });
      SocialStore.emitChange();
      break;

    case SocialConstants.FILTER_BY_REDDITS:
      updateState({
        showReddits: action.showReddits,
        feed: mergeFeed(_state.tweets, _state.reddits, _state.showTweets, action.showReddits)
      });
      SocialStore.emitChange();
      break;
    case SocialConstants.SYNC_TWEETS:
      updateState({
        tweets: action.tweets,
        feed: mergeFeed(action.tweets, _state.reddits, _state.showTweets, _state.showReddits)
      });
      SocialStore.emitChange();
      break;

    case SocialConstants.SYNC_REDDITS:
      updateState({
        reddits: action.reddits,
        feed: mergeFeed(_state.tweets, action.reddits, _state.showTweets, _state.showReddits)
      });
      SocialStore.emitChange();
      break;
    default:
    // no op
  }
});

"每当AppDispatcher.dispatch被有效载荷调用时,前面的方法就会被调用。"

"让我们看看这些操作中的一个。"

    case SocialConstants.SYNC_TWEETS:
      updateState({
        tweets: action.tweets,
        feed: mergeFeed(action.tweets, _state.reddits, _state.showTweets, _state.showReddits)
      });
      SocialStore.emitChange();
      break;

"我们在这里所做的就是调用updateState来通过提供更新的推文和基于mergeFeed方法的更新来更新当前状态。"

"让我们看看它。"

function mergeFeed(tweets, reddits, showTweets, showReddits) {
  let mergedFeed = [];
  mergedFeed = showTweets ? mergedFeed.concat(tweets) : mergedFeed;
  mergedFeed = showReddits ? mergedFeed.concat(reddits) : mergedFeed;

  mergedFeed = _.sortBy(mergedFeed, (feedItem) => {
    if (feedItem.type == 'tweet') {
      let date = new Date(feedItem.created_at);
      return date.getTime();
    } else if ((feedItem.type == 'reddit')) {
      return feedItem.created_utc * 1000;
    }
  })
  return mergedFeed;
};

"我根据是否选择了showTweetsshowReddits来组合了各种要处理的操作。"

"所以,这个方法所做的就是接受推文和 reddit 数组数据,以及检查是否选中了显示 reddits 或显示推文。我们根据这些选中的/未选中的字段将这些字段构建到mergedFeed数组中。"

"然后,我们使用underscorejssortBy方法对这个混合的推文和 reddits 数据数组mergedFeed进行排序,基于两种类型对象的time字段。对于推文,这个字段是created_at字段,而对于 reddit,它是created_utc字段。我们使用 UTC 时间戳来规范化时间以进行比较。"

"回到同步推文操作,在更新状态后,我们在存储上调用发射器方法:"

      SocialStore.emitChange();

"这会从存储中调用我们的发射器,最终将更新传递给组件。"

"明白了。我认为下一步是创建我们的视图。"

"没错。我们将视图拆分为三个组件——HeaderMainSectionSocialTracker容器组件。"

"我们首先从Header开始,如下:"

var React = require('react');
var ReactBootstrap =  require('react-bootstrap');
var Row =  ReactBootstrap.Row, Jumbotron =  ReactBootstrap.Jumbotron;

var Header = React.createClass({

  render: function () {
    return (
        <Row>
          <Jumbotron className="center-text">
            <h1>Social Media Tracker</h1>
          </Jumbotron>
        </Row>
    );
  }

});

module.exports = Header;

"这是一个简单的显示组件,包含标题。"

"啊,Mike。我注意到你正在使用 react-bootstrap 模块。这看起来很整洁。它帮助我们用属性将它们包裹在 React 组件中,而不是在普通元素和 bootstrap 属性中定义。"

"是的。我们在这里使用JumbotronRow。这个Row将被包裹在一个 bootstrap 网格组件中。"

"接下来,我们将设置我们的MainSection组件,这将显示获取 Twitter 和 Reddit 主题用户名的输入,以及检查它们:"

var React = require('react');
…
var SocialActions = require('../actions/SocialActions');
var SocialStore = require('../stores/SocialStore');
var MainSection = React.createClass({

  getInitialState: function () {
    return assign({twitter: 'twitter', reddit: 'twitter'}, SocialStore.getState());
  },

  componentDidMount: function () {
    SocialStore.addChangeListener(this._onChange);
    this.syncFeed();
  },

  componentWillUnmount: function () {
    SocialStore.removeChangeListener(this._onChange);
  },

  render: function () {

    return (
        <Row>
          <Col xs={8} md={8} mdOffset={2}>
            <Table striped hover>
              <thead>
              <tr>
                <th width='200'>Feed Type</th>
                <th>Feed Source</th>
              </tr>
              </thead>
              <tbody>
              <tr>
                <td><Input id='test' type="checkbox" label="Twitter" onChange={SocialActions.filterTweets}
                           checked={this.state.showTweets}/></td>
                <td><Input onChange={this.changeTwitterSource} type="text" addonBefore="@" value={this.state.twitter}/>
                </td>
              </tr>
              <tr>
                <th><Input type="checkbox" label="Reddit" onChange={SocialActions.filterReddits}
                           checked={this.state.showReddits}/></th>
                <td><Input onChange={this.changeRedditSource} type="text" addonBefore="@"
                           value={this.state.reddit}/></td>
              </tr>
              <tr>
                <th></th>
                <td><Button bsStyle="primary" bsSize="large" onClick={this.syncFeed}>Sync Feed</Button>
                </td>
              </tr>
              </tbody>
            </Table>
          </Col>
        </Row>
    );
  },

  changeTwitterSource: function (event) {
    this.setState({twitter: event.target.value});
  },

  changeRedditSource: function (event) {
    this.setState({reddit: event.target.value});
  },

  syncFeed: function () {
    SocialActions.fetchReddits(this.state.reddit);
    SocialActions.fetchTweets(this.state.twitter);
  },

  _onChange: function () {
    this.setState(SocialStore.getState());
  }

});

module.exports = MainSection;

"现在组件在这里做了一些事情。首先,它根据存储设置状态。"

  getInitialState: function () {
    return assign({twitter: 'twitter', reddit: 'twitter'}, SocialStore.getState());
  },

"它还在跟踪两个不同的字段——Twitter 和 Reddit——用户名信息。我们根据之前看到的字段输入绑定这些值:"

  changeTwitterSource: function (event) {
    this.setState({twitter: event.target.value});
  },

  changeRedditSource: function (event) {
    this.setState({reddit: event.target.value});
  },

"然后在输入字段上使用这个更改处理程序,就像这样。"

<Input onChange={this.changeTwitterSource} type="text" addonBefore="@" value={this.state.twitter}/>

"接下来,我们有 componentDidMountcomponentWillUnmount 函数来注册和注销,以便监听来自 SocialStore 的事件:"

  componentDidMount: function () {
    SocialStore.addChangeListener(this._onChange);
    this.syncFeed();
  },

  componentWillUnmount: function () {
    SocialStore.removeChangeListener(this._onChange);
  },

"在这里,我们将 _onChange 方法注册为每当 SocialStore 发生变化时被调用。_onChange 方法反过来根据存储的状态更新组件的当前状态,如下所示:"

    this.setState(SocialStore.getState());

"接下来,我们指定要为检查/取消检查推特/Reddit 显示和同步推文和 Reddit 调用等事件调用的 SocialAction 方法。在调用同步数据时,syncFeed 被调用,它从 SocialActions 调用相关的同步方法,传入当前的推特名称和 Reddit 主题。"

  syncFeed: function () {
    SocialActions.fetchReddits(this.state.reddit);
    SocialActions.fetchTweets(this.state.twitter);
  },

"最后,我们将使用 SocialTracker 组件来封装一切,如下所示:"

var ArrayUtil = require('../utils/array');
var assign = require('object-assign');
var Header = require('./Header.react');
var MainSection = require('./MainSection.react');
var React = require('react');
var SocialStore = require('../stores/SocialStore');
var SocialActions = require('../actions/SocialActions');
var ReactBootstrap =  require('react-bootstrap');
var Col =  ReactBootstrap.Col, Grid =  ReactBootstrap.Grid, Row =  ReactBootstrap.Row;

var SocialTracker = React.createClass({
  getInitialState: function() {
    return assign({}, SocialStore.getState());
  },
  componentDidMount: function() {
    SocialStore.addChangeListener(this._onChange);
  },
  componentWillUnmount: function() {
    SocialStore.removeChangeListener(this._onChange);
  },
  render: function() {
    return (
        <Grid className="grid">
          <Header/>
          <MainSection/>
          {this.renderFeed()}
        </Grid>
    )
  },

  renderFeed: function() {
    var feed = this.state.feed;
    var feedCollection = ArrayUtil.in_groups_of(feed, 3);
    if (feed.length > 0) {
      return feedCollection.map((feedGroup, index) => {
        console.log(feedGroup);
        return <Row key={`${feedGroup[0].id}${index}`}>
          {feedGroup.map((feed) => {
            if (feed.type == 'tweet') {
              return <Col md={4} key={feed.id}><div className="well twitter"><p>{feed.text}</p></div></Col>;
            } else {
              var display = feed.selftext == "" ? `${feed.title}: ${feed.url}` : feed.selftext;
              return <Col md={4} key={feed.id}><div className="well reddit"><p>{display}</p></div></Col>;
            }
          })}
        </Row>
      });
    } else {
      return <div></div>
    }
  },

  _onChange: function() {
    this.setState(SocialStore.getState());
  }

});

module.exports = SocialTracker;

"我们使用了之前用来监听存储更新并更新组件当前状态的相同设置。"

"很好,我看到,剩下的只是遍历信息流并将它们显示出来,”肖恩继续说。

"我看到我们正在以每行三组的格式显示信息流,并根据是否是推文等应用单独的样式。为了分组,我们似乎使用了 ArrayUtil。"

var ArrayUtil = (function () {
  function in_groups_of(arr, n) {
    var ret = [];
    var group = [];
    var len = arr.length;
    for (var i = 0; i < len; ++i) {
      group.push(arr[i]);
      if ((i + 1) % n == 0) {
        ret.push(group);
        group = [];
      }
    }
    if (group.length) ret.push(group);
    return ret;
  };

  return {'in_groups_of': in_groups_of}
}());

module.exports = ArrayUtil;

"没错。有了这个,看起来我们一切都准备好了。我们最终将按常规显示组件。"

var React = require('react');
var ReactDOM = require('react-dom');
var SocialTracker = require('./components/SocialTracker.react');

ReactDOM.render(
  <SocialTracker />,
  document.getElementById('container')
);

"让我们看看它看起来怎么样,好吗?"

Flux stores

"这是它的样子,没有推文:"

Flux stores

"当更改推特用户时,它看起来是这样的:"

Flux stores

"这看起来很棒,迈克!"

摘要

"我们深入研究了 Flux 作为架构。我们看到 Dispatcher 充当中央枢纽来传输我们的数据和动作,以及处理它们的动作。我们看到主要责任是操纵状态和更新状态被委托给了存储本身。最后,我们看到它们是如何结合在一起,并使其在视图中使用和跨组件共享存储变得容易。"

第十三章。Redux 和 React

在上一章中,我们深入探讨了 Flux 作为架构。我们看到了在组件间共享数据时出现的问题。我们看到了这个架构的不同部分——动作、商店、视图和分发器——并基于我们的纯 Flux 示例、Facebook 的分发器和 EventEmitter 进行了构建。最后,我们构建了一个简单的应用来查看所有这些组件是如何结合在一起以创建一个简单的流程,在组件间共享公共状态。

在本章中,我们将探讨在流行的基于 Flux 的状态管理实现中如何使用 Flux。我们将看到它与之前看到的纯 Flux 实现有何不同。我们将查看 Redux 的不同组件——它的商店、动作和 reducer。最后,我们将看到应用如何与商店连接,维护商店的单个状态,并在视图中传递信息。

在本章中,我们将涵盖以下主题:

  • Redux

  • 设置 Redux

  • Redux 商店

  • Reducers

  • 将商店连接到应用组件

  • Redux 中的数据流

在本章结束时,我们将能够开始在应用中使用 Redux 来维护视图的状态。我们将能够设置它并与应用的各个部分连接。我们将能够看到如何在商店中分配数据,并使用 reducer 和 actions 来管理商店数据。

Redux

"早上好,肖恩,"迈克开始说道。

"早上好,迈克。我们今天要做什么?"

"啊,昨天,我们使用了 Flux。这是为了向您介绍 Flux 的基础。在我们的大多数项目中,我们使用了类似的方法来管理应用的状态。"

"今天,我们将看到如何使用 Redux。"

"酷。"

"正如在github.coktreactjs/redux中所述,Redux 是 JavaScript 应用的可预测状态容器。它有点像我们之前实现的那样。"

"使用 Redux,我们为整个应用维护一个单一的状态树,并添加 reducer 来增强商店的状态。之前,我们直接修改_state的值,然后通知订阅者关于变化。让我们看看我们的应用设置,如下开始:"

├── actions

│ └── social.js

├── components

│ └── SocialTracker.js

├── config.js

├── containers

│ └── App.js

├── reducers

│ ├── index.js

│ └── social.js

├── server.js

├── store

│ └── configureStore.js

├── styles

│ └── App.css

├── utils

"这是基于我们之前的应用和来自github.acktreactjs/redux/tree/master/examples/todomvc的示例。"

"嗯,我没有在这里看到分发器。"

"正确。Redux 与正常的 Flux 不同;在这里,它没有分发器。在设置过程中,我们将看到如何将商店连接到组件,以便组件能够获取商店状态变化的更新。"

"明白了。那我们就像以前一样开始吧?"

"是的。"

"让我们从我们的主组件开始;这将通过 Redux 进行包装以监听存储,如下所示:"

      // App.js
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import SocialTracker from '../components/SocialTracker'
import * as SocialActions from '../actions/social'

function mapStateToProps(state) {
  return {
    social: state.social
  }
}

function mapDispatchToProps(dispatch) {
  return bindActionCreators(SocialActions, dispatch)
}

export default connect(mapStateToProps, mapDispatchToProps)(SocialTracker)

设置 Redux

"现在,这里有一些不同的事情发生来设置我们的存储。让我们逐一来看:"

  • mapStateToProps:我们使用这个方法来定义我们将如何将 Redux 存储的状态映射到发送给连接到存储的组件的 props。每当存储中发生新的变化时,组件会收到通知,并通过此方法传递给对象有效载荷。

  • mapDispatchToProps:这个方法用于映射动作并将它们传递到 props 上,以便它们可以在组件内部使用。

  • bindActionCreators:这用于将我们的动作创建器(SocialActions)包装成分发调用,以支持直接调用动作。这有助于调用动作并通知存储进行更新,这些更新是由于分发而进行的。

  • connect:最后,我们有connect调用。这实际上将 React 组件连接到存储。它不会改变原始组件,而是增强并创建一个新的组件。然后我们可以开始使用组件中的动作。

"明白了。所以,我们创建了两个方法来映射 Redux 中的动作和状态如何提供给组件。然后我们将存储连接到 Redux 以监听更新,并在存储有更新时将动作和存储提供给组件。"

"明白了。然后我们将开始在 index 中使用这个设置,如下面的代码所示:"

import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import App from './containers/App'
import configureStore from './store/configureStore'

const store = configureStore()

render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

"来自react-redux模块的Provider组件允许我们将组件连接到存储。它接受一个我们设置的初始存储状态。Provider组件使这个存储对连接到存储的组件可用。这就是我们在上一个文件中通过连接到存储所做的事情。"

"明白了。这就是Redux 作为一个单一存储出现的地方,对吧?我注意到我们有一个完整的应用程序将要使用的单一存储。"

"是的。"

"最后,我们将通过定义传递给<Provider>标签的存储来完成我们的设置,如下所示:"

import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import reducer from '../reducers'

const createStoreWithMiddleware = applyMiddleware(
  thunk
)(createStore)

export default function configureStore(initialState) {
  const store = createStoreWithMiddleware(reducer, initialState)

  if (module.hot) {
    // Enable Webpack hot module replacement for reducers
    module.hot.accept('../reducers', () => {
      const nextReducer = require('../reducers')
      store.replaceReducer(nextReducer)
    })
  }

  return store
}

"再次,设置存储需要执行不同的操作,让我们逐一来看:"

  • createStore:这为我们创建一个存储,以表示应用程序的完整状态树。它接受参数——reducer(我们很快将看到的 reducer)和存储的初始状态。

  • applyMiddleware:这用于增强 Redux 以使用中间件。在这里,我们使用 thunk 中间件,它允许我们进行异步分发。

  • configureStore:最后,在configureStore中,我们通过调用增强的createStorecreateStoreWithMiddleware来创建存储。我们这里有一些条件来处理热模块替换以自动重新加载代码更改,这是我们之前在 HMR 中看到的。

"明白了。"

"接下来,让我们看看以下操作:"

import JSONUtil from '../utils/jsonutil'
import ArrayUtil from '../utils/array'

export const FILTER_BY_TWEETS = 'FILTER_BY_TWEETS';
export const FILTER_BY_REDDITS = 'FILTER_BY_REDDITS';
export const SYNC_TWEETS = 'SYNC_TWEETS';
export const SYNC_REDDITS = 'SYNC_REDDITS';

export function filterTweets(event) {
  return {
    type: FILTER_BY_TWEETS,
    showTweets: event.target.checked
  }
}

export function filterReddits(event) {
  return {
    type: FILTER_BY_REDDITS,
    showReddits: event.target.checked
  }
}

export function syncTweets(json) {
  return {
    type: SYNC_TWEETS,
    tweets: json.map((tweet) => {
      return {...tweet, type: 'tweet'}
    }),
    receivedAt: Date.now()
  }
}

export function syncReddits(json) {
  return {
    type: SYNC_REDDITS,
    reddits: json.data.children.map((child) => {
      return {...child.data, type: 'reddit'}
    }),
    receivedAt: Date.now()
  }
}

export function fetchTweets(username) {
  return dispatch => {
    fetch(`/tweets.json?username=${username}`)
        .then(JSONUtil.parseJSON)
        .then(json => dispatch(syncTweets(json))).catch(JSONUtil.handleParseException)
  }
}

export function fetchReddits(topic) {
  return dispatch => {
    fetch(`https://www.reddit.com/r/${topic}.json`)
        .then(JSONUtil.parseJSON)
        .then(json => dispatch(syncReddits(json))).catch(JSONUtil.handleParseException)
  }
}

"我们正在导入以下代码:"

import JSONUtil from '../utils/jsonutil'
import ArrayUtil from '../utils/array'

"与之前一样,JSONUtilArrayUtil类。我已经将它们移动到使用类而不是模块。"

"ArrayUtil类的代码如下:"

class ArrayUtil {
  static in_groups_of(arr, n) {
    var ret = [];
    var group = [];
    var len = arr.length;
    for (var i = 0; i < len; ++i) {
      group.push(arr[i]);
      if ((i + 1) % n == 0) {
        ret.push(group);
        group = [];
      }
    }
    if (group.length) ret.push(group);
    return ret;
  };
}

export {ArrayUtil as default};

"JSONUtil类的代码如下:"

class JSONUtil{
  static parseJSON(response){
    return response.json()
  }

  static handleParseException(ex) {
    console.log('parsing failed', ex)
  }
}

export { JSONUtil as default }

"现在,我们将定义动作作为常量,而不是之前定义的动作对象,我们将跨命令引用它们。"

export const FILTER_BY_TWEETS = 'FILTER_BY_TWEETS';
export const FILTER_BY_REDDITS = 'FILTER_BY_REDDITS';
export const SYNC_TWEETS = 'SYNC_TWEETS';
export const SYNC_REDDITS = 'SYNC_REDDITS';

"对于其他方法,我们简单地定义方法如下:"

export function filterTweets(event) {
  return {
    type: FILTER_BY_TWEETS,
    showTweets: event.target.checked
  }
}

"类似于我们之前的实现,我们包装并返回将用于 reducer 以突变存储的负载。"

"在从 API 获取数据的情况下,我们将实际的调用包装在dispatch中,如下所示:"

export function fetchTweets(username) {
  return dispatch => {
    fetch(`/tweets.json?username=${username}`)
        .then(JSONUtil.parseJSON)
        .then(json => dispatch(syncTweets(json))).catch(JSONUtil.handleParseException)
  }
}

"在这里,我们以不同的方式分发方法,它们将在结果返回时被链式调用和调用。正如我们之前看到的,当我们从SocialActions调用以下方法,我们将其包装在 dispatch 调用中以通知存储时:"

bindActionCreators(SocialActions, dispatch)

"在先前的方法中,因为它默认没有包装,所以我们将fetchTweets中的方法包装在dispatch()调用中。我们还将以下代码包装起来:"

dispatch(syncTweets(json))

"在收到响应后,我们将调用 syncTweets,它也会通知 Redux 存储。"

"明白了。接下来,我们应该看到 reducer,我想?"

"是的,让我们看看它:"

import { FILTER_BY_TWEETS, FILTER_BY_REDDITS, SYNC_REDDITS, SYNC_TWEETS } from '../actions/social'
import _ from 'underscore'

const mergeFeed = (tweets = [], reddits = [], showTweets = true, showReddits = true) => {
  let mergedFeed = []
  mergedFeed = showTweets ? mergedFeed.concat(tweets) : mergedFeed;
  mergedFeed = showReddits ? mergedFeed.concat(reddits) : mergedFeed;

  mergedFeed = _.sortBy(mergedFeed, (feedItem) => {
    if (feedItem.type == 'tweet') {
      let date = new Date(feedItem.created_at);
      return date.getTime();
    } else if ((feedItem.type == 'reddit')) {
      return feedItem.created_utc * 1000;
    }
  })
  return mergedFeed;
};

export default function social(state = {
  tweets: [],
  reddits: [],
  feed: [],
  showTweets: true,
  showReddits: true
}, action) {
  switch (action.type) {
    case FILTER_BY_TWEETS:
      return {...state, showTweets: action.showTweets, feed: mergeFeed(state.tweets, state.reddits, action.showTweets, state.showReddits)};
    case FILTER_BY_REDDITS:
      return {...state, showReddits: action.showReddits, feed: mergeFeed(state.tweets, state.reddits, state.showTweets, action.showReddits)};
    case SYNC_TWEETS:
      return {...state, tweets: action.tweets, feed: mergeFeed(action.tweets, state.reddits, state.showTweets, state.showReddits)};
    case SYNC_REDDITS:
      return {...state, reddits: action.reddits, feed: mergeFeed(state.tweets, action.reddits,  state.showTweets, state.showReddits)}
    default:
      return state
  }
}

"我们之前已经看到了mergeFeed。类似于迁移到类中,我将实现迁移到了 ES6。确定 feed 的逻辑与之前相同,我们将接受 Twitter 和 Reddit 的 feed 以及showReddit/showTwitter标志来决定如何构建 feed。"

"现在,特殊的方法如下:"

export default function social(state = {
  tweets: [],
  reddits: [],
  feed: [],
  showTweets: true,
  showReddits: true
}, action) 

"reducer 在动作分发时被调用。它接收状态中的上一个状态和在动作中的动作负载。如您所见,状态有一个默认值。"

"现在,根据动作负载,我们将确定需要使用数据运行什么,就像我们之前做的那样:"

switch (action.type) {
    case FILTER_BY_TWEETS:
      return {...state, showTweets: action.showTweets, feed: mergeFeed(state.tweets, state.reddits, action.showTweets, state.showReddits)};
  …
}

"这里的区别是我们没有直接突变状态。根据之前的状态,我们基于动作类型合并了之前和当前的计算状态,并返回它。"

"这是现在应用程序的当前状态。"

"明白了,我相信。我们现在只剩下应用程序了。"

"是的,让我们看看它会如何。我已经改为使用类。"

class SocialTracker extends Component {
  constructor() {
    super();
    this.state = {twitter: 'twitter', reddit: 'twitter'}
  }
  componentDidMount() {
    this.syncFeed();
  }
  render() {
   let {filterTweets, filterReddits} = this.props;
    let {showTweets, showReddits} = this.props.social;
    return (
        <Grid className="grid">
          <Row>
            <Jumbotron className="center-text">
              <h1>Social Media Tracker</h1>
            </Jumbotron>
          </Row>
          <Row>
            <Col xs={8} md={8} mdOffset={2}>
              <Table striped  hover>
                <thead>
                <tr>
                  <th width='200'>Feed Type</th>
                  <th>Feed Source</th>
                </tr>
                </thead>
                <tbody>
                <tr>
                  <td><Input id='test' type="checkbox" label="Twitter" onChange={filterTweets} checked={showTweets}/></td>
                  <td><Input onChange={::this.changeTwitterSource} type="text" addonBefore="@" value={this.state.twitter}/></td>
                </tr>
                <tr>
                  <th><Input type="checkbox" label="Reddit" onChange={filterReddits} checked={showReddits}/></th>
                  <td><Input onChange={::this.changeRedditSource} type="text" addonBefore="@" value={this.state.twitter}/></td>
                </tr>
                <tr>
                  <th></th>
                  <td><Button bsStyle="primary" bsSize="large" onClick={::this.syncFeed}>Sync Feed</Button>
                  </td>
                </tr>
                </tbody>
              </Table>
            </Col>
          </Row>
          {this.renderFeed()}
        </Grid>
    )
  }

  changeTwitterSource(event) {
    this.setState({twitter: event.target.value});
  }

  changeRedditSource(event) {
    this.setState({reddit: event.target.value});
  }

  syncFeed() {
    const { fetchTweets, fetchReddits } = this.props;
    fetchReddits(this.state.reddit);
    fetchTweets(this.state.twitter);
    console.log('syncFeed was called');
  }

  renderFeed() {
    let {feed} = this.props.social;
    let feedCollection = ArrayUtil.in_groups_of(feed, 3);
    if (feed.length > 0) {
      return feedCollection.map((feedGroup, index) => {
        return <Row key={`${feedGroup[0].id}${index}`}>
          {feedGroup.map((feed) => {
            if (feed.type == 'tweet') {
              return <Col md={4} key={feed.id}><div className="well twitter"><p>{feed.text}</p></div></Col>;
            } else {
              let display = feed.selftext == "" ? `${feed.title}: ${feed.url}` : feed.selftext;
              return <Col md={4} key={feed.id}><div className="well reddit"><p>{display}</p></div></Col>;
            }

          })}
        </Row>
      });
    } else {
      return <div></div>
    }
  }

}

export default SocialTracker

"所以,我们首先将本地状态设置为管理 Twitter 用户和 Reddit 用户,如下所示:"

constructor() {
    super();
    this.state = {twitter: 'twitter', reddit: 'twitter'}
  }

"在render方法中,我们获取 Redux 传递给组件作为 props 的值(即存储),以便显示:"

    let {filterTweets, filterReddits} = this.props;
    let {showTweets, showReddits} = this.props.social;

"现在,如果你还记得以下内容:"

function mapStateToProps(state) {
  return {
    social: state.social
  }
}

"我们将从 Redux 将状态转换为传递社交对象存储作为 props 到组件。然后我们从社交 prop 值获取诸如showTweetsshowReddits等值。"

"同样,我们有以下代码:"

function mapDispatchToProps(dispatch) {
  return bindActionCreators(SocialActions, dispatch)
}

"这会将动作转换为 props 并传递下去。我们接收它们作为filterTweetsfilterReddits在 props 上。然后我们将这些动作作为onclick事件处理器,如下所示:"

<Input id='test' type="checkbox" label="Twitter" onChange={filterTweets} checked={showTweets}/>

"最后,我们通过从相同的 props 中获取值来显示数据源本身:"

renderFeed() {
    let {feed} = this.props.social;
    let feedCollection = ArrayUtil.in_groups_of(feed, 3);
    if (feed.length > 0) {
      return feedCollection.map((feedGroup, index) => {
        console.log(feedGroup);
        return <Row key={`${feedGroup[0].id}${index}`}>
          {feedGroup.map((feed) => {
            if (feed.type == 'tweet') {
              return <Col md={4} key={feed.id}><div className="well twitter"><p>{feed.text}</p></div></Col>;
            } else {
              let display = feed.selftext == "" ? `${feed.title}: ${feed.url}` : feed.selftext;
              return <Col md={4} key={feed.id}><div className="well reddit"><p>{display}</p></div></Col>;
            }

          })}
        </Row>
      });
    } else {
      return <div></div>
    }
  }

"我们将从传递给我们的社交 prop 中获取数据源,如下:"

    let {feed} = this.props.social;

"最后,为了同步内容,我们有以下代码:"

  syncFeed() {
    const { fetchTweets, fetchReddits } = this.props;
    fetchReddits(this.state.reddit);
    fetchTweets(this.state.twitter);
    console.log('syncFeed was called');
  }

"太棒了。我想,有了这个,我们就完成了!"

"是的。你想回顾一下设置过程吗?"

"当然。我们首先设置了我们想要将 actions 和 stores 映射到传递给组件的 props 的方式。然后我们设置了 store 并将其连接到组件。"

"为了设置 store,我们使用了并应用了 thunk 中间件模块来增强 Redux,以便我们可以异步地分发 actions。"

"然后我们创建了从组件中调用的 actions,并将有效载荷和动作类型包装起来,以便发送到 store。"

"我们还创建了 reducers——社交 reducer——来实际操作、创建并返回新的 Redux 状态。"

"没错!让我们看看它看起来怎么样,好吗?"

设置 Redux

"太棒了!卡拉一定会喜欢这个的。"

摘要

我们查看了一下使用 Redux 和设置它的方法。我们看到了它与之前看到的纯 Flux 实现的不同之处。我们查看了一下 Redux 的不同组件——它的 stores、actions 以及用于 stores 和 actions 的 reducers。最后,我们看到了应用如何与 store 连接,以及我们如何通过 props 使用 actions 和数据。

posted @ 2025-09-08 13:02  绝不原创的飞龙  阅读(15)  评论(0)    收藏  举报