React-和-ReactNative-第二版-全-

React 和 ReactNative 第二版(全)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

我从来没有对开发移动应用程序感兴趣。我曾坚信这是网络,或者什么都不是,已经有太多应用程序安装在设备上了,没有必要再安装更多的应用程序。然后 React Native 出现了。我已经在为 Web 应用程序编写 React 代码并且喜欢它。事实证明,我并不是唯一一个对使用不同的工具、环境和编程语言来维护同一个应用程序的想法感到犹豫的开发人员。React Native 的诞生是出于自然的愿望,即将 Web 开发体验中表现良好的东西(React),应用到原生应用程序开发中。原生移动应用程序提供比 Web 浏览器更好的用户体验。事实证明我错了,我们现在确实需要移动应用程序。但这没关系,因为 React Native 是一个很棒的工具。这本书基本上是我作为 Web 上的 React 开发人员和作为一个经验较少的移动应用程序开发人员的经验。React Native 旨在让已经了解 Web 上的 React 的开发人员轻松过渡。通过这本书,你将学习在两种环境中进行 React 开发的微妙之处。你还将学习 React 的概念主题,一个可以针对任何东西的简单渲染抽象。今天,它是 Web 浏览器和移动设备。明天,它可能是任何东西。

这本书的第二版是为了应对不断发展的 React 项目而写的 - 包括实施 React 组件的最新最佳实践以及围绕 React 的生态系统。我认为,React 开发人员重要的是要了解 React 的工作原理,以及 React 的实现如何改变以更好地支持依赖它的人。在这一版的 React 和 React Native 中,我尽力捕捉了 React 今天的本质和它的发展方向。

这本书适合谁

这本书是为任何想要开始学习如何使用 Facebook 的两个 UI 库的 JavaScript 开发人员 - 无论是初学者还是专家编写的。不需要了解 React,但对 ES2017 的工作知识会帮助你更好地跟上。

第一部分:React

第一章,为什么选择 React?,介绍了 React 的基本知识以及为什么你想要使用它。

第二章,“使用 JSX 进行渲染”,解释了 JSX 是 React 用于渲染内容的语法。HTML 是最常见的输出,但 JSX 也可以用于渲染许多其他内容,例如原生 UI 组件。

第三章,“组件属性,状态和上下文”,展示了属性如何传递给组件,状态在更改时如何重新渲染组件,以及上下文在组件中的作用。

第四章,“事件处理-React 方式”,解释了 React 中的事件是在 JSX 中指定的。React 处理事件的方式有微妙之处,以及您的代码应该如何响应它们。

第五章,“打造可重用组件”,显示组件通常是使用较小的组件组合而成的。这意味着您必须正确地将数据和行为传递给子组件。

第六章,“React 组件生命周期”,解释了 React 组件是如何不断创建和销毁的。在此期间还有其他几个生命周期事件,您可以在其中执行诸如从网络获取数据之类的操作。

第七章,“验证组件属性”,展示了 React 具有一种机制,允许您验证传递给组件的属性类型。这确保了没有意外的值传递给您的组件。

第八章,“扩展组件”,介绍了用于扩展 React 组件的机制。这包括继承和高阶组件。

第九章,“使用路由处理导航”,解释了导航是任何 Web 应用程序的重要部分。React 使用react-router包以声明方式处理路由。

第十章,“服务器端 React 组件”,讨论了当在浏览器中渲染时,React 如何将组件呈现到 DOM 中。它还可以将组件呈现为字符串,这对于在服务器上呈现页面并将静态内容发送到浏览器非常有用。

第十一章,移动优先 React 组件,解释了移动 Web 应用程序与为桌面屏幕分辨率设计的 Web 应用程序在根本上是不同的。react-bootstrap包可用于以移动优先的方式构建 UI。

第二部分:React Native

第十二章,为什么选择 React Native?,显示 React Native 是用于移动应用程序的 React。如果您已经投资于 Web 应用程序的 React,为什么不利用相同的技术提供更好的移动体验呢?

第十三章,启动 React Native 项目,讨论了没有人喜欢编写样板代码或设置项目目录。React Native 有工具来自动化这些单调的任务。

第十四章,使用 Flexbox 构建响应式布局,解释了为什么 Flexbox 布局模型在使用 CSS 的 Web UI 布局中很受欢迎。React Native 使用相同的机制来布局屏幕。

第十五章,在屏幕之间导航,讨论了导航是 Web 应用程序的重要部分,移动应用程序也需要工具来处理用户如何从一个屏幕移动到另一个屏幕。

第十六章,渲染项目列表,显示 React Native 有一个列表视图组件,非常适合渲染项目列表。您只需提供数据源,它就会处理剩下的事情。

第十七章,显示进度,解释了进度条非常适合显示确定数量的进度。当您不知道某事会花费多长时间时,您可以使用进度指示器。React Native 具有这两个组件。

第十八章,地理位置和地图,显示了react-native-maps包为 React Native 提供了地图功能。在 Web 应用程序中使用的地理位置 API 直接由 React Native 提供。

第十九章,收集用户输入,显示大多数应用程序需要从用户那里收集输入。移动应用程序也不例外,React Native 提供了各种控件,与 HTML 表单元素类似。

第二十章,警报、通知和确认,解释了警报用于打断用户,让他们知道发生了重要的事情,通知是不显眼的更新,确认用于立即获得答案。

第二十一章,响应用户手势,讨论了移动设备上的手势在浏览器中很难做到正确。另一方面,原生应用程序为滑动、触摸等提供了更好的体验。React Native 为你处理了很多细节。

第二十二章,控制图像显示,展示了图像在大多数应用程序中扮演着重要角色,无论是作为图标、标志还是物品的照片。React Native 具有加载图像、缩放图像和适当放置图像的工具。

第二十三章,离线操作,解释了移动设备往往具有不稳定的网络连接。因此,移动应用程序需要能够处理临时的离线条件。为此,React Native 具有本地存储 API。

第三部分:React 架构

第二十四章,处理应用程序状态,讨论了应用程序状态对于任何 React 应用程序,无论是 Web 还是移动应用程序都很重要。这就是为什么理解 Redux 和 Immutable.js 等库很重要。

第二十五章,为什么使用 Relay 和 GraphQL?解释了 Relay 和 GraphQL 结合使用是一种处理规模化状态的新方法。它是一个查询和变异语言,以及一个用于包装 React 组件的库。

第二十六章,构建 Relay React 应用程序,显示了 Relay 和 GraphQL 的真正优势在于你的状态模式在应用程序的 Web 和原生版本之间是共享的。

为了充分利用本书

  1. 告知读者在开始之前需要了解的事情,并明确你所假设的知识。

  2. 任何额外的安装说明和他们设置所需的信息。

  • 一个代码编辑器

  • 一个现代的网络浏览器

  • NodeJS

下载示例代码文件

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

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

  1. 登录或注册www.packt.com

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

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

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

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

  • Windows 系统使用 WinRAR/7-Zip

  • Mac 系统使用 Zipeg/iZip/UnRarX

  • Linux 系统使用 7-Zip/PeaZip

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

我们还有其他代码包,可以从我们丰富的图书和视频目录中获得,网址为github.com/PacktPublishing/。请查看!

使用的约定

本书中使用了许多文本约定。

CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名。例如:“将下载的WebStorm-10*.dmg磁盘映像文件挂载为系统中的另一个磁盘。”

代码块设置如下:

import React, { Component } from 'react';
// Renders a "<button>" element, using
// "this.props.children" as the text.
export default class MyButton extends Component {
  render() {
    return <button>{this.props.children}</button>;
  }
}

任何命令行输入或输出都以以下方式编写:

$ npm install -g create-react-native-app $ create-react-native-app my-project 

粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词会在文本中以这种方式出现。例如:“从管理面板中选择系统信息。”

警告或重要说明会以这种方式出现。提示和技巧会以这种方式出现。

第一章:为什么要使用 React?

如果你正在阅读这本书,你可能已经对 React 有一些想法。你可能也听过一两个 React 的成功故事。如果没有,不用担心。我会尽力在本章节中避免让你接触到额外的营销文学。然而,这是一本内容丰富的书,所以我觉得设定基调是一个合适的第一步。是的,目标是学习 React 和 React Native。但同时也是为了构建一个持久的架构,可以处理我们今天和未来想要用 React 构建的一切。

本章以 React 存在的简要解释开始。然后,我们将讨论使 React 成为一种吸引人的技术的简单性,以及 React 如何能够处理 Web 开发人员面临的许多典型性能问题。接下来,我们将介绍 React 的声明性哲学以及 React 程序员可以期望使用的抽象级别。最后,我们将介绍 React 16 的一些主要新功能。

让我们开始吧!

什么是 React?

我认为 React 在其主页上的一行描述(facebook.github.io/react))非常出色:

“用于构建用户界面的 JavaScript 库。”

这是一个用于构建用户界面的库。这很完美,因为事实证明,这正是我们大多数时候想要的。我认为这个描述最好的部分是它所省略的一切。它不是一个大型框架。它不是一个从数据库到实时更新的 Web 套接字连接处理一切的全栈解决方案。实际上,我们并不想要大多数这些预打包的解决方案,因为最终它们通常会带来更多问题而不是解决问题。

React 只是视图

React 通常被认为是应用程序中的视图层。你可能以前使用过类似 Handlebars 或 jQuery 的库。就像 jQuery 操作 UI 元素,或者 Handlebars 模板被插入到页面上一样,React 组件改变了用户所看到的内容。下面的图表说明了 React 在我们前端代码中的位置:

这就是 React 的全部核心概念。当然,在我们阅读本书的过程中,这个主题可能会有一些微妙的变化,但流程基本上是一样的。我们有一些应用逻辑生成一些数据。我们想要将这些数据渲染到 UI 上,所以我们将其传递给一个 React 组件,它负责将 HTML 放入页面中。

也许你会想知道这有什么大不了的,特别是因为在表面上,React 似乎只是另一种渲染技术。在本章的其余部分,我们将涉及 React 可以简化应用程序开发的一些关键领域。

简单就是好

React 并没有太多需要学习和理解的部分。在内部,有很多事情正在发生,我们将在本书中逐渐涉及这些事情。与大型框架相比,拥有一个小的 API 可以让你花更多的时间熟悉它,进行实验等等。大型框架则相反,你需要花费大量时间来弄清楚所有东西是如何工作的。下图大致展示了我们在使用 React 编程时需要考虑的 API:

React 分为两个主要的 API。首先是 React DOM。这是用于在网页上执行实际渲染的 API。其次是 React 组件 API。这些是实际由 React DOM 渲染的页面的部分。在 React 组件中,我们需要考虑以下几个方面:

  • 数据:这是来自某处的数据(组件不关心来自哪里),并由组件渲染。

  • 生命周期:这些是我们实现的方法,用于响应组件生命周期的变化。例如,组件即将被渲染。

  • 事件:这是我们编写的用于响应用户交互的代码。

  • JSX:这是 React 组件的语法,用于描述 UI 结构。

暂时不要过于专注于 React API 的这些不同领域代表什么。这里要记住的是,React 本质上是简单的。看看需要弄清楚的东西是多么少!这意味着我们不必在这里花费大量时间去了解 API 的细节。相反,一旦掌握了基础知识,我们可以花更多时间来研究 React 的微妙用法模式。

声明式 UI 结构

React 新手很难接受组件将标记与 JavaScript 混合在一起的想法。如果您看过 React 示例并有相同的不良反应,不要担心。最初,我们都对这种方法持怀疑态度,我认为原因是我们几十年来一直被关注分离原则所影响。现在,每当我们看到事物混合在一起,我们自动假设这是不好的,不应该发生。

React 组件使用的语法称为JSXJavaScript XML)。组件通过返回一些 JSX 来呈现内容。JSX 本身通常是 HTML 标记,混合了用于 React 组件的自定义标记。在这一点上具体细节并不重要;我们将在接下来的章节中详细讨论。这里绝对突破性的是,我们不必执行微操作来改变组件的内容。

虽然我在本书中不会遵循惯例,但一些 React 开发人员更喜欢使用.jsx扩展名而不是.js来命名他们的组件。

例如,想想使用类似 jQuery 来构建应用程序。您有一个页面上有一些内容,当单击按钮时,您想向段落添加一个类。执行这些步骤足够简单。这被称为命令式编程,对 UI 开发来说是有问题的。虽然在响应事件时更改元素的类的这个例子很简单,但实际应用程序往往涉及超过三四个步骤才能实现某些事情。

React 组件不需要以命令式的方式执行步骤来呈现内容。这就是为什么 JSX 对于 React 组件如此重要的原因。XML 风格的语法使得描述 UI 应该是什么样子变得容易。也就是说,这个组件将呈现哪些 HTML 元素?这被称为声明式编程,非常适合 UI 开发。

时间和数据

React 新手难以理解的另一个领域是 JSX 就像一个静态字符串,代表了一块渲染输出。这就是时间和数据发挥作用的地方。React 组件依赖于传递给它们的数据。这些数据代表了 UI 的动态方面。例如,基于布尔值呈现的 UI 元素可能会在下次组件渲染时发生变化。这里是这个想法的一个例证:

每次渲染 React 组件时,就像在那个确切的时间点拍摄 JSX 的快照。随着应用程序随时间向前推进,您将拥有一个有序的渲染用户界面组件的集合。除了声明性地描述 UI 应该是什么之外,重新渲染相同的 JSX 内容对开发人员来说更加容易。挑战在于确保 React 能够处理这种方法的性能要求。

性能很重要

使用 React 构建用户界面意味着我们可以使用 JSX 声明 UI 的结构。这比逐个组装 UI 的命令式方法更不容易出错。然而,声明性方法确实给我们带来了一个挑战:性能。

例如,具有声明性 UI 结构对于初始渲染是可以的,因为页面上还没有任何内容。因此,React 渲染器可以查看 JSX 中声明的结构,并将其呈现到 DOM 浏览器中。

DOM代表文档对象模型,表示在浏览器中呈现后的 HTML。DOM API 是 JavaScript 能够更改页面上内容的方式。

这个概念在下图中有所说明:

在初始渲染时,React 组件及其 JSX 与其他模板库没有区别。例如,Handlebars 将模板呈现为 HTML 标记作为字符串,然后插入到浏览器 DOM 中。React 与诸如 Handlebars 之类的库不同之处在于数据发生变化时,我们需要重新渲染组件。Handlebars 将重新构建整个 HTML 字符串,就像在初始渲染时所做的那样。由于这对性能有问题,我们经常需要实现命令式的解决方法,手动更新 DOM 的一小部分。我们最终会得到一堆混乱的声明性模板和命令式代码来处理 UI 的动态方面。

在 React 中我们不这样做。这就是 React 与其他视图库不同的地方。组件在初始渲染时是声明性的,并且即使在重新渲染时也保持这种状态。React 在幕后所做的工作使得重新渲染声明性 UI 结构成为可能。

React 有一个叫做虚拟 DOM的东西,用于在内存中保持对真实 DOM 元素的表示。它这样做是为了每次重新渲染组件时,它可以比较新内容和已经显示在页面上的内容。根据差异,虚拟 DOM 可以执行必要的命令步骤来进行更改。因此,当我们需要更新 UI 时,我们不仅可以保留我们的声明式代码,React 还会确保以高效的方式完成。这个过程看起来是这样的:

当你阅读关于 React 的内容时,你经常会看到诸如diffingpatching之类的词语。Diffing 意味着比较旧内容和新内容,以找出发生了什么变化。Patching 意味着执行必要的 DOM 操作来渲染新内容。

和任何其他 JavaScript 库一样,React 受到主线程运行完成性质的限制。例如,如果 React 内部正在忙于 diffing 内容和 patching DOM,浏览器就无法响应用户输入。正如你将在本章的最后一节中看到的,React 16 对内部渲染算法进行了更改,以减轻这些性能缺陷。

适当的抽象水平

在我们深入研究 React 代码之前,我想以高层次来讨论另一个主题,即抽象。React 并没有太多抽象,但 React 实现的抽象对其成功至关重要。

在前面的部分中,你看到了 JSX 语法如何转换为我们不感兴趣的低级操作。观察 React 如何转换我们的声明式 UI 组件的更重要的方式是,我们并不一定关心渲染目标是什么。渲染目标恰好是浏览器 DOM,但它并不局限于浏览器 DOM。

React 有潜力用于我们想要创建的任何用户界面,可以在任何可想象的设备上使用。我们只是刚刚开始在 React Native 中看到这一点,但可能性是无限的。当 React Toast 成为一种事物时,我个人不会感到惊讶,它可以将 JSX 的渲染输出烤到面包上。React 的抽象水平正好,而且位置合适。

以下图表让你了解 React 可以针对的不仅仅是浏览器:

从左到右,我们有 React Web(纯粹的 React)、React Native、React Desktop 和 React Toast。正如你所看到的,为了针对新的目标,同样的模式适用:

  • 实现特定于目标的组件

  • 实现一个可以在底层执行特定于平台的操作的 React 渲染器

  • 利润

这显然是对任何给定的 React 环境实际实现的过度简化。但这些细节对我们来说并不那么重要。重要的是,我们可以利用我们的 React 知识来专注于描述任何平台上用户界面的结构。

不幸的是,React Toast 可能永远不会成为一种东西。

React 16 的新功能

在这一部分,我想强调 React 16 的主要变化和新功能。随着我们在整本书中遇到这些变化,我将更详细地介绍这些变化。

核心架构改进

React 16 中最大的变化可能是内部协调代码。这些变化不会影响您与 React API 交互的方式。相反,这些变化是为了解决一些痛点,这些痛点阻碍了 React 在某些情况下的扩展。例如,这个新架构的主要概念之一是 fiber。React 不再以运行到编译的方式渲染页面上的每个组件,而是渲染 fiber - 页面的较小块,可以优先级和异步渲染。

要更深入地了解这种新架构,这些资源应该会有所帮助:

生命周期方法

React 16 必须重新设计一些可用于类组件的生命周期方法。一些生命周期方法已被弃用,并最终将被移除。有新的生命周期方法来替换它们。主要问题是,弃用的生命周期方法鼓励以一种与新的异步 React 核心不兼容的方式编码。

有关这些生命周期方法的更多信息,请访问此页面:reactjs.org/blog/2018/03/27/update-on-async-rendering.html

上下文 API

React 一直为开发人员提供上下文 API,但它一直被视为实验性的。上下文是将数据从一个组件传递到下一个组件的替代方法。例如,使用属性,您可以通过多层组件树传递数据。这个树中间的组件实际上并不使用任何这些属性,它们只是充当中间人。随着应用程序的增长,这变得有问题,因为您的源代码中有很多属性,增加了复杂性。

React 16.3 中的新上下文 API 更加官方,并提供了一种方法,让您在任何树级别为组件提供数据。您可以在这里阅读有关新上下文 API 的更多信息:reactjs.org/docs/context.html

渲染片段

如果您的 React 组件呈现了几个兄弟元素,例如三个<p>元素,您将不得不将它们包装在<div>中,因为 React 只允许组件返回单个元素。这种方法的唯一问题是它会导致大量不必要的 DOM 结构。使用<Fragment>包装您的元素与使用<div>包装它们的想法是一样的,只是不会有多余的 DOM 元素。

您可以在这里阅读更多关于片段的信息:reactjs.org/docs/fragments.html

门户

当 React 组件返回内容时,它会被渲染到其父组件中。然后,父级的内容被渲染到其父组件中,依此类推,一直到树根。有时,您希望渲染的内容专门针对 DOM 元素。例如,应该将其呈现为对话框的组件可能不需要挂载到父级。使用门户,您可以控制组件内容的具体渲染位置。

您可以在这里阅读更多关于门户的信息:reactjs.org/docs/portals.html

渲染列表和字符串

在 React 16 之前,组件必须返回 HTML 元素或另一个 React 组件作为其内容。这可能会限制您如何组合应用程序。例如,您可能有一个负责生成错误消息的组件。以前,您必须将这些字符串包装在 HTML 标记中,以被视为有效的 React 组件输出。现在您可以直接返回字符串。同样,您可以直接返回字符串列表或元素列表。

介绍 React 16 的博客文章中有关于这个新功能的更多细节:reactjs.org/blog/2017/09/26/react-v16.0.html

处理错误

在 React 中处理错误可能很困难。到底在哪里处理错误?如果一个组件处理 JavaScript 异常并将组件的错误状态设置为 true,那么如何重置这个状态?在 React 16 中,有错误边界。错误边界是通过在组件中实现componentDidCatch()生命周期方法来创建的。然后,这个组件可以作为错误边界来包装其他组件。如果任何被包装的组件抛出异常,错误边界组件可以渲染替代内容。

像这样设置错误边界可以让您以最适合您的应用程序的方式构建组件。您可以在这里阅读更多关于错误边界的信息:reactjs.org/docs/error-boundaries.html

服务器端渲染

在 React 中的服务器端渲染SSR)可能很难理解。你在服务器上渲染,然后在客户端上也渲染?由于 SSR 模式变得更加普遍,React 团队在 React 16 中使其更易于使用。此外,通过启用将渲染内容流式传输到客户端,还可以获得一些内部性能和效率方面的收益。

如果您想阅读更多关于 React 16 中的 SSR 的内容,我推荐以下资源:

摘要

在本章中,您以高层次介绍了 React。React 是一个库,具有一个小的 API,用于构建用户界面。接下来,您将介绍 React 的一些关键概念。首先,我们讨论了 React 之所以简单,因为它没有太多的移动部分。接下来,我们看了 React 组件和 JSX 的声明性质。然后,您了解到 React 认真对待性能,这就是我们能够编写可以一遍又一遍重新渲染的声明性代码的原因。接下来,您了解了渲染目标的概念,以及 React 如何轻松成为所有这些目标的首选 UI 工具。最后,我大致概述了 React 16 的新功能。

现在关于介绍和概念的内容就够了。当我们逐渐接近书的结尾时,我们将重新讨论这些想法。现在,让我们退一步,从 JSX 开始,打好基础。

测试您的知识

  1. 什么是声明式 UI 结构,React 如何支持这个想法?

  2. 声明式 UI 是由在使用之前声明的所有组件构建的。如果所有组件没有预先声明,React 将无法渲染。

  3. 声明式 UI 结构定义了 UI 组件是什么,而不用担心它是如何定义的。React 通过允许使用 JSX 语法声明组件来支持这个想法。

  4. 在 React 中,声明式 UI 结构是完全可选的。您也可以轻松地采用命令式方法。

  5. React 如何提高渲染性能?

  6. React 有一个虚拟 DOM,它在内存中比较组件数据的更改,尽量避免使用浏览器 DOM。React 16 有一个新的内部架构,允许将渲染分成更小的工作块并设置优先级。

  7. React 设置了 Web Workers,以便尽可能地并行处理工作。

  8. React 不专注于性能,而是依赖于增量浏览器性能改进。

  9. 何时会渲染一个片段?

  10. 当您需要在渲染的内容中使用占位符时,可以使用片段。

  11. 片段用于提高其子元素的性能。

  12. 片段用于避免渲染不必要的 DOM 元素。

进一步阅读

点击以下链接获取更多信息:

第二章:使用 JSX 渲染

本章将向您介绍 JSX。我们将从基础知识开始:什么是 JSX?然后,您会发现 JSX 内置支持 HTML 标记,正如您所期望的那样,所以我们将在这里运行一些示例。在查看了一些 JSX 代码之后,我们将讨论它如何使我们轻松描述 UI 的结构。然后,我们将开始构建我们自己的 JSX 元素,并使用 JavaScript 表达式进行动态内容。最后,您将学习如何使用片段来产生更少的 HTML——这是 React 16 的一个新功能。

准备好了吗?

什么是 JSX?

在这一部分,我们将实现义不容辞的你好世界JSX 应用程序。在这一点上,我们只是在试水;更深入的例子将会接下来。我们还将讨论什么使这种语法适合声明式 UI 结构。

你好 JSX

话不多说,这是你的第一个 JSX 应用程序:

// The "render()" function will render JSX markup and
// place the resulting content into a DOM node. The "React"
// object isn't explicitly used here, but it's used
// by the transpiled JSX source.
import React from 'react';
import { render } from 'react-dom';

// Renders the JSX markup. Notice the XML syntax
// mixed with JavaScript? This is replaced by the
// transpiler before it reaches the browser.
render(
 <p>
    Hello, <strong>JSX</strong>
  </p>,
  document.getElementById('root')
);

让我们来看看这里发生了什么。首先,我们需要导入相关的部分。render()函数是这个例子中真正重要的部分,因为它将 JSX 作为第一个参数并将其呈现到作为第二个参数传递的 DOM 节点上。

在这个例子中,实际的 JSX 内容呈现了一个段落,里面有一些加粗的文本。这里没有什么花哨的东西,所以我们可以直接将这个标记插入到 DOM 中作为普通字符串。然而,JSX 比这里展示的更复杂。这个例子的目的是展示将 JSX 呈现到页面上所涉及的基本步骤。现在,让我们稍微谈一下声明式 UI 结构。

JSX 被转译成 JavaScript 语句;浏览器不知道 JSX 是什么。我强烈建议您从github.com/PacktPublishing/React-and-React-Native-Second-Edition下载本书的配套代码,并在阅读时运行它。一切都会自动转译给您;您只需要遵循简单的安装步骤。

声明式 UI 结构

在我们继续进行代码示例之前,让我们花一点时间来反思我们的hello world示例。 JSX 内容简短而简单。它也是声明性的,因为它描述了要渲染的内容,而不是如何渲染它。具体来说,通过查看 JSX,您可以看到此组件将呈现一个段落,并在其中呈现一些粗体文本。如果这是以命令式方式完成的,可能会涉及一些更多的步骤,并且它们可能需要按特定顺序执行。

我们刚刚实施的示例应该让您了解声明性 React 的全部内容。随着我们在本章和整本书中的继续前进,JSX 标记将变得更加复杂。但是,它始终将描述用户界面中的内容。让我们继续。

就像 HTML 一样

归根结底,React 组件的工作是将 HTML 渲染到 DOM 浏览器中。这就是为什么 JSX 默认支持 HTML 标记。在本节中,我们将查看一些代码,用于渲染一些可用的 HTML 标记。然后,我们将介绍在 React 项目中使用 HTML 标记时通常遵循的一些约定。

内置 HTML 标记

当我们渲染 JSX 时,元素标记引用的是 React 组件。由于为 HTML 元素创建组件将是繁琐的,React 带有 HTML 组件。我们可以在我们的 JSX 中渲染任何 HTML 标记,输出将如我们所期望的那样。现在,让我们尝试渲染一些这些标记:

import React from 'react';
import { render } from 'react-dom';

// The render() function will only complain if the browser doesn't
// recognize the tag
render(
  <div>
    <button />
    <code />
    <input />
    <label />
    <p />
    <pre />
    <select />
    <table />
    <ul />
  </div>,
  document.getElementById('root')
);

不要担心此示例的渲染输出;这没有意义。我们在这里所做的一切只是确保我们可以渲染任意 HTML 标记,并且它们会按预期渲染。

你可能已经注意到周围的<div>标签,将所有其他标签分组为其子标签。这是因为 React 需要一个根组件来渲染。在本章的后面,你将学习如何渲染相邻的元素,而不需要将它们包装在父元素中。

HTML 标记约定

当您在 JSX 标记中渲染 HTML 标记时,期望是您将使用小写来表示标记名称。事实上,大写 HTML 标记的名称将失败。标记名称是区分大小写的,而非 HTML 元素是大写的。这样,很容易扫描标记并找到内置的 HTML 元素与其他所有内容。

您还可以传递 HTML 元素的任何标准属性。当您传递意外的内容时,将记录有关未知属性的警告。以下是一个说明这些想法的示例:

import React from 'react';
import { render } from 'react-dom';

// This renders as expected, except for the "foo"
// property, since this is not a recognized button
// property.
render(
  <button title="My Button" foo="bar">
    My Button
  </button>,
  document.getElementById('root')
);

// This fails with a "ReferenceError", because
// tag names are case-sensitive. This goes against
// the convention of using lower-case for HTML tag names.
render(<Button />, document.getElementById('root'));

在书的后面,我将介绍你制作的组件的属性验证。这可以避免类似于这个例子中foo属性的静默错误行为。

描述 UI 结构

JSX 是描述复杂 UI 结构的最佳方式。让我们看一些声明比单个段落更复杂结构的 JSX 标记:

import React from 'react';
import { render } from 'react-dom';

// This JSX markup describes some fairly-sophisticated
// markup. Yet, it's easy to read, because it's XML and
// XML is good for concisely-expressing hierarchical
// structure. This is how we want to think of our UI,
// when it needs to change, not as an individual element
// or property.
render(
  <section>
    <header>
      <h1>A Header</h1>
    </header>
    <nav>
      <a href="item">Nav Item</a>
    </nav>
    <main>
      <p>The main content...</p>
    </main>
    <footer>
      <small>&copy; 2018</small>
    </footer>
  </section>,
  document.getElementById('root')
);

正如你所看到的,在这个标记中有很多语义元素,描述了 UI 的结构。关键在于这种复杂结构很容易理解,我们不需要考虑渲染它的特定部分。但在我们开始实现动态 JSX 标记之前,让我们创建一些自己的 JSX 组件。

这是渲染的内容:

创建你自己的 JSX 元素

组件是 React 的基本构建块。事实上,组件是 JSX 标记的词汇。在本节中,我们将看到如何在组件中封装 HTML 标记。我们将构建示例,向你展示如何嵌套自定义 JSX 元素以及如何为你的组件命名空间。

封装 HTML

你想创建新的 JSX 元素的原因是为了封装更大的结构。这意味着你可以使用自定义标签,而不是输入复杂的标记。React 组件返回替换元素的 JSX。现在让我们看一个例子:

// We also need "Component" so that we can
// extend it and make a new JSX tag.
import React, { Component } from 'react';
import { render } from 'react-dom';

// "MyComponent" extends "Compoennt", which means that
// we can now use it in JSX markup.
class MyComponent extends Component {
  render() {
    // All components have a "render()" method, which
    // retunrns some JSX markup. In this case, "MyComponent"
    // encapsulates a larger HTML structure.
    return (
      <section>
        <h1>My Component</h1>
        <p>Content in my component...</p>
      </section>
    );
  }
}

// Now when we render "<MyComponent>" tags, the encapsulated
// HTML structure is actually rendered. These are the
// building blocks of our UI.
render(<MyComponent />, document.getElementById('root'));

这是渲染的输出:

这是你实现的第一个 React 组件,所以让我们花点时间来分析一下这里发生了什么。你创建了一个名为MyComponent的类,它继承自 React 的Component类。这是你创建一个新的 JSX 元素的方式。正如你在render()中看到的,你正在渲染一个<MyComponent>元素。

这个组件封装的 HTML 是由render()方法返回的。在这种情况下,当 JSX <MyComponent>react-dom渲染时,它被一个<section>元素替换,并且其中的所有内容。

当 React 渲染 JSX 时,你使用的任何自定义元素必须在同一个作用域内具有相应的 React 组件。在前面的例子中,MyComponent类在render()调用的同一个作用域中声明,所以一切都按预期工作。通常,你会导入组件,将它们添加到适当的作用域中。随着你在书中的进展,你会看到更多这样的情况。

嵌套元素

使用 JSX 标记有助于描述具有父子关系的 UI 结构。例如,<li>标记只有作为<ul><ol>标记的子标记才有用-您可能会使用自己的 React 组件创建类似的嵌套结构。为此,您需要使用children属性。让我们看看这是如何工作的。以下是 JSX 标记:

import React from 'react';
import { render } from 'react-dom';

// Imports our two components that render children...
import MySection from './MySection';
import MyButton from './MyButton';

// Renders the "MySection" element, which has a child
// component of "MyButton", which in turn has child text.
render(
  <MySection>
    <MyButton>My Button Text</MyButton>
  </MySection>,
  document.getElementById('root')
);

您正在导入两个自己的 React 组件:MySectionMyButton。现在,如果您查看 JSX 标记,您会注意到<MyButton><MySection>的子代。您还会注意到MyButton组件接受文本作为其子代,而不是更多的 JSX 元素。让我们看看这些组件是如何工作的,从MySection开始:

import React, { Component } from 'react';

// Renders a "<section>" element. The section has
// a heading element and this is followed by
// "this.props.children".
export default class MySection extends Component {
  render() {
    return (
      <section>
        <h2>My Section</h2>
        {this.props.children}
      </section>
    );
  }
}

这个组件呈现了一个标准的<section>HTML 元素,一个标题,然后是{this.props.children}。正是这个构造允许组件访问嵌套元素或文本,并将其呈现出来。

在前面的例子中使用的两个大括号用于 JavaScript 表达式。我将在下一节中详细介绍在 JSX 标记中找到的 JavaScript 表达式语法的更多细节。

现在,让我们看一下MyButton组件:

import React, { Component } from 'react';

// Renders a "<button>" element, using
// "this.props.children" as the text.
export default class MyButton extends Component {
  render() {
    return <button>{this.props.children}</button>;
  }
}

这个组件使用与MySection完全相同的模式;获取{this.props.children}的值,并用有意义的标记包围它。React 会为您处理混乱的细节。在这个例子中,按钮文本是MyButton的子代,而MyButton又是MySection的子代。但是,按钮文本是透明地通过MySection传递的。换句话说,我们不需要在MySection中编写任何代码来确保MyButton获得其文本。很酷,对吧?渲染输出如下所示:

命名空间组件

到目前为止,您创建的自定义元素都使用了简单的名称。有时,您可能希望给组件一个命名空间。在您的 JSX 标记中,您将写入<MyNamespace.MyComponent>而不是<MyComponent>。这样可以清楚地告诉任何人MyComponentMyNamespace的一部分。

通常,MyNamespace也将是一个组件。命名空间的想法是使用命名空间语法呈现其子组件。让我们来看一个例子:

import React from 'react';
import { render } from 'react-dom';

// We only need to import "MyComponent" since
// the "First" and "Second" components are part
// of this "namespace".
import MyComponent from './MyComponent';

// Now we can render "MyComponent" elements,
// and it's "namespaced" elements as children.
// We don't actually have to use the namespaced
// syntax here, we could import the "First" and
// "Second" components and render them without the
// "namespace" syntax. It's a matter of readability
// and personal taste.
render(
  <MyComponent>
    <MyComponent.First />
    <MyComponent.Second />
  </MyComponent>,
  document.getElementById('root')
);

这个标记呈现了一个带有两个子元素的<MyComponent>元素。关键在于,我们不是写<First>,而是写<MyComponent.First><MyComponent.Second>也是一样。这个想法是我们想要明确地显示FirstSecond属于MyComponent,在标记内部。

我个人不依赖于这样的命名空间组件,因为我宁愿通过查看模块顶部的import语句来看哪些组件正在使用。其他人可能更愿意导入一个组件,并在标记中明确标记关系。没有正确的做法;这是个人品味的问题。

现在,让我们来看一下MyComponent模块:

import React, { Component } from 'react';

// The "First" component, renders some basic JSX...
class First extends Component {
  render() {
    return <p>First...</p>;
  }
}

// The "Second" component, renders some basic JSX...
class Second extends Component {
  render() {
    return <p>Second...</p>;
  }
}

// The "MyComponent" component renders it's children
// in a "<section>" element.
class MyComponent extends Component {
  render() {
    return <section>{this.props.children}</section>;
  }
}

// Here is where we "namespace" the "First" and
// "Second" components, by assigning them to
// "MyComponent" as class properties. This is how
// other modules can render them as "<MyComponent.First>"
// elements.
MyComponent.First = First;
MyComponent.Second = Second;

export default MyComponent;

// This isn't actually necessary. If we want to be able
// to use the "First" and "Second" components independent
// of "MyComponent", we would leave this in. Otherwise,
// we would only export "MyComponent".
export { First, Second };

这个模块声明了MyComponent以及属于这个命名空间的其他组件(FirstSecond)。这个想法是将组件分配给命名空间组件(MyComponent)作为类属性。在这个模块中有很多可以改变的东西。例如,你不必直接导出FirstSecond,因为它们可以通过MyComponent访问。你也不需要在同一个模块中定义所有东西;你可以导入FirstSecond并将它们分配为类属性。使用命名空间是完全可选的,如果你使用它们,应该一致地使用它们。

使用 JavaScript 表达式

正如你在前面的部分中看到的,JSX 有特殊的语法,允许你嵌入 JavaScript 表达式。每当 React 渲染 JSX 内容时,标记中的表达式都会被评估。这是 JSX 的动态方面,在本节中,你将学习如何使用表达式来设置属性值和元素文本内容。你还将学习如何将数据集合映射到 JSX 元素。

动态属性值和文本

一些 HTML 属性或文本值是静态的,意味着它们在 JSX 重新渲染时不会改变。其他值,即属性或文本的值,是基于应用程序中其他地方找到的数据。记住,React 只是视图层。让我们看一个例子,这样你就可以感受一下在 JSX 标记中 JavaScript 表达式语法是什么样子的:

import React from 'react';
import { render } from 'react-dom';

// These constants are passed into the JSX
// markup using the JavaScript expression syntax.
const enabled = false;
const text = 'A Button';
const placeholder = 'input value...';
const size = 50;

// We're rendering a "<button>" and an "<input>"
// element, both of which use the "{}" JavaScript
// expression syntax to fill in property, and text
// values.
render(
  <section>
    <button disabled={!enabled}>{text}</button>
    <input placeholder={placeholder} size={size} />
  </section>,
  document.getElementById('root')
);

任何有效的 JavaScript 表达式,包括嵌套的 JSX,都可以放在大括号{}之间。对于属性和文本,这通常是一个变量名或对象属性。请注意,在这个例子中,!enabled表达式计算出一个布尔值。渲染输出如下所示:

如果你正在使用可下载的配套代码进行跟进,我强烈建议你这样做,尝试玩玩这些值,看看渲染的 HTML 如何改变。

将集合映射到元素

有时,你需要编写 JavaScript 表达式来改变你的标记结构。在前面的部分中,你学会了如何使用 JavaScript 表达式语法来动态改变 JSX 元素的属性值。那么当你需要根据 JavaScript 集合添加或删除元素时呢?

在整本书中,当我提到 JavaScript集合时,我指的是普通对象和数组。或者更一般地说,任何可迭代的东西。

动态控制 JSX 元素的最佳方式是从集合中映射它们。让我们看一个如何做到这一点的例子:

import React from 'react';
import { render } from 'react-dom';

// An array that we want to render as s list...
const array = ['First', 'Second', 'Third'];

// An object that we want to render as a list...
const object = {
  first: 1,
  second: 2,
  third: 3
};

render(
  <section>
    <h1>Array</h1>

    {/* Maps "array" to an array of "<li>"s.
         Note the "key" property on "<li>".
         This is necessary for performance reasons,
         and React will warn us if it's missing. */}
    <ul>{array.map(i => <li key={i}>{i}</li>)}</ul>
    <h1>Object</h1>

    {/* Maps "object" to an array of "<li>"s.
         Note that we have to use "Object.keys()"
         before calling "map()" and that we have
         to lookup the value using the key "i". */}
    <ul>
      {Object.keys(object).map(i => (
        <li key={i}>
          <strong>{i}: </strong>
          {object[i]}
        </li>
      ))}
    </ul>
  </section>,
  document.getElementById('root')
);

第一个集合是一个名为array的数组,其中包含字符串值。在 JSX 标记中,你可以看到对array.map()的调用,它将返回一个新数组。映射函数实际上返回了一个 JSX 元素(<li>),这意味着数组中的每个项目现在在标记中表示。

评估这个表达式的结果是一个数组。别担心;JSX 知道如何渲染元素数组。

对象集合使用相同的技术,只是你需要调用Object.keys(),然后映射这个数组。将集合映射到页面上的 JSX 元素的好处是,你可以根据集合数据驱动 React 组件的结构。这意味着你不必依赖命令式逻辑来控制 UI。

渲染输出如下:

JSX 片段的片段

React 16 引入了JSX 片段的概念。片段是一种将标记块组合在一起的方式,而无需向页面添加不必要的结构。例如,一种常见的方法是让 React 组件返回包裹在<div>元素中的内容。这个元素没有实际目的,只会给 DOM 添加混乱。

让我们看一个例子。这里有一个组件的两个版本。一个使用包装元素,另一个使用新的片段功能:

import React from 'react';
import { render } from 'react-dom';

import WithoutFragments from './WithoutFragments';
import WithFragments from './WithFragments';

render(
  <div>
    <WithoutFragments />
    <WithFragments />
  </div>,
  document.getElementById('root')
);

渲染的两个元素分别是<WithoutFragments><WithFragments>。渲染时的样子如下:

现在让我们比较这两种方法。

包装元素

第一种方法是将兄弟元素包装在<div>中。以下是源代码的样子:

import React, { Component } from 'react';

class WithoutFragments extends Component {
  render() {
    return (
      <div>
        <h1>Without Fragments</h1>
        <p>
          Adds an extra <code>div</code> element.
        </p>
      </div>
    );
  }
}

export default WithoutFragments;

这个组件的本质是<h1><p>标签。然而,为了从render()中返回它们,你必须用<div>包装它们。实际上,使用浏览器开发工具检查 DOM 会发现这个<div>除了增加了另一层结构外并没有做任何事情。

现在,想象一个有很多这些组件的应用程序,那就是很多无意义的元素!

避免使用片段的不必要标签

现在让我们来看一下WithFragments组件:

import React, { Component, Fragment } from 'react';

class WithFragments extends Component {
  render() {
    return (
      <Fragment>
        <h1>With Fragments</h1>
        <p>Doesn't have any unused DOM elements.</p>
      </Fragment>
    );
  }
}

export default WithFragments;

而不是将组件内容包装在<div>中,使用了<Fragment>元素。这是一种特殊类型的元素,表示只需要渲染它的子元素。如果你检查 DOM,你可以看到与WithoutFragments组件相比的区别:

注意在前面的例子中你不得不从 React 中导入Fragment吗?这是因为并非所有的转译器(如 Babel)都能理解 Fragment 元素。在未来的版本中,实际上会有一种简写的方式来在 JSX 中表示片段:<>My Content</>。但是目前,React.Fragment应该可以在所有的 React 工具中使用。

摘要

在本章中,你学习了 JSX 的基础知识,包括其声明性结构以及为什么这是一件好事。然后,你编写了一些代码来渲染一些基本的 HTML,并学习了如何使用 JSX 描述复杂的结构。

接下来,你花了一些时间学习了通过实现自己的 React 组件来扩展 JSX 标记的词汇量,这是 UI 的基本构建块。然后,你学习了如何将动态内容带入到 JSX 元素属性中,以及如何将 JavaScript 集合映射到 JSX 元素,消除了控制 UI 显示的命令式逻辑的需要。最后,你学习了如何使用新的 React 16 功能来渲染 JSX 内容的片段。

现在你已经感受到了在 JavaScript 模块中嵌入声明性 XML 来渲染 UI 的感觉,是时候进入下一章了,在那里我们将更深入地了解组件属性和状态。

测试你的知识

  1. 你可以将所有标准的 HTML 标签作为 JSX 元素使用吗?

  2. 是的,但你必须从 react-dom 中导入你想要使用的任何 HTML 标签

  3. 不,你必须实现自己的 React 组件来渲染 HTML 内容

  4. 是的,React 支持这个功能

  5. 如何访问组件的子元素?

  6. 子 JSX 元素始终可以通过 children 属性访问

  7. 子 JSX 元素作为参数传递给 render() 方法

  8. 无法从组件内部访问子元素

  9. Fragment 组件从 React 做什么?

  10. 它更有效地呈现其子元素

  11. 它创建一个可重复使用的标记片段,然后可以在整个应用程序中重复使用

  12. 它通过消除渲染无意义的元素(如容器 div)来充当容器组件

进一步阅读

查看以下链接以获取更多信息:

第三章:组件属性,状态和上下文

React 组件依赖于 JSX 语法,用于描述 UI 的结构。JSX 只能带你走这么远 - 你需要数据来填充 React 组件的结构。本章的重点是组件数据,它有两种主要的变体:属性状态。向组件传递数据的另一种选择是通过上下文。

我将首先定义属性和状态的含义。然后,我将通过一些示例来演示设置组件状态和传递组件属性的机制。在本章的末尾,我们将建立在您对 props 和 state 的新知识的基础上,并介绍功能组件和容器模式。最后,您将了解上下文以及何时选择它比属性更好地向组件传递数据。

组件状态是什么?

React 组件使用 JSX 声明 UI 元素的结构。但是,如果组件要有用,它们需要数据。例如,您的组件 JSX 可能声明一个<ul>,将 JavaScript 集合映射到<li>元素。这个集合是从哪里来的?

状态是 React 组件的动态部分。您可以声明组件的初始状态,随着时间的推移而改变。

想象一下,您正在渲染一个组件,其中其状态的一部分被初始化为空数组。稍后,该数组将被填充数据。这被称为状态变化,每当您告诉 React 组件更改其状态时,组件将自动重新渲染自身。该过程在这里可视化:

组件的状态是组件本身可以设置的东西,或者是组件外的其他代码片段。现在我们将看看组件属性以及它们与组件状态的区别。

组件属性是什么?

属性用于将数据传递给您的 React 组件。与使用新状态作为参数调用方法不同,属性仅在组件呈现时传递。也就是说,您将属性值传递给 JSX 元素。

在 JSX 的上下文中,属性被称为属性,可能是因为在 XML 术语中是这样称呼它们的。在本书中,属性和属性是同义词。

属性与状态不同,因为它们在组件初始渲染后不会改变。如果属性值已更改,并且你想重新渲染组件,那么我们必须重新渲染用于首次渲染的 JSX。React 内部会确保这样做的效率。下面是使用属性渲染和重新渲染组件的图示:

这看起来与有状态的组件有很大不同。真正的区别在于,对于属性来说,往往是父组件决定何时渲染 JSX。组件实际上不知道如何重新渲染自己。正如你将在本书中看到的那样,这种自上而下的流程比在各个地方更改状态更容易预测。

让我们通过编写一些代码来理解这两个概念。

设置组件状态

在这一部分,你将编写一些设置组件状态的 React 代码。首先,你将了解初始状态——这是组件的默认状态。接下来,你将学习如何改变组件的状态,导致它重新渲染自己。最后,你将看到新状态如何与现有状态合并。

初始组件状态

组件的初始状态实际上并不是必需的,但如果你的组件使用状态,应该设置初始状态。这是因为如果组件期望某些状态属性存在,而它们不存在,那么组件要么会失败,要么会渲染出意外的东西。幸运的是,设置初始组件状态很容易。

组件的初始状态应该始终是一个具有一个或多个属性的对象。例如,你可能有一个使用单个数组作为状态的组件。这没问题,但确保将初始数组设置为状态对象的属性。不要将数组用作状态。原因很简单:一致性。每个 React 组件都使用普通对象作为其状态。

现在让我们把注意力转向一些代码。这是一个设置初始状态对象的组件:

import React, { Component } from 'react';

export default class MyComponent extends Component {
 // The initial state is set as a simple property
  // of the component instance.
  state = {
    first: false,
    second: true
  };

  render() {
    // Gets the "first" and "second" state properties
    // into constants, making our JSX less verbose.
    const { first, second } = this.state;

    // The returned JSX uses the "first" and "second"
    // state properties as the "disabled" property
    // value for their respective buttons.
    return (
      <main>
        <section>
          <button disabled={first}>First</button>
        </section>
        <section>
          <button disabled={second}>Second</button>
        </section>
      </main>
    );
  }
}

当你查看render()返回的 JSX 时,你实际上可以看到这个组件依赖的状态值——firstsecond。由于你在初始状态中设置了这些属性,所以可以安全地渲染组件,不会有任何意外。例如,你可以只渲染这个组件一次,它会按预期渲染,多亏了初始状态:

import React from 'react';
import { render } from 'react-dom';

import MyComponent from './MyComponent';

// "MyComponent" has an initial state, nothing is passed
// as a property when it's rendered.
render(<MyComponent />, document.getElementById('root'));

渲染输出如下所示:

设置初始状态并不是很令人兴奋,但它仍然很重要。让组件在状态改变时重新渲染自己。

设置组件状态

让我们创建一个具有一些初始状态的组件。然后渲染这个组件,并更新它的状态。这意味着组件将被渲染两次。让我们来看看这个组件:

import React, { Component } from 'react';

export default class MyComponent extends Component {
  // The initial state is used, until something
  // calls "setState()", at which point the state is
  // merged with this state.
  state = {
    heading: 'React Awesomesauce (Busy)',
    content: 'Loading...'
  };

  render() {
    const { heading, content } = this.state;

    return (
      <main>
        <h1>{heading}</h1>
        <p>{content}</p>
      </main>
    );
  }
}

这个组件的 JSX 取决于两个状态值——headingcontent。该组件还设置了这两个状态值的初始值,这意味着它可以在没有任何意外情况的情况下被渲染。现在,让我们看一些代码,渲染组件,然后通过改变状态重新渲染它:

import React from 'react';
import { render } from 'react-dom';

import MyComponent from './MyComponent';

// The "render()" function returns a reference to the
// rendered component. In this case, it's an instance
// of "MyComponent". Now that we have the reference,
// we can call "setState()" on it whenever we want.
const myComponent = render(
  <MyComponent />,
  document.getElementById('root')
);

// After 3 seconds, set the state of "myComponent",
// which causes it to re-render itself.
setTimeout(() => {
  myComponent.setState({
    heading: 'React Awesomesauce',
    content: 'Done!'
  });
}, 3000);

首先使用默认状态渲染组件。然而,这段代码中有趣的地方是setTimeout()的调用。3 秒后,它使用setState()来改变两个状态属性的值。果然,这个改变在 UI 中得到了体现。在渲染时,初始状态如下所示:

在状态改变后,渲染输出如下:

这个例子突出了具有声明性 JSX 语法来描述 UI 组件结构的强大功能。你只需声明一次,然后随着应用程序中的变化随时间更新组件的状态以反映这些变化。所有 DOM 交互都经过优化并隐藏在视图之外。

在这个例子中,你替换了整个组件状态。也就是说,调用setState()传入了与初始状态中找到的相同对象属性。但是,如果你只想更新组件状态的一部分呢?

合并组件状态

当你设置 React 组件的状态时,实际上是将组件的状态与传递给setState()的对象进行合并。这很有用,因为这意味着你可以设置组件状态的一部分,同时保持其余状态不变。现在让我们来看一个例子。首先,一个带有一些状态的组件:

import React, { Component } from 'react';

export default class MyComponent extends Component {
  // The initial state...
  state = {
    first: 'loading...',
    second: 'loading...',
    third: 'loading...',
    fourth: 'loading...',
    doneMessage: 'finished!'
  };

  render() {
    const { state } = this;

    // Renders a list of items from the
    // component state.
    return (
      <ul>
        {Object.keys(state)
          .filter(key => key !== 'doneMessage')
          .map(key => (
            <li key={key}>
              <strong>{key}: </strong>
              {state[key]}
            </li>
          ))}
      </ul>
    );
  }
}

该组件呈现其状态的键和值——除了doneMessage。每个值默认为loading...。让我们编写一些代码,分别设置每个状态属性的状态:

import React from 'react';
import { render } from 'react-dom';

import MyComponent from './MyComponent';

// Stores a reference to the rendered component...
const myComponent = render(
  <MyComponent />,
  document.getElementById('root')
);

// Change part of the state after 1 second...
setTimeout(() => {
  myComponent.setState({ first: 'done!' });
}, 1000);

// Change another part of the state after 2 seconds...
setTimeout(() => {
  myComponent.setState({ second: 'done!' });
}, 2000);

// Change another part of the state after 3 seconds...
setTimeout(() => {
  myComponent.setState({ third: 'done!' });
}, 3000);

// Change another part of the state after 4 seconds...
setTimeout(() => {
  myComponent.setState(state => ({
    ...state,
    fourth: state.doneMessage
  }));
}, 4000);

从此示例中可以得出的结论是,您可以在组件上设置单个状态属性。它将有效地重新呈现自身。以下是初始组件状态的呈现输出:

以下是两个setTimeout()回调运行后输出的样子:

setState()的第四次调用与前三次不同。您可以传递一个函数,而不是传递一个新对象以合并到现有状态中。此函数接受一个状态参数-组件的当前状态。当您需要基于当前状态值进行状态更改时,这将非常有用。在此示例中,doneMessage值用于设置fourth的值。然后函数返回组件的新状态。您需要将现有状态值合并到新状态中。您可以使用扩展运算符来执行此操作(...state)。

传递属性值

属性就像传递到组件中的状态数据。但是,属性与状态不同之处在于它们只在组件呈现时设置一次。在本节中,您将了解默认属性值。然后,我们将看看设置属性值。在本节之后,您应该能够理解组件状态和属性之间的区别。

默认属性值

默认属性值的工作方式与默认状态值略有不同。它们被设置为一个名为defaultProps的类属性。让我们看一个声明默认属性值的组件:

import React, { Component } from 'react';

export default class MyButton extends Component {
  // The "defaultProps" values are used when the
  // same property isn't passed to the JSX element.
  static defaultProps = {
    disabled: false,
    text: 'My Button'
  };

  render() {
    // Get the property values we want to render.
    // In this case, it's the "defaultProps", since
    // nothing is passed in the JSX.
    const { disabled, text } = this.props; 

    return <button disabled={disabled}>{text}</button>;
  }
}

为什么不像默认状态一样将默认属性值设置为实例属性?原因是属性是不可变的,它们不需要保留为实例属性值。另一方面,状态不断变化,因此组件需要对其进行实例级引用。

您可以看到,此组件为disabledtext设置了默认属性值。只有在通过用于呈现组件的 JSX 标记未传递这些值时,才会使用这些值。让我们继续呈现此组件,而不使用任何属性,以确保使用defaultProps值:

import React from 'react';
import { render } from 'react-dom';

import MyButton from './MyButton';

// Renders the "MyButton" component, without
// passing any property values.
render(<MyButton />, document.getElementById('root'));

始终具有默认状态的相同原则也适用于属性。您希望能够呈现组件,而无需预先知道组件的动态值是什么。

设置属性值

首先,让我们创建一些期望不同类型的属性值的组件:

在第七章验证组件属性中,我将更详细地讨论验证传递给组件的属性值。

import React, { Component } from 'react';

export default class MyButton extends Component {
  // Renders a "<button>" element using values
  // from "this.props".
  render() {
    const { disabled, text } = this.props;

    return <button disabled={disabled}>{text}</button>;
  }
}

这个简单的按钮组件期望一个布尔类型的disabled属性和一个字符串类型的text属性。让我们再创建一个期望一个数组属性值的组件:

import React, { Component } from 'react';

export default class MyList extends Component {
  render() {
    // The "items" property is an array.
    const { items } = this.props;

    // Maps each item in the array to a list item.
    return <ul>{items.map(i => <li key={i}>{i}</li>)}</ul>;
  }
}

你可以通过 JSX 传递几乎任何你想要的东西作为属性值,只要它是一个有效的 JavaScript 表达式。现在让我们编写一些代码来设置这些属性值:

import React from 'react';
import { render as renderJSX } from 'react-dom';

// The two components we're to pass props to
// when they're rendered.
import MyButton from './MyButton';
import MyList from './MyList';

// This is the "application state". This data changes
// over time, and we can pass the application data to
// components as properties.
const appState = {
  text: 'My Button',
  disabled: true,
  items: ['First', 'Second', 'Third']
};

// Defines our own "render()" function. The "renderJSX()"
// function is from "react-dom" and does the actual
// rendering. The reason we're creating our own "render()"
// function is that it contains the JSX that we want to
// render, and so we can call it whenever there's new
// application data.
function render(props) {
  renderJSX(
    <main>
      {/* The "MyButton" component relies on the "text"
           and the "disabed" property. The "text" property
           is a string while the "disabled" property is a
           boolean. */}
      <MyButton text={props.text} disabled={props.disabled} />

      {/* The "MyList" component relies on the "items"
           property, which is an array. Any valid
           JavaScript data can be passed as a property. */}
      <MyList items={props.items} />
    </main>,
    document.getElementById('root')
  );
}

// Performs the initial rendering...
render(appState);

// After 1 second, changes some application data, then
// calls "render()" to re-render the entire structure.
setTimeout(() => {
  appState.disabled = false;
  appState.items.push('Fourth');

  render(appState);
}, 1000);

render()函数看起来像是每次调用时都在创建新的 React 组件实例。React 足够聪明,能够弄清楚这些组件已经存在,并且只需要弄清楚使用新的属性值时输出的差异是什么。

从这个例子中得出的另一个要点是,你有一个appState对象,它保存了应用程序的状态。然后将这个状态的部分作为属性传递给组件,当组件被渲染时。状态必须存在于某个地方,在这种情况下,它在组件之外。我将在下一节中继续讨论这个话题,届时你将学习如何实现无状态的功能组件。

无状态组件

到目前为止,在本书中你所见过的组件都是扩展了基础的Component类的类。现在是时候学习 React 中的功能性组件了。在本节中,你将通过实现一个功能性组件来学习什么是功能性组件。然后,你将学习如何为无状态的功能性组件设置默认属性值。

纯函数组件

一个功能性的 React 组件就像它听起来的那样——一个函数。想象一下你见过的任何 React 组件的render()方法。这个方法本质上就是组件。一个功能性的 React 组件的工作是返回 JSX,就像基于类的 React 组件一样。不同之处在于,这是一个功能性组件可以做的全部。它没有状态和生命周期方法。

为什么要使用函数组件?这更多是简单性的问题。如果你的组件只渲染一些 JSX 而不做其他事情,那么为什么要使用类,而不是一个函数更简单呢?

纯函数是没有副作用的函数。也就是说,给定一组参数调用函数时,函数总是产生相同的输出。这对于 React 组件是相关的,因为给定一组属性,更容易预测渲染的内容会是什么。总是返回相同值的函数在测试时也更容易。

现在让我们看一个函数组件:

import React from 'react'; 

// Exports an arrow function that returns a 
// "<button>" element. This function is pure 
// because it has no state, and will always 
// produce the same output, given the same 
// input. 
export default ({ disabled, text }) => ( 
  <button disabled={disabled}>{text}</button> 
); 

简洁明了,不是吗?这个函数返回一个<button>元素,使用传入的属性作为参数(而不是通过this.props访问它们)。这个函数是纯的,因为如果传入相同的disabledtext属性值,就会渲染相同的内容。现在,让我们看看如何渲染这个组件:

import React from 'react';
import { render as renderJSX } from 'react-dom';

// "MyButton" is a function, instead of a
// "Component" subclass.
import MyButton from './MyButton';

// Renders two "MyButton" components. We only need
// the "first" and "second" properties from the
// props argument by destructuring it.
function render({ first, second }) {
  renderJSX(
    <main>
      <MyButton text={first.text} disabled={first.disabled} />
      <MyButton text={second.text} disabled={second.disabled} />
    </main>,
    document.getElementById('root')
  );
}

// Reders the components, passing in property data.
render({
  first: {
    text: 'First Button',
    disabled: false
  },
  second: {
    text: 'Second Button',
    disabled: true
  }
});

从 JSX 的角度来看,基于类和基于函数的 React 组件没有任何区别。无论是使用类还是函数语法声明的组件,JSX 看起来都是一样的。

惯例是使用箭头函数语法来声明功能性的 React 组件。然而,如果传统的 JavaScript 函数语法更适合你的风格,也是完全有效的。

渲染后的 HTML 如下所示:

函数组件中的默认值

函数组件很轻量;它们没有任何状态或生命周期。然而,它们支持一些元数据选项。例如,你可以像类组件一样指定函数组件的默认属性值。下面是一个示例:

import React from 'react';

// The functional component doesn't care if the property
// values are the defaults, or if they're passed in from
// JSX. The result is the same.
const MyButton = ({ disabled, text }) => (
  <button disabled={disabled}>{text}</button>
);

// The "MyButton" constant was created so that we could
// attach the "defaultProps" metadata here, before
// exporting it.
MyButton.defaultProps = {
  text: 'My Button',
  disabled: false
};

export default MyButton;

defaultProps属性是在函数上定义的,而不是在类上。当 React 遇到具有此属性的函数组件时,它知道如果没有通过 JSX 提供默认值,就会传递默认值。

容器组件

在这一部分,你将学习容器组件的概念。这是一个常见的 React 模式,它汇集了你所学到的关于状态和属性的许多概念。

容器组件的基本原则很简单:不要将数据获取与渲染数据的组件耦合在一起。容器负责获取数据并将其传递给其子组件。它包含负责渲染数据的组件。

这个模式的目的是让你能够在一定程度上实现可替换性。例如,一个容器可以替换它的子组件。或者,一个子组件可以在不同的容器中使用。让我们看看容器模式的实际应用,从容器本身开始:

import React, { Component } from 'react';

import MyList from './MyList';

// Utility function that's intended to mock
// a service that this component uses to
// fetch it's data. It returns a promise, just
// like a real async API call would. In this case,
// the data is resolved after a 2 second delay.
function fetchData() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(['First', 'Second', 'Third']);
    }, 2000);
  });
}

// Container components usually have state, so they
// can't be declared as functions.
export default class MyContainer extends Component {
  // The container should always have an initial state,
  // since this will be passed down to child components
  // as properties.
  state = { items: [] };

  // After the component has been rendered, make the
  // call to fetch the component data, and change the
  // state when the data arrives.
  componentDidMount() {
    fetchData().then(items => this.setState({ items }));
  }

  // Renders the container, passing the container
  // state as properties, using the spread operator: "...".
  render() {
    return <MyList {...this.state} />;
  }
}

这个组件的工作是获取数据并设置它的状态。每当状态被设置时,render()就会被调用。这就是子组件的作用。容器的状态被传递给子组件作为属性。接下来让我们来看一下MyList组件:

import React from 'react';

// A stateless component that expects
// an "items" property so that it can render
// a "<ul>" element.
export default ({ items }) => (
  <ul>{items.map(i => <li key={i}>{i}</li>)}</ul>
);

MyList是一个期望有一个items属性的函数组件。让我们看看容器组件实际上是如何使用的:

import React from 'react';
import { render } from 'react-dom';

import MyContainer from './MyContainer';

// All we have to do is render the "MyContainer"
// component, since it looks after providing props
// for it's children.
render(<MyContainer />, document.getElementById('root'));

容器组件设计将在第五章中更深入地介绍,Crafting Reusable Components。这个例子的目的是让你感受一下在 React 组件中状态和属性之间的相互作用。

当你加载页面时,你会在模拟 HTTP 请求需要 3 秒后看到以下内容被渲染出来:

提供和消费上下文

随着你的 React 应用程序的增长,它将使用更多的组件。它不仅会有更多的组件,而且你的应用程序的结构将发生变化,使得组件嵌套更深。嵌套在最深层级的组件仍然需要传递数据给它们。从父组件向子组件传递数据并不是什么大问题。挑战在于当你不得不开始使用组件作为传递数据的间接方式时。

对于需要传递到应用程序中任何组件的数据,你可以创建并使用一个上下文。在使用 React 中上下文时,有两个关键概念要记住——提供者和消费者。上下文提供者创建数据并确保它对任何 React 组件都可用。上下文消费者是一个在上下文中使用这些数据的组件。

你可能会想知道上下文是否只是在 React 应用程序中说全局数据的另一种方式。基本上,这正是上下文的用途。使用 React 的方法将组件与上下文包装在一起比创建全局数据更好,因为你可以更好地控制数据如何流经你的组件。例如,你可以有嵌套的上下文和许多其他高级用例。但现在,让我们只关注简单的用法。

假设您有一些应用程序数据,用于确定给定应用程序功能的权限。这些数据可以从 API 中获取,也可以是硬编码的。无论哪种情况,要求是您不希望通过组件树传递所有这些权限数据。如果权限数据只需存在,供任何需要它的组件使用,那就太好了。

从组件树的顶部开始,让我们看一下index.js

import React from 'react';
import { render } from 'react-dom';

import { PermissionProvider } from './PermissionContext';
import App from './App';

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

<App>组件是<PermissionProvider>组件的子组件。这意味着权限上下文已经提供给了<App>组件及其所有子组件,一直到树的最底部。让我们看一下定义权限上下文的PermissionContext.js模块。

import React, { Component, createContext } from 'react';

const { Provider, Consumer } = createContext('permissions');

export class PermissionProvider extends Component {
  state = {
    first: true,
    second: false,
    third: true
  };

  render() {
    return (
      <Provider value={this.state}>{this.props.children}</Provider>
    );
  }
}

const PermissionConsumer = ({ name, children }) => (
  <Consumer>{value => value[name] && children}</Consumer>
);

export { PermissionConsumer };

createContext()函数用于创建实际的上下文。返回值是一个包含两个组件——ProviderConsumer的对象。接下来,有一个用于整个应用程序的权限提供者的简单抽象。状态包含组件可能想要使用的实际数据。在这个例子中,如果值为 true,则应该正常显示该功能。如果为 false,则该功能没有权限进行渲染。在这里,状态只设置一次,但由于这是一个常规的 React 组件,您可以像在任何其他组件上设置状态一样设置状态。渲染的值是<Provider>组件。这通过value属性为任何子组件提供上下文数据。

接下来,有一个用于权限消费者的小抽象。不是让每个需要测试权限的组件一遍又一遍地实现相同的逻辑,PermissionConsumer组件可以做到。<Consumer>组件的子组件始终是一个以上下文数据作为参数的函数。在这个例子中,PermissionConsumer组件有一个name属性,用于功能的名称。这与上下文中的值进行比较,如果为 false,则不会渲染任何内容。

现在让我们看一下App组件:

import React, { Fragment } from 'react';

import First from './First';
import Second from './Second';
import Third from './Third';

export default () => (
  <Fragment>
    <First />
    <Second />
    <Third />
  </Fragment>
);

这个组件渲染了三个需要检查权限的功能组件。如果没有 React 的上下文功能,您将不得不通过这个组件将这些数据作为属性传递给每个组件。如果<First>有需要检查权限的子组件或孙子组件,相同的属性传递机制可能会变得非常混乱。

现在让我们来看一下<First>组件(<Second><Third>几乎完全相同):

import React from 'react';
import { PermissionConsumer } from './PermissionContext';

export default () => (
  <PermissionConsumer name="first">
    <div>
      <button>First</button>
    </div>
  </PermissionConsumer>
);

这就是PermissionConsumer组件的用法。您只需要为其提供一个name属性,如果权限检查通过,则子组件将被渲染。<PermissionConsumer>组件可以在任何地方使用,无需传递数据即可使用。以下是这三个组件的渲染输出:

第二个组件没有被渲染,因为它在PermissionProvider组件中的权限被设置为 false。

摘要

在本章中,您了解了 React 组件中的状态和属性。您首先定义并比较了这两个概念。然后,您实现了几个 React 组件并操纵了它们的状态。接下来,您通过实现了从 JSX 传递属性值到组件的代码来了解了属性。然后,您了解了容器组件的概念,用于将数据获取与呈现内容解耦。最后,您了解了 React 16 中的新上下文 API 以及如何使用它来避免在组件中引入间接性。

在下一章中,您将学习如何处理 React 组件中的用户事件。

测试您的知识

  1. 为什么始终初始化组件的状态是个好主意?

  2. 因为如果不这样做,当您尝试渲染时,React 将抛出错误。

  3. 因为 React 不知道您在组件状态中有什么类型,并且无法优化渲染。

  4. 因为如果render()方法期望状态值,您需要确保它们始终存在,以避免意外的渲染行为。

  5. 什么时候应该使用属性而不是状态?

  6. 状态应该只用于可以更改的值。对于其他所有情况,应该使用属性。

  7. 尽量避免使用状态。

  8. 您应该只使用属性来更新现有状态。

  9. 什么是 React 中的上下文?

  10. 上下文是您如何将事件处理程序函数传递给应用程序中的不同组件的方法。

  11. 上下文用于避免瞬态属性。上下文用于与少数组件共享公共数据。

  12. 上下文就像在组件之间共享的状态。

进一步阅读

访问以下链接获取更多信息:

第四章:事件处理,React 方式

本章的重点是事件处理。React 在处理事件方面有独特的方法:在 JSX 中声明事件处理程序。我将首先看一下在 JSX 中声明特定元素的事件处理程序。然后,您将了解如何绑定处理程序上下文和参数值。接下来,我们将实现内联和高阶事件处理程序函数。

然后您将了解 React 实际上是如何将事件处理程序映射到 DOM 元素的。最后,您将了解 React 传递给事件处理程序函数的合成事件,以及它们如何为性能目的进行池化。

声明事件处理程序

在 React 组件中处理事件的不同因素是它是声明式的。与 jQuery 相比,你必须编写命令式代码来选择相关的 DOM 元素并将事件处理程序函数附加到它们上。

在 JSX 标记中声明事件处理程序的声明性方法的优势在于它们是 UI 结构的一部分。不必追踪分配事件处理程序的代码是一种心理上的解放。

在本节中,您将编写一个基本的事件处理程序,以便了解在 React 应用程序中找到的声明性事件处理语法。然后,您将学习如何使用通用事件处理程序函数。

声明处理程序函数

让我们看一个声明了元素点击事件的基本组件:

import React, { Component } from 'react';

export default class MyButton extends Component {
  // The click event handler, there's nothing much
  // happening here other than a log of the event.
  onClick() {
    console.log('clicked');
  }

  // Renders a "<button>" element with the "onClick"
  // event handler set to the "onClick()" method of
  // this component.
  render() {
    return (
      <button onClick={this.onClick}>{this.props.children}</button>
    );
  }
}

事件处理程序函数this.onClick()被传递给<button>元素的onClick属性。通过查看这个标记,清楚地知道按钮被点击时将运行什么代码。

请参阅官方的 React 文档,了解支持的事件属性名称的完整列表:facebook.github.io/react/docs/

多个事件处理程序

我真的很喜欢声明式事件处理程序语法的一点是,当一个元素分配了多个处理程序时,它很容易阅读。有时,例如,一个元素有两个或三个处理程序。命令式代码很难处理单个事件处理程序,更不用说多个事件处理程序了。当一个元素需要更多处理程序时,它只是另一个 JSX 属性。从代码可维护性的角度来看,这在很大程度上是可扩展的。

import React, { Component } from 'react';

export default class MyInput extends Component {
  // Triggered when the value of the text input changes...
  onChange() {
    console.log('changed');
  }

  // Triggered when the text input loses focus...
  onBlur() {
    console.log('blured');
  }

  // JSX elements can have as many event handler
  // properties as necessary.
  render() {
    return <input onChange={this.onChange} onBlur={this.onBlur} />;
  }
}

这个<input>元素可能有几个更多的事件处理程序,代码仍然可以读得很清楚。

当您不断向组件添加更多事件处理程序时,您会注意到很多事件处理程序都在做相同的事情。接下来,您将学习如何在组件之间共享通用处理程序函数。

导入通用处理程序

任何 React 应用程序都可能会为不同组件共享相同的事件处理逻辑。例如,响应按钮点击时,组件应该对项目列表进行排序。这些类型的通用行为应该属于它们自己的模块,以便多个组件可以共享它们。让我们实现一个使用通用事件处理程序函数的组件:

import React, { Component } from 'react';

// Import the generic event handler that
// manipulates the state of a component.
import reverse from './reverse';

export default class MyList extends Component {
  state = {
    items: ['Angular', 'Ember', 'React']
  };

  // Makes the generic function specific
  // to this component by calling "bind(this)".
  onReverseClick = reverse.bind(this);

  render() {
    const { state: { items }, onReverseClick } = this;

    return (
      <section>
        {/* Now we can attach the "onReverseClick" handler
            to the button, and the generic function will
            work with this component's state. */}
        <button onClick={onReverseClick}>Reverse</button>
        <ul>{items.map((v, i) => <li key={i}>{v}</li>)}</ul>
      </section>
    );
  }
}

让我们从这里开始,逐步了解正在发生的事情,从导入开始。您正在导入一个名为reverse()的函数。这是您在<button>元素中使用的通用事件处理程序函数。当它被点击时,列表应该反转其顺序。

onReverseClick方法实际上调用了通用的reverse()函数。它是使用bind()来将通用函数的上下文绑定到此组件实例而创建的。

最后,看一下 JSX 标记,您可以看到onReverseClick()函数被用作按钮点击的处理程序。

那么,这到底是如何工作的呢?您有一个通用函数,它以某种方式改变了此组件的状态,因为您将上下文绑定到它?嗯,基本上是的,就是这样。现在让我们来看一下通用函数的实现:

// Exports a generic function that changes the 
// state of a component, causing it to re-render 
// itself.
export default function reverse() { 
  this.setState(this.state.items.reverse()); 
} 

此函数依赖于this.state属性和状态中的items数组。关键在于状态是通用的;一个应用程序可能有许多具有其状态中的items数组的组件。

我们渲染的列表如下所示:

如预期的那样,点击按钮会导致列表排序,使用您的通用reverse()事件处理程序:

接下来,您将学习如何绑定事件处理程序函数的上下文和参数值。

事件处理程序上下文和参数

在这一部分,您将了解绑定其事件处理程序上下文的 React 组件以及如何将数据传递给事件处理程序。对于 React 事件处理程序函数来说,拥有正确的上下文是很重要的,因为它们通常需要访问组件属性或状态。能够对事件处理程序进行参数化也很重要,因为它们不会从 DOM 元素中提取数据。

获取组件数据

在本节中,您将了解处理程序需要访问组件属性以及参数值的情况。您将渲染一个自定义列表组件,该组件在列表中的每个项目上都有一个点击事件处理程序。组件将按以下方式传递一个值数组:

import React from 'react';
import { render } from 'react-dom';

import MyList from './MyList';

// The items to pass to "<MyList>" as a property.
const items = [
  { id: 0, name: 'First' },
  { id: 1, name: 'Second' },
  { id: 2, name: 'Third' }
];

// Renders "<MyList>" with an "items" property.
render(<MyList items={items} />, document.getElementById('root'));

列表中的每个项目都有一个id属性,用于标识该项目。当用户在 UI 中点击项目时,您需要能够访问此 ID,以便事件处理程序可以处理该项目。以下是MyList组件的实现方式:

import React, { Component } from 'react';

export default class MyList extends Component {
  constructor() {
    super();

    // We want to make sure that the "onClick()"
    // handler is explicitly bound to this component
    // as it's context.
    this.onClick = this.onClick.bind(this);
  }

  // When a list item is clicked, look up the name
  // of the item based on the "id" argument. This is
  // why we need access to the component through "this",
  // for the properties.
  onClick(id) {
    const { name } = this.props.items.find(i => i.id === id);
    console.log('clicked', `"${name}"`);
  }

  render() {
    return (
      <ul>
        {/* Creates a new handler function with
            the bound "id" argument. Notice that
            the context is left as null, since that
            has already been bound in the
            constructor. */}
        {this.props.items.map(({ id, name }) => (
          <li key={id} onClick={this.onClick.bind(null, id)}>
            {name}
          </li>
        ))}
      </ul>
    );
  }
}

渲染列表如下所示:

您必须绑定事件处理程序的上下文,这是在构造函数中完成的。如果您查看onClick()事件处理程序,您会发现它需要访问组件,以便它可以在this.props.items中查找被点击的项目。此外,onClick()处理程序需要一个id参数。如果您查看此组件的 JSX 内容,您会发现调用bind()为列表中的每个项目提供了参数值。这意味着当处理程序响应点击事件时,项目的id已经提供了。

这种参数化事件处理的方法与以往的方法有很大不同。例如,我过去常常依赖于从 DOM 元素本身获取参数数据。当你只需要一个事件处理程序时,这种方法效果很好,它可以从事件参数中提取所需的数据。这种方法也不需要通过迭代集合并调用bind()来设置几个新函数。

这就是其中的权衡。React 应用程序避免触及 DOM,因为 DOM 实际上只是 React 组件的渲染目标。如果您可以编写不引入对 DOM 元素的显式依赖的代码,那么您的代码将是可移植的。这就是您在此示例中事件处理程序所实现的内容。

如果你担心为集合中的每个项目创建一个新函数会对性能产生影响,那就不用担心。你不会一次在页面上渲染成千上万个项目。对你的代码进行基准测试,如果结果表明bind()调用是 React 事件处理程序中最慢的部分,那么你可能有一个非常快速的应用程序。

高阶事件处理程序

高阶函数是返回新函数的函数。有时,高阶函数也将函数作为参数。在前面的例子中,您使用bind()来绑定事件处理程序函数的上下文和参数值。返回事件处理程序函数的高阶函数是另一种技术。这种技术的主要优点是您不需要多次调用bind()。相反,您只需在要将参数绑定到函数的位置调用该函数。让我们看一个示例组件:

import React, { Fragment, Component } from 'react';

export default class App extends Component {
  state = {
    first: 0,
    second: 0,
    third: 0
  };

  // This function is defined as an arrow function, so "this" is
  // lexically-bound to this component. The name argument is used
  // by the function that's returned as the event handler in the
  // computed property name.
  onClick = name => () => {
    this.setState(state => ({
      ...state,
      [name]: state[name] + 1
    }));
  };

  render() {
    const { first, second, third } = this.state;

    return (
      <Fragment>
        {/* By calling this.onClick() and supplying an argument value,
            you're creating a new event handler function on the fly. 
       */}
        <button onClick={this.onClick('first')}>First {first}</button>
        <button onClick={this.onClick('second')}>
          Second {second}
        </button>
        <button onClick={this.onClick('third')}>Third {third}</button>
      </Fragment>
    );
  }
}

该组件呈现三个按钮,并具有三个状态片段-每个按钮的计数器。onClick()函数会自动绑定到组件上下文,因为它被定义为箭头函数。它接受一个name参数并返回一个新函数。返回的函数在调用时使用这个name值。它使用计算属性语法([]内的变量)来增加给定名称的状态值。在每个按钮被点击几次后,该组件内容如下:

内联事件处理程序

将处理程序函数分配给 JSX 属性的典型方法是使用命名函数。但是,有时您可能想要使用内联函数。这是通过直接将箭头函数分配给 JSX 标记中的事件属性来完成的:

import React, { Component } from 'react';

export default class MyButton extends Component {
  // Renders a button element with an "onClick()" handler.
  // This function is declared inline with the JSX, and is
  // useful in scenarios where you need to call another
  // function.
  render() {
    return (
      <button onClick={e => console.log('clicked', e)}>
        {this.props.children}
      </button>
    );
  }
}

像这样内联事件处理程序的主要用途是当您有一个静态参数值要传递给另一个函数时。在这个例子中,您正在使用字符串clicked调用console.log()。您可以通过在 JSX 标记之外创建一个使用bind()创建新函数,或者使用高阶函数来为此目的设置一个特殊函数。但是,您将不得不再想一个新的函数名称。有时内联更容易。

将处理程序绑定到元素

当您将事件处理程序函数分配给 JSX 中的元素时,React 实际上并没有将事件侦听器附加到底层 DOM 元素上。相反,它将函数添加到内部函数映射中。页面上的文档有一个单一的事件侦听器。当事件通过 DOM 树冒泡到文档时,React 处理程序会检查是否有匹配的处理程序。该过程如下图所示:

你可能会问,为什么 React 要费这么大的劲?这与我在过去几章中一直在讲的原则相同;尽可能将声明式 UI 结构与 DOM 分开。

例如,当渲染新组件时,其事件处理程序函数只是添加到 React 维护的内部映射中。当触发事件并且它命中document对象时,React 将事件映射到处理程序。如果找到匹配项,它会调用处理程序。最后,当 React 组件被移除时,处理程序只是从处理程序列表中移除。

这些 DOM 操作实际上都没有触及 DOM。它都是由单个事件侦听器抽象出来的。这对性能和整体架构都是有利的(保持渲染目标与应用程序代码分开)。

合成事件对象

当您使用原生的addEventListener()函数将事件处理程序函数附加到 DOM 元素时,回调函数将会传递一个事件参数。React 中的事件处理程序函数也会传递一个事件参数,但它不是标准的Event实例。它被称为SyntheticEvent,它是原生事件实例的简单包装。

在 React 中,合成事件有两个目的:

  • 提供一致的事件接口,规范浏览器的不一致性

  • 合成事件包含传播所需的信息

以下是在 React 组件上下文中合成事件的示例:

在下一节中,您将看到这些合成事件是如何为了性能原因而进行池化的,以及这对异步代码的影响。

事件池化

用原生事件实例包装的一个挑战是可能会导致性能问题。每个创建的合成事件包装器最终都需要被垃圾回收,这在 CPU 时间方面可能是昂贵的。

当垃圾收集器运行时,您的 JavaScript 代码将无法运行。这就是为什么要节约内存;频繁的垃圾收集意味着对响应用户交互的代码的 CPU 时间较少。

例如,如果您的应用程序只处理少量事件,这可能并不重要。但即使按照适度的标准,应用程序也会响应许多事件,即使处理程序实际上并不对其执行任何操作。如果 React 不断地必须分配新的合成事件实例,这就成了一个问题。

React 通过分配合成实例池来解决这个问题。每当触发事件时,它都会从池中取出一个实例并填充其属性。当事件处理程序运行结束时,合成事件实例将被释放回池中,如下所示:

这可以防止在触发大量事件时垃圾收集器频繁运行。池保留对合成事件实例的引用,因此它们永远不会被垃圾收集。React 也不需要分配新实例。

然而,有一个需要注意的地方。它涉及在事件处理程序的异步代码中访问合成事件实例。这是一个问题,因为一旦处理程序运行结束,实例就会返回到池中。当它返回到池中时,它的所有属性都被清除。下面是一个示例,展示了这种情况可能出错的情况:

import React, { Component } from 'react'; 

// Mock function, meant to simulate fetching 
// data asynchronously from an API. 
function fetchData() { 
  return new Promise((resolve) => { 
    setTimeout(() => { 
      resolve(); 
    }, 1000); 
  }); 
} 

export default class MyButton extends Component { 
  onClick(e) { 
    // This works fine, we can access the DOM element 
    // through the "currentTarget" property. 
    console.log('clicked', e.currentTarget.style); 

    fetchData().then(() => { 
      // However, trying to access "currentTarget" 
      // asynchronously fails, because it's properties 
      // have all been nullified so that the instance 
      // can be reused. 
      console.log('callback', e.currentTarget.style); 
    }); 
  } 

  render() { 
    return ( 
      <button onClick={this.onClick}> 
        {this.props.children} 
      </button> 
    ); 
  } 
} 

第二次调用console.log()试图从异步回调中访问合成事件属性,直到事件处理程序完成才运行,这导致事件清空其属性。这会导致警告和undefined值。

这个例子的目的是说明当您编写与事件交互的异步代码时,事情可能会出错。千万不要这样做!

摘要

本章向您介绍了 React 中的事件处理。React 和其他事件处理方法的关键区别在于处理程序是在 JSX 标记中声明的。这使得追踪哪些元素处理哪些事件变得更加简单。

您学到了在单个元素上有多个事件处理程序是添加新的 JSX 属性的问题。接下来,您学到了共享处理通用行为的事件处理函数是一个好主意。如果事件处理程序函数需要访问组件属性或状态,则上下文可能很重要。您了解了绑定事件处理程序函数上下文和参数值的各种方法。这些包括调用bind()和使用高阶事件处理程序函数。

然后,您了解了内联事件处理程序函数及其潜在用途,以及 React 实际上是如何将单个 DOM 事件处理程序绑定到文档对象的。合成事件是包装本机事件的抽象,您了解了它们为什么是必要的以及它们如何被池化以实现高效的内存消耗。

在下一章中,您将学习如何创建可重用于各种目的的组件。

测试你的知识

  1. 什么使 React 中的事件处理程序是声明式的?

  2. 任何事件处理程序函数都是声明式的

  3. React 事件处理程序被声明为组件 JSX 的一部分

  4. React 事件处理程序不是声明式的

  5. 高阶事件处理程序函数的常见用途是什么?

  6. 当你有几个处理相同事件的组件时,你可以使用高阶函数将被点击的项目的 ID 绑定到处理程序函数

  7. 应该尽可能使用高阶函数作为 React 事件处理程序函数

  8. 当你不确定事件处理程序需要什么数据时,高阶函数允许你传递任何你需要的东西

  9. 你能把内联函数传递给事件属性吗?

  10. 是的。当事件处理程序是简单的一行代码时,这是首选。

  11. 不。你应该总是将事件处理程序函数声明为方法或绑定函数。

  12. 为什么 React 使用事件实例池而不是在每个事件中创建新实例?

  13. React 不使用事件池

  14. 如果不这样做,最终会耗尽内存,因为这些对象永远不会被删除

  15. 为了避免在短时间内触发大量事件时调用垃圾收集器来删除未使用的事件实例

进一步阅读

访问以下链接以获取更多信息:

第五章:打造可重用的组件

本章的重点是向您展示如何实现不止一种用途的 React 组件。阅读完本章后,您将对如何组合应用程序功能感到自信。

本章以简要介绍 HTML 元素及其在帮助实现功能方面的工作方式开始。然后,您将看到一个单片组件的实现,并发现它将在未来引起的问题。接下来的部分致力于以一种使功能由更小的组件组成的方式重新实现单片组件。

最后,本章以讨论渲染 React 组件树结束,并为您提供一些建议,以避免由于分解组件而引入过多复杂性。我将通过重申高级功能组件与实用组件的概念来结束这一最后部分。

可重用的 HTML 元素

让我们思考一下 HTML 元素。根据 HTML 元素的类型,它要么是以功能为中心,要么是以实用为中心。实用为中心的 HTML 元素比以功能为中心的 HTML 元素更具重用性。例如,考虑<section>元素。这是一个通用元素,可以在任何地方使用,但它的主要目的是组成功能的结构方面——功能的外壳和功能的内部部分。这就是<section>元素最有用的地方。

另一方面,您还有诸如<p><span><button>之类的元素。这些元素提供了高度的实用性,因为它们从设计上就是通用的。当用户可以点击时,您应该使用<button>元素,从而产生一个动作。这比功能的概念低一个级别。

虽然谈论具有高度实用性的 HTML 元素与针对特定功能的元素很容易,但当涉及数据时,讨论就会更加详细。HTML 是静态标记——React 组件将静态标记与数据结合在一起。问题是,如何确保您正在创建正确的以功能为中心和以实用为中心的组件?

本章的目标是找出如何从定义功能的单片 React 组件转变为与实用组件相结合的更小的以功能为中心的组件。

单片组件的困难

如果您可以为任何给定功能实现一个组件,那将简化您的工作。至少,就不会有太多需要维护的组件,也不会有太多数据流通的路径,因为一切都将是组件内部的。

然而,这个想法出于许多原因是行不通的。拥有单体功能组件使得协调任何团队开发工作变得困难。单体组件变得越大,以后重构为更好的东西就会变得越困难。

还有功能重叠和功能通信的问题。重叠是因为功能之间的相似之处而发生的——一个应用程序不太可能具有完全彼此独特的一组功能。这将使应用程序非常难以学习和使用。组件通信基本上意味着一个功能中的某些东西的状态将影响另一个功能中的某些东西的状态。状态很难处理,当有很多状态打包到单体组件中时更是如此。

学习如何避免单体组件的最佳方法是亲身体验一个。您将在本节的其余部分中实现一个单体组件。在接下来的部分中,您将看到如何将此组件重构为更可持续的东西。

JSX 标记

我们要实现的单体组件是一个列出文章的功能。这只是为了举例说明,所以我们不希望组件过大。它将是简单的,但是单体的。用户可以向列表中添加新项目,切换列表中项目的摘要,并从列表中删除项目。这是组件的render方法:

render() {
 const { articles, title, summary } = this.data.toJS();

  return (
    <section>
      <header>
        <h1>Articles</h1>
        <input
          placeholder="Title"
          value={title}
          onChange={this.onChangeTitle}
        />
        <input
          placeholder="Summary"
          value={summary}
          onChange={this.onChangeSummary}
        />
        <button onClick={this.onClickAdd}>Add</button>
      </header>
      <article>
        <ul>
          {articles.map(i => (
            <li key={i.id}>
              <a
                href={`#${i.id}`}
                title="Toggle Summary"
                onClick={this.onClickToggle.bind(null, i.id)}
              >
                {i.title}
              </a>
              &nbsp;
              <a
                href={`#${i.id}`}
                title="Remove"
                onClick={this.onClickRemove.bind(null, i.id)}
              >
                ✗
              </a>
              <p style={{ display: i.display }}>{i.summary}</p>
            </li>
          ))}
        </ul>
      </article>
    </section>
  );
} 

在一个地方使用的 JSX 肯定比必要的要多。您将在接下来的部分中改进这一点,但现在让我们为这个组件实现初始状态。

我强烈建议您从github.com/PacktPublishing/React-and-React-Native-Second-Edition下载本书的配套代码。我可以拆分组件代码,以便在这些页面上解释它。但是,如果您可以完整地看到代码模块,并运行它们,学习体验会更容易。

初始状态和状态助手

现在让我们看看这个组件的初始状态:

// The state of this component is consists of
// three properties: a collection of articles,
// a title, and a summary. The "fromJS()" call
// is used to build an "Immutable.js" Map. Also
// note that this isn't set directly as the component
// state - it's in a "data" property of the state -
// otherwise, state updates won't work as expected.
state = {
  data: fromJS({
    articles: [
      {
        id: cuid(),
        title: 'Article 1',
        summary: 'Article 1 Summary',
        display: 'none'
      },
      {
        id: cuid(),
        title: 'Article 2',
        summary: 'Article 2 Summary',
        display: 'none'
      },
      {
        id: cuid(),
        title: 'Article 3',
        summary: 'Article 3 Summary',
        display: 'none'
      },
      {
        id: cuid(),
        title: 'Article 4',
        summary: 'Article 4 Summary',
        display: 'none'
      }
    ],
    title: '',
    summary: ''
  })
}; 

有两个有趣的函数用于初始化状态。第一个是来自cuid包的cuid()——一个用于生成唯一 ID 的有用工具。第二个是来自immutable包的fromJS()。以下是引入这两个依赖项的导入:

// Utility for constructing unique IDs... 
import cuid from 'cuid'; 

// For building immutable component states... 
import { fromJS } from 'immutable'; 

正如其名称所示,fromJS()函数用于构建不可变的数据结构。Immutable.js对于操作 React 组件的状态非常有用的功能。在本书的其余部分,你将继续使用Immutable.js,并且随着学习的深入,你将了解更多具体内容,从这个例子开始。

要更深入地了解Immutable.js,请查看《精通 Immutable.js》:www.packtpub.com/web-development/mastering-immutablejs

你可能还记得上一章中提到的setState()方法只能使用普通对象。嗯,Immutable.js对象不是普通对象。如果我们想使用不可变数据,就需要将它们包装在一个普通对象中。让我们实现一个帮助器的获取器和设置器:

// Getter for "Immutable.js" state data... 
get data() { 
  return this.state.data; 
} 

// Setter for "Immutable.js" state data... 
set data(data) { 
  this.setState({ data }); 
} 

现在,你可以在我们的事件处理程序中使用不可变的组件状态。

事件处理程序实现

在这一点上,你已经有了初始状态、状态辅助属性和组件的 JSX。现在是时候实现事件处理程序本身了:

// When the title of a new article changes, update the state
// of the component with the new title value, by using "set()"
// to create a new map.
onChangeTitle = e => {
  this.data = this.data.set('title', e.target.value);
};

// When the summary of a new article changes, update the state
// of the component with the new summary value, by using "set()"
// to create a new map.
onChangeSummary = e => {
  this.data = this.data.set('summary', e.target.value);
};

// Creates a new article and empties the title
// and summary inputs. The "push()" method creates a new
// list and "update()" is used to update the list by
// creating a new map.
onClickAdd = () => {
  this.data = this.data
    .update('articles', a =>
      a.push(
        fromJS({
          id: cuid(),
          title: this.data.get('title'),
          summary: this.data.get('summary'),
          display: 'none'
        })
      )
    )
    .set('title', '')
    .set('summary', '');
};

// Removes an article from the list. Calling "delete()"
// creates a new list, and this is set in the new component
// state.
onClickRemove = id => {
  const index = this.data
    .get('articles')
    .findIndex(a => a.get('id') === id);

  this.data = this.data.update('articles', a => a.delete(index));
};

// Toggles the visibility of the article summary by
// setting the "display" state of the article. This
// state is dependent on the current state.
onClickToggle = id => {
  const index = this.data
    .get('articles')
    .findIndex(a => a.get('id') === id);

  this.data = this.data.update('articles', articles =>
    articles.update(index, a =>
      a.update('display', display => (display ? '' : 'none'))
    )
  );
};

天啊!这是很多Immutable.js代码!不用担心,实际上这比使用普通 JavaScript 实现这些转换要少得多。以下是一些指针,帮助你理解这段代码:

  • setState()总是以一个普通对象作为其参数调用。这就是为什么我们引入了数据设置器。当你给this.data赋一个新值时,它会用一个普通对象调用setState()。你只需要关心Immutable.js数据。同样,数据获取器返回Immutable.js对象而不是整个状态。

  • 不可变方法总是返回一个新实例。当你看到像article.set(...)这样的东西时,它实际上并没有改变article,而是创建了一个新的实例。

  • render()方法中,不可变数据结构被转换回普通的 JavaScript 数组和对象,以便在 JSX 标记中使用。

如果需要,尽管花费你需要的时间来理解这里发生了什么。随着你在书中的进展,你会看到不可变状态可以被 React 组件利用的方式。这些事件处理程序只能改变这个组件的状态。也就是说,它们不会意外地改变其他组件的状态。正如你将在接下来的部分中看到的,这些处理程序实际上已经相当完善了。

这是渲染输出的截图:

重构组件结构

你有一个庞大的功能组件,现在怎么办?让我们把它做得更好。

在本节中,你将学习如何将刚刚在前一节中实现的功能组件分割成更易维护的组件。你将从 JSX 开始,因为这可能是最好的重构起点。然后,你将为这个功能实现新的组件。

接下来,你将使这些新组件变成功能性的,而不是基于类的。最后,你将学会如何使用渲染属性来减少应用程序中直接组件的依赖数量。

从 JSX 开始

任何庞大组件的 JSX 都是找出如何将其重构为更小组件的最佳起点。让我们来可视化一下我们当前正在重构的组件的结构:

JSX 的顶部部分是表单控件,所以这很容易成为自己的组件:

<header> 
  <h1>Articles</h1> 
  <input 
    placeholder="Title" 
    value={title} 
    onChange={this.onChangeTitle} 
  /> 
  <input 
    placeholder="Summary" 
    value={summary} 
    onChange={this.onChangeSummary} 
  /> 
  <button onClick={this.onClickAdd}>Add</button> 
</header> 

接下来,你有文章列表:

<ul> 
  {articles.map(i => ( 
    <li key={i.id}> 
      <a 
        href="#" 

        onClick={ 
          this.onClickToggle.bind(null, i.id) 
        } 
      > 
        {i.title} 
      </a> 
      &nbsp; 
      <a 
        href="#" 

        onClick={this.onClickRemove.bind(null, i.id)} 
      > 
        ✗
      </a> 
      <p style={{ display: i.display }}> 
        {i.summary} 
      </p> 
    </li> 
  ))} 
</ul> 

在这个列表中,有可能有一个文章项目,它将是<li>标签中的所有内容。

单单 JSX 就展示了 UI 结构如何可以被分解成更小的 React 组件。没有声明性的 JSX 标记,这种重构练习将会很困难。

实现文章列表组件

文章列表组件的实现如下:

import React, { Component } from 'react';

export default class ArticleList extends Component {
  render() {
    // The properties include things that are passed in
    // from the feature component. This includes the list
    // of articles to render, and the two event handlers
    // that change state of the feature component.
    const { articles, onClickToggle, onClickRemove } = this.props;

    return (
      <ul>
        {articles.map(article => (
          <li key={article.id}>
            {/* The "onClickToggle()" callback changes
                the state of the "MyFeature" component. */}
            <a
              href={`#${article.id}`}
              title="Toggle Summary"
              onClick={onClickToggle.bind(null, article.id)}
            >
              {article.title}
            </a>
            &nbsp;
            {/* The "onClickRemove()" callback changes
                the state of the "MyFeature" component. */}
            <a
              href={`#${article.id}`}
              title="Remove"
              onClick={onClickRemove.bind(null, article.id)}
            >
              ✗
            </a>
            <p style={{ display: article.display }}>
              {article.summary}
            </p>
          </li>
        ))}
      </ul>
    );
  }
}

你只需从庞大的组件中取出相关的 JSX,并放到这里。现在让我们看看功能组件 JSX 是什么样的:

render() {
  const { articles, title, summary } = this.data.toJS();

  return (
    <section>
      <header>
        <h1>Articles</h1>
        <input
          placeholder="Title"
          value={title}
          onChange={this.onChangeTitle}
        />
        <input
          placeholder="Summary"
          value={summary}
          onChange={this.onChangeSummary}
        />
        <button onClick={this.onClickAdd}>Add</button>
      </header>

      {/* Now the list of articles is rendered by the
           "ArticleList" component. This component can
           now be used in several other components. */}
      <ArticleList
        articles={articles}
        onClickToggle={this.onClickToggle}
        onClickRemove={this.onClickRemove}
      />
    </section>
  );
} 

文章列表现在由<ArticleList>组件渲染。要渲染的文章列表作为属性传递给这个组件,以及两个事件处理程序。

等等,为什么我们要将事件处理程序传递给子组件?原因是ArticleList组件不需要担心状态或状态如何改变。它只关心呈现内容,并确保适当的事件回调连接到适当的 DOM 元素。这是我稍后在本章中会扩展的容器组件概念。

实现文章项目组件

在实现文章列表组件之后,您可能会决定进一步拆分此组件,因为该项目可能会在另一页上的另一个列表中呈现。实现文章列表项作为其自己的组件最重要的一点是,我们不知道标记将来会如何改变。

另一种看待它的方式是,如果事实证明我们实际上不需要该项目作为其自己的组件,这个新组件并不会引入太多间接性或复杂性。话不多说,这就是文章项目组件:

import React, { Component } from 'react';

export default class ArticleItem extends Component {
  render() {
    // The "article" is mapped from the "ArticleList"
    // component. The "onClickToggle()" and
    // "onClickRemove()" event handlers are passed
    // all the way down from the "MyFeature" component.
    const { article, onClickToggle, onClickRemove } = this.props;

    return (
      <li>
        {/* The "onClickToggle()" callback changes
            the state of the "MyFeature" component. */}
        <a
          href={`#{article.id}`}
          title="Toggle Summary"
          onClick={onClickToggle.bind(null, article.id)}
        >
          {article.title}
        </a>
        &nbsp;
        {/* The "onClickRemove()" callback changes
            the state of the "MyFeature" component. */}
        <a
          href={`#{article.id}`}
          title="Remove"
          onClick={onClickRemove.bind(null, article.id)}
        >
          ✗
        </a>
        <p style={{ display: article.display }}>{article.summary}</p>
      </li>
    );
  }
}

这是由ArticleList组件呈现的新的ArticleItem组件:

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

export default class ArticleList extends Component {
  render() {
    // The properties include things that are passed in
    // from the feature component. This includes the list
    // of articles to render, and the two event handlers
    // that change state of the feature component. These,
    // in turn, are passed to the "ArticleItem" component.
    const { articles, onClickToggle, onClickRemove } = this.props;

    // Now this component maps to an "<ArticleItem>" collection.
    return (
      <ul>
        {articles.map(i => (
          <ArticleItem
            key={i.id}
            article={i}
            onClickToggle={onClickToggle}
            onClickRemove={onClickRemove}
          />
        ))}
      </ul>
    );
  }
}

您看到这个列表只是映射了文章列表吗?如果您想要实现另一个还进行一些过滤的文章列表呢?如果是这样,拥有可重用的ArticleItem组件是有益的。

实现添加文章组件

现在你已经完成了文章列表,是时候考虑用于添加新文章的表单控件了。让我们为这个功能的这一方面实现一个组件:

import React, { Component } from 'react';

export default class AddArticle extends Component {
  render() {
    const {
      name,
      title,
      summary,
      onChangeTitle,
      onChangeSummary,
      onClickAdd
    } = this.props;

    return (
      <section>
        <h1>{name}</h1>
        <input
          placeholder="Title"
          value={title}
          onChange={onChangeTitle}
        />
        <input
          placeholder="Summary"
          value={summary}
          onChange={onChangeSummary}
        />
        <button onClick={onClickAdd}>Add</button>
      </section>
    );
  }
}

现在,您的功能组件只需要呈现<AddArticle><ArticleList>组件:

render() { 
  const {  
    articles,  
    title,  
    summary, 
  } = this.state.data.toJS(); 

  return ( 
    <section> 
      { /* Now the add article form is rendered by the 
           "AddArticle" component. This component can 
           now be used in several other components. */ } 
      <AddArticle 
        name="Articles" 
        title={title} 
        summary={summary} 
        onChangeTitle={this.onChangeTitle} 
        onChangeSummary={this.onChangeSummary} 
        onClickAdd={this.onClickAdd} 
      /> 

      { /* Now the list of articles is rendered by the 
           "ArticleList" component. This component can 
           now be used in several other components. */ } 
      <ArticleList 
        articles={articles} 
        onClickToggle={this.onClickToggle} 
        onClickRemove={this.onClickRemove} 
      /> 
    </section> 
  ); 
} 

该组件的重点是功能数据,同时它会推迟到其他组件来呈现 UI 元素。

使组件功能化

在实现这些新组件时,您可能已经注意到它们除了使用属性值呈现 JSX 之外没有任何职责。这些组件是纯函数组件的良好候选者。每当您遇到仅使用属性值的组件时,最好将它们制作成功能性组件。首先,这明确表明组件不依赖于任何状态或生命周期方法。它还更有效,因为当 React 检测到组件是函数时,它不会执行太多工作。

这是文章列表组件的功能版本:

import React from 'react';
import ArticleItem from './ArticleItem';

export default ({ articles, onClickToggle, onClickRemove }) => (
  <ul>
    {articles.map(i => (
      <ArticleItem
        key={i.id}
        article={i}
        onClickToggle={onClickToggle}
        onClickRemove={onClickRemove}
      />
    ))}
  </ul>
);

这是文章项目组件的功能版本:

import React from 'react';

export default ({ article, onClickToggle, onClickRemove }) => (
  <li>
    {/* The "onClickToggle()" callback changes
         the state of the "MyFeature" component. */}
    <a
      href={`#${article.id}`}
      title="Toggle Summary"
      onClick={onClickToggle.bind(null, article.id)}
    >
      {article.title}
    </a>
    &nbsp;
    {/* The "onClickRemove()" callback changes
         the state of the "MyFeature" component. */}
    <a
      href={`#${article.id}`}
      title="Remove"
      onClick={onClickRemove.bind(null, article.id)}
    >
      ✗
    </a>
    <p style={{ display: article.display }}>{article.summary}</p>
  </li>
);

这是添加文章组件的功能版本:

import React from 'react';

export default ({
  name,
  title,
  summary,
  onChangeTitle,
  onChangeSummary,
  onClickAdd
}) => (
  <section>
    <h1>{name}</h1>
    <input
      placeholder="Title"
      value={title}
      onChange={onChangeTitle}
    />
    <input
      placeholder="Summary"
      value={summary}
      onChange={onChangeSummary}
    />
    <button onClick={onClickAdd}>Add</button>
  </section>
);

使组件变成功能性的另一个好处是,减少了引入不必要方法或其他数据的机会。

利用渲染属性

想象一下实现一个由几个较小的组件组成的特性,就像你在本章中一直在做的那样。MyFeature组件依赖于ArticleListAddArticle。现在想象一下,在应用程序的不同部分使用MyFeature,在那里使用不同的ArticleListAddArticle的实现是有意义的。根本的挑战是用一个组件替换另一个组件。

渲染属性是解决这一挑战的一种好方法。其思想是,你向组件传递一个属性,其值是一个返回要渲染的组件的函数。这样,你可以根据需要配置子组件,而不是让特性组件直接依赖它们;你可以将它们作为渲染属性值传递进去。

渲染属性不是 React 16 的新特性。它是一种技术,其流行程度与 React 16 的发布同时增加。这是一种官方认可的处理依赖和替换问题的方法。你可以在这里阅读更多关于渲染属性的内容:reactjs.org/docs/render-props.html让我们来看一个例子。不是让MyFeature直接依赖于AddArticleArticleList,你可以将它们作为渲染属性传递。当MyFeature使用渲染属性来填补<AddArticle><ArticleList>原来的位置时,MyFeaturerender()方法是什么样子的:

// Now when <MyFeature> is rendered, it uses render props to
// render <ArticleList> and <AddArticle>. It no longer has
// a direct dependency to these components.
render() {
  const { articles, title, summary } = this.data.toJS();
  const {
    props: { addArticle, articleList },
    onClickAdd,
    onClickToggle,
    onClickRemove,
    onChangeTitle,
    onChangeSummary
  } = this;

  return (
    <section>
      {addArticle({
        title,
        summary,
        onChangeTitle,
        onChangeSummary,
        onClickAdd
      })}
      {articleList({ articles, onClickToggle, onClickRemove })}
    </section>
  );
}

addArticle()articleList()函数被调用,传递的是与<AddArticle><ArticleList>应该传递的相同的属性值。现在的区别是,这个模块不再将AddArticleArticleList作为依赖导入。

现在让我们来看一下index.js,在这里<MyFeature>被渲染:

// <MyFeature> is now passed a "addArticle" and a "articleList"
// property. These are functions that return components to render.
render(
  <MyFeature
    addArticle={({
      title,
      summary,
      onChangeTitle,
      onChangeSummary,
      onClickAdd
    }) => (
      <AddArticle
        name="Articles"
        title={title}
        summary={summary}
        onChangeTitle={onChangeTitle}
        onChangeSummary={onChangeSummary}
        onClickAdd={onClickAdd}
      />
    )}
    articleList={({ articles, onClickToggle, onClickRemove }) => (
      <ArticleList
        articles={articles}
        onClickToggle={onClickToggle}
        onClickRemove={onClickRemove}
      />
    )}
  />,
  document.getElementById('root')
);

这里现在发生的事情比只渲染<MyFeature>时要多得多。让我们分解一下为什么会这样。在这里,您传递了addArticlearticleList渲染属性。这些属性值是从MyComponent接受参数值的函数。例如,onClickToggle()函数来自MyFeature,用于更改该组件的状态。您可以使用渲染属性函数将其传递给将要呈现的组件,以及任何其他值。这些函数的返回值最终被呈现。

呈现组件树

让我们花点时间来反思一下在本章中我们已经取得的成就。曾经是单片的功能组件最终几乎完全专注于状态数据。它处理了初始状态并处理了状态的转换,如果有的话,它还会处理获取状态的网络请求。这是 React 应用程序中典型的容器组件,也是数据的起点。

您实现的新组件,用于更好地组合功能,是这些数据的接收者。这些组件与它们的容器之间的区别在于,它们只关心在它们呈现时传递给它们的属性。换句话说,它们只关心特定时间点的数据快照。从这里,这些组件可能将属性数据作为属性传递给它们自己的子组件。组合 React 组件的通用模式如下:

容器组件通常只包含一个直接子组件。在这个图表中,您可以看到容器既有一个项目详细信息组件,也有一个列表组件。当然,这两个类别会有所不同,因为每个应用程序都是不同的。这种通用模式有三个级别的组件组合。数据从容器一直流向实用组件。

一旦添加了超过三层,应用程序架构就变得难以理解。会有偶尔需要添加四层 React 组件的情况,但一般情况下,应该避免这样做。

功能组件和实用组件

在庞大的组件示例中,你开始时只有一个完全专注于某个特性的组件。这意味着该组件在应用程序的其他地方几乎没有效用。

这是因为顶层组件处理应用程序状态。有状态的组件在任何其他上下文中都很难使用。当你重构庞大的特性组件时,你创建了更远离数据的新组件。一般规则是,你的组件离有状态数据越远,它们的效用就越大,因为它们的属性值可以从应用程序的任何地方传递进来。

总结

本章是关于避免庞大的组件设计。然而,在任何 React 组件的设计中,庞大的组件通常是一个必要的起点。

你开始学习不同的 HTML 元素具有不同程度的效用。接下来,你了解了庞大的 React 组件的问题,并演示了如何实现庞大的组件。

然后,你花了几节课学习如何将庞大的组件重构为更可持续的设计。通过这个练习,你学到了容器组件只需要考虑处理状态,而较小的组件具有更多的效用,因为它们的属性值可以从任何地方传递进来。你还学到了可以使用渲染属性更好地控制组件的依赖关系和替换。

在下一章中,你将学习关于 React 组件生命周期。这对于实现容器组件来说是一个特别相关的话题。

测试你的知识

  1. 为什么应该避免庞大的 React 组件?

  2. 因为一旦组件达到一定的大小,整个应用程序的性能就会开始受到影响。

  3. 因为它们难以理解,并且难以在以后重构为更小的可重用组件。

  4. 你不需要担心避免庞大的组件。

  5. 为什么要使组件功能化?

  6. 功能组件只依赖于传递给它的属性值。它们不依赖于状态或生命周期方法,这两者都是潜在的问题来源。

  7. 功能组件更容易阅读。

  8. 不应该使组件功能化,即使它们没有任何状态。

  9. 渲染属性如何简化 React 应用程序?

  10. 它们减少了你需要为给定组件编写的代码量。

  11. 它们不会简化 React 应用程序。

  12. 它们减少了组件的直接依赖数量,允许您组合新的行为。

更多阅读

点击以下链接获取更多信息:

第六章:React 组件生命周期

本章的目标是让您了解 React 组件的生命周期以及如何编写响应生命周期事件的代码。您将学习为什么组件首先需要生命周期。然后,您将使用这些方法实现几个初始化其属性和状态的组件。

接下来,您将学习如何通过避免在不必要时进行渲染来优化组件的渲染效率。然后,您将了解如何在 React 组件中封装命令式代码以及在组件卸载时如何进行清理。最后,您将学习如何使用新的 React 16 生命周期方法捕获和处理错误。

组件为什么需要生命周期

React 组件经历生命周期。实际上,您在本书中迄今为止在组件中实现的render()方法实际上是一个生命周期方法。渲染只是 React 组件中的一个生命周期事件。

例如,当组件挂载到 DOM 时,当组件更新时等都有生命周期事件。生命周期事件是另一个移动部分,因此您希望将其保持最少。正如您将在本章中学到的那样,一些组件确实需要响应生命周期事件以执行初始化、渲染启发式、在组件从 DOM 中卸载时进行清理,或者处理组件抛出的错误。

以下图表让您了解组件如何通过其生命周期流程,依次调用相应的方法:

这是 React 组件的两个主要生命周期流程。第一个发生在组件初始渲染时。第二个发生在组件更新时。以下是每个方法的大致概述:

  • getDerivedStateFromProps(): 此方法允许您根据组件的属性值更新组件的状态。当组件首次渲染和接收新的属性值时,将调用此方法。

  • render(): 返回组件要渲染的内容。当组件首次挂载到 DOM 时,当它接收新的属性值时以及调用setState()时都会调用此方法。

  • componentDidMount(): 这在组件挂载到 DOM 后调用。这是您可以执行组件初始化工作的地方,例如获取数据。

  • shouldComponentUpdate(): 您可以使用此方法将新状态或属性与当前状态或属性进行比较。然后,如果不需要重新渲染组件,可以返回 false。此方法用于使您的组件更有效。

  • getSnapshotBeforeUpdate(): 此方法允许您在实际提交到 DOM 之前直接在组件的 DOM 元素上执行操作。此方法与render()的区别在于getSnapshotBeforeUpdate()不是异步的。使用render()时,调用它和实际在 DOM 中进行更改之间的 DOM 结构可能会发生变化的可能性很大。

  • componentDidUpdate(): 当组件更新时调用此方法。您很少需要使用此方法。

此图表中未包括的另一个生命周期方法是componentWillUnmount()。这是组件即将被移除时调用的唯一生命周期方法。我们将在本章末尾看到如何使用此方法的示例。在此之前,让我们开始编码。

初始化属性和状态

在本节中,您将看到如何在 React 组件中实现初始化代码。这涉及使用在组件首次创建时调用的生命周期方法。首先,您将实现一个基本示例,该示例使用来自 API 的数据设置组件。然后,您将看到如何从属性初始化状态,以及如何在属性更改时更新状态。

获取组件数据

当初始化组件时,您将希望填充其状态或属性。否则,组件除了其骨架标记之外将没有任何内容可渲染。例如,假设您想要渲染以下用户列表组件:

import React from 'react';
import { Map } from 'immutable';

// This component displays the passed-in "error"
// property as bold text. If it's null, then
// nothing is rendered.
const ErrorMessage = ({ error }) =>
  Map([[null, null]]).get(error, <strong>{error}</strong>);

// This component displays the passed-in "loading"
// property as italic text. If it's null, then
// nothing is rendered.
const LoadingMessage = ({ loading }) =>
  Map([[null, null]]).get(loading, <em>{loading}</em>);

export default ({
  error, 
  loading,
  users
}) => (
  <section>
    {/* Displays any error messages... */}
    <ErrorMessage error={error} />

    {/* Displays any loading messages, while
         waiting for the API... */}
    <LoadingMessage loading={loading} />

    {/* Renders the user list... */}
    <ul>{users.map(i => <li key={i.id}>{i.name}</li>)}</ul>
  </section>
);

此 JSX 依赖于三个数据:

  • 加载中:在获取 API 数据时显示此消息

  • error: 如果出现问题,将显示此消息

  • users: 从 API 获取的数据

此处使用了两个辅助组件:ErrorMessageLoadingMessage。它们分别用于格式化errorloading状态。但是,如果errorloading为 null,您不希望在组件中引入命令式逻辑来处理此情况。这就是为什么您使用Immutable.js映射的一个很酷的小技巧:

  1. 您创建了一个具有单个键值对的映射。键为 null,值也为 null。

  2. 您使用errorloading属性调用get()。如果errorloading属性为 null,则找到键并且不渲染任何内容。

  3. get()接受第二个参数,如果找不到键,则返回该参数。这是您传递您的真值值并完全避免命令逻辑的地方。这个特定的组件很简单,但是当存在两种以上可能性时,这种技术尤其强大。

您应该如何进行 API 调用并使用响应来填充users集合?答案是使用一个容器组件进行 API 调用,然后渲染UserList组件:

import React, { Component } from 'react';
import { fromJS } from 'immutable';

import { users } from './api';
import UserList from './UserList';

export default class UserListContainer extends Component {
  state = {
    data: fromJS({
      error: null,
      loading: 'loading...',
      users: []
    })
  };

  // Getter for "Immutable.js" state data...
  get data() {
    return this.state.data;
  }

  // Setter for "Immutable.js" state data...
  set data(data) {
    this.setState({ data });
  }

  // When component has been rendered, "componentDidMount()"
  // is called. This is where we should perform asynchronous
  // behavior that will change the state of the component.
  // In this case, we're fetching a list of users from
  // the mock API.
  componentDidMount() {
    users().then(
      result => {
        // Populate the "users" state, but also
        // make sure the "error" and "loading"
        // states are cleared.
        this.data = this.data
          .set('loading', null)
          .set('error', null)
          .set('users', fromJS(result.users));
      },
      error => {
        // When an error occurs, we want to clear
        // the "loading" state and set the "error"
        // state.
        this.data = this.data
          .set('loading', null)
          .set('error', error);
      }
    );
  }

  render() {
    return <UserList {...this.data.toJS()} />;
  }
}

让我们来看看render()方法。它的工作是渲染<UserList>组件,并将this.state作为属性传递。实际的 API 调用发生在componentDidMount()方法中。此方法在组件挂载到 DOM 后调用。

由于componentDidMount()的命名,React 开发人员认为在发出组件数据的请求之前等待组件挂载到 DOM 是不好的。换句话说,如果 React 在发送请求之前必须执行大量工作,用户体验可能会受到影响。实际上,获取数据是一个异步任务,在render()之前或之后启动它对您的应用程序来说没有真正的区别。

您可以在这里阅读更多信息:reactjs.org/blog/2018/03/27/update-on-async-rendering.html

一旦 API 调用返回数据,users集合就会被填充,导致UserList重新渲染自身,只是这一次,它有了需要的数据。让我们来看看这里使用的users()模拟 API 函数调用:

// Returns a promise that's resolved after 2
// seconds. By default, it will resolve an array
// of user data. If the "fail" argument is true,
// the promise is rejected.
export function users(fail) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (fail) {
        reject('epic fail');
      } else {
        resolve({
          users: [
            { id: 0, name: 'First' },
            { id: 1, name: 'Second' },
            { id: 2, name: 'Third' },
          ],
        });
      }
    }, 2000);
  });
}

它返回一个在 2 秒后解析为数组的 promise。Promise 是模拟诸如 API 调用之类的东西的好工具,因为它们使您能够在 React 组件中使用不止 HTTP 调用作为数据源。例如,您可能正在从本地文件中读取数据,或者使用返回解析来自各种来源的数据的库。

loading状态为字符串,users状态为空数组时,UserList组件渲染如下:

loadingnullusers不为空时,它渲染如下:

我想再次强调UserListContainerUserList组件之间的责任分离。因为容器组件处理生命周期管理和实际的 API 通信,你可以创建一个通用的用户列表组件。事实上,它是一个不需要任何状态的功能组件,这意味着你可以在应用程序中的其他容器组件中重用它。

使用属性初始化状态

前面的例子向你展示了如何通过在componentDidMount()生命周期方法中进行 API 调用来初始化容器组件的状态。然而,组件状态中唯一填充的部分是users集合。你可能想填充其他不来自 API 端点的状态部分。

例如,当状态初始化时,errorloading状态消息已经设置了默认值。这很好,但是如果渲染UserListContainer的代码想要使用不同的加载消息怎么办?你可以通过允许属性覆盖默认状态来实现这一点。让我们继续完善UserListContainer组件:

import React, { Component } from 'react';
import { fromJS } from 'immutable';

import { users } from './api';
import UserList from './UserList';

class UserListContainer extends Component {
  state = {
    data: fromJS({
      error: null,
      users: []
    })
  };

  // Getter for "Immutable.js" state data...
  get data() {
    return this.state.data;
  }

  // Setter for "Immutable.js" state data...
  set data(data) {
    this.setState({ data });
  }

  // When component has been rendered, "componentDidMount()"
  // is called. This is where we should perform asynchronous
  // behavior that will change the state of the component.
  // In this case, we're fetching a list of users from
  // the mock API.
  componentDidMount() {
    users().then(
      result => {
        // Populate the "users" state, but also
        // make sure the "error" and "loading"
        // states are cleared.
        this.data = this.data
          .set('error', null)
          .set('users', fromJS(result.users));
      },
      error => {
        // When an error occurs, we want to clear
        // the "loading" state and set the "error"
        // state.
        this.data = this.data
          .set('loading', null)
          .set('error', error);
      }
    );
  }

  render() {
    return <UserList {...this.data.toJS()} />;
  }

  // Called right before render, you can use this method
  // to update the state of the component based on prop
  // values.
  static getDerivedStateFromProps(props, state) {
    return {
      ...state,
      data: state.data.set(
        'loading',
        state.data.get('users').size === 0 ? props.loading : null
      )
    };
  }
}

UserListContainer.defaultProps = {
  loading: 'loading...'
};

export default UserListContainer;

loading属性不再具有默认字符串值。相反,defaultProps为属性提供默认值。新的生命周期方法是getDerivedStateFromProps()。它使用loading属性来设置loading状态。由于loading属性有一个默认值,所以只需改变状态是安全的。该方法在组件挂载之前和组件的后续重新渲染时被调用。

这个方法是静态的,因为在 React 16 中有内部变化。预期这个方法的行为像一个纯函数,没有副作用。如果这个方法是一个实例方法,你将可以访问组件上下文,并且副作用将很常见。

使用这种新的 React 16 方法的挑战在于它在初始渲染和后续重新渲染时都会被调用。在 React 16 之前,你可以使用componentWillMount()方法来运行只在初始渲染之前运行的代码。在这个例子中,你必须检查users集合中是否有值,然后再将loading状态设置为 null - 你不知道这是初始渲染还是第 40 次渲染。

现在让我们看看如何将状态数据传递给UserListContainer

import React from 'react';
import { render } from 'react-dom';

import UserListContainer from './UserListContainer';

// Renders the component with a "loading" property.
// This value ultimately ends up in the component state.
render(
  <UserListContainer loading="playing the waiting game..." />,
  document.getElementById('root')
);

当首次渲染UserList时,初始加载消息是什么样子的:

仅仅因为组件有状态并不意味着你不能进行定制。接下来,你将学习这个概念的一个变种——使用属性更新组件状态。

使用属性更新状态

你已经看到了componentDidMount()getDerivedStateFromProps()生命周期方法如何帮助你的组件获取所需的数据。还有一个情景你需要考虑——重新渲染组件容器。

让我们来看一个简单的button组件,它会跟踪被点击的次数:

import React from 'react';

export default ({
  clicks,
  disabled,
  text,
  onClick
}) => (
  <section>
    {/* Renders the number of button clicks,
         using the "clicks" property. */}
    <p>{clicks} clicks</p>

    {/* Renders the button. It's disabled state
         is based on the "disabled" property, and
         the "onClick()" handler comes from the
         container component. */}
    <button disabled={disabled} onClick={onClick}>
      {text}
    </button>
  </section>
);

现在,让我们为这个功能实现一个容器组件:

import React, { Component } from 'react';
import { fromJS } from 'immutable';

import MyButton from './MyButton';

class MyFeature extends Component {
  state = {
    data: fromJS({
      clicks: 0,
      disabled: false,
      text: ''
    })
  };

  // Getter for "Immutable.js" state data...
  get data() {
    return this.state.data;
  }

  // Setter for "Immutable.js" state data...
  set data(data) {
    this.setState({ data });
  }

  // Click event handler, increments the "click" count.
  onClick = () => {
    this.data = this.data.update('clicks', c => c + 1);
  };

  // Renders the "<MyButton>" component, passing it the
  // "onClick()" handler, and the state as properties.
  render() {
    return <MyButton onClick={this.onClick} {...this.data.toJS()} />;
  }

  // If the component is re-rendered with new
  // property values, this method is called with the
  // new property values. If the "disabled" property
  // is provided, we use it to update the "disabled"
  // state. Calling "setState()" here will not
  // cause a re-render, because the component is already
  // in the middle of a re-render.
  static getDerivedStateFromProps({ disabled, text }, state) {
    return {
      ...state,
      data: state.data.set('disabled', disabled).set('text', text)
    };
  }
}

MyFeature.defaultProps = {
  text: 'A Button'
};

export default MyFeature;

与前面的例子相同的方法在这里也被使用。getDerivedStateFromProps()方法在每次渲染之前被调用,这是你可以使用属性值来确定组件状态是否应该更新的地方。让我们看看如何重新渲染这个组件以及状态是否如预期般行为:

import React from 'react';
import { render as renderJSX } from 'react-dom';

import MyFeature from './MyFeature';

// Determines the state of the button
// element in "MyFeature".
let disabled = true;

function render() {
  // Toggle the state of the "disabled" property.
  disabled = !disabled;

  renderJSX(
    <MyFeature {...{ disabled }} />,
    document.getElementById('root')
  );
}

// Re-render the "<MyFeature>" component every
// 3 seconds, toggling the "disabled" button
// property.
setInterval(render, 3000);

render();

果然,一切都按计划进行。每当按钮被点击时,点击计数器都会更新。<MyFeature>每 3 秒重新渲染一次,切换按钮的disabled状态。当按钮重新启用并且点击恢复时,计数器会从上次停止的地方继续。

这是MyButton组件在首次渲染时的样子:

这是在点击了几次后,按钮进入禁用状态后的样子:

优化渲染效率

接下来你要学习的下一个生命周期方法用于实现改进组件渲染性能的启发式。你会发现,如果组件的状态没有改变,那么就没有必要进行渲染。然后,你将实现一个组件,该组件使用来自 API 的特定元数据来确定是否需要重新渲染组件。

渲染还是不渲染

shouldComponentUpdate()生命周期方法用于确定当被要求渲染时组件是否会进行渲染。例如,如果实现了这个方法,并返回 false,那么组件的整个生命周期都会被中断,不会进行渲染。如果组件渲染了大量数据并且经常重新渲染,这个检查就非常重要。关键是要知道组件状态是否已经改变。

这就是不可变数据的美妙之处——你可以轻松地检查它是否发生了变化。如果你正在使用Immutable.js等库来控制组件的状态,这一点尤为真实。让我们看一个简单的列表组件:

import React, { Component } from 'react';
import { fromJS } from 'immutable';

export default class MyList extends Component {
  state = {
    data: fromJS({
      items: [...Array(5000).keys()]
    })
  };

  // Getter for "Immutable.js" state data...
  get data() {
    return this.state.data;
  }

  // Setter for "Immutable.js" state data...
  set data(data) {
    this.setState({ data });
  }

  // If this method returns false, the component
  // will not render. Since we're using an Immutable.js
  // data structure, we simply need to check for equality.
  // If "state.data" is the same, then there's no need to
  // render because nothing has changed since the last render.
  shouldComponentUpdate(props, state) {
    return this.data !== state.data;
  }

  // Renders the complete list of items, even if it's huge.
  render() {
    const items = this.data.get('items');

    return <ul>{items.map(i => <li key={i}>{i}</li>)}</ul>;
  }
}

items状态初始化为一个包含 5000 个项目的Immutable.js List。这是一个相当大的集合,所以你不希望 React 内部的虚拟 DOM 不断地对比这个列表。虚拟 DOM 在它所做的事情上是高效的,但远不及能执行简单的渲染检查的代码高效。你在这里实现的shouldComponentRender()方法正是这样做的。它比较新状态和当前状态;如果它们是相同的对象,完全绕过虚拟 DOM。

现在,让我们让这个组件开始工作,看看你能获得什么样的效率提升:

import React from 'react';
import { render as renderJSX } from 'react-dom';

import MyList from './MyList';

// Renders the "<MyList>" component. Then, it sets
// the state of the component by changing the value
// of the first "items" element. However, the value
// didn't actually change, so the same Immutable.js
// structure is reused. This means that
// "shouldComponentUpdate()" will return false.
function render() {
  const myList = renderJSX(
    <MyList />,
    document.getElementById('root')
  );

  // Not actually changing the value of the first
  // "items" element. So, Immutable.js recognizes
  // that nothing changed, and instead of
  // returning a new object, it returns the same
  // "myList.data" reference.
  myList.data = myList.data.setIn(['items', 0], 0);
}

// Instead of performing 500,000 DOM operations,
// "shouldComponentUpdate()" turns this into
// 5000 DOM operations.
for (let i = 0; i < 100; i++) {
  render();
}

你正在循环渲染<MyList>。每次迭代都有 5000 个列表项要渲染。由于状态没有改变,shouldComponentUpdate()的调用在每次迭代中都返回false。出于性能原因,这很重要,因为迭代次数很多。在真实应用中,你不会有代码在紧密循环中重新渲染组件。这段代码旨在测试 React 的渲染能力。如果你注释掉shouldComponentUpdate()方法,你就会明白我的意思。这个组件的性能概况如下:

初始渲染时间最长——几百毫秒。但接下来有很多微小的时间片段,对用户体验完全不可感知。这些是shouldComponentUpdate()返回 false 的结果。现在让我们注释掉这个方法,看看这个概况会如何改变:

没有shouldComponentUpdate(),最终结果是更长的时间片段,对用户体验有极大的负面影响。

你可能注意到,我们实际上是使用Immutable.jssetIn()来改变状态。这应该会导致状态改变,对吧?实际上,这将返回相同的Immutable.js实例,原因很简单,我们设置的值与当前值相同:0。当没有发生改变时,Immutable.js方法返回相同的对象,因为它没有发生变化。

使用元数据优化渲染

在本节中,你将学习如何使用 API 响应的元数据来确定组件是否应该重新渲染自己。这里是一个简单的用户详情组件:

import React, { Component } from 'react';

export default class MyUser extends Component {
  state = {
    modified: new Date(),
    first: 'First',
    last: 'Last'
  };

  // The "modified" property is used to determine
  // whether or not the component should render.
  shouldComponentUpdate(props, state) {
    return Number(state).modified > Number(this.state.modified);
  }

  render() {
    const { modified, first, last } = this.state;

    return (
      <section>
        <p>{modified.toLocaleString()}</p>
        <p>{first}</p>
        <p>{last}</p>
      </section>
    );
  }
}

shouldComponentUpdate()方法正在比较新的modified状态和旧的modified状态。这段代码假设modified值是一个反映 API 返回的数据实际修改时间的日期。这种方法的主要缺点是shouldComponentUpdate()方法现在与 API 数据紧密耦合。优点是,你可以像使用不可变数据一样获得性能提升。

这就是这个启发式方法的实际效果:

import React from 'react';
import { render } from 'react-dom';

import MyUser from './MyUser';

// Performs the initial rendering of "<MyUser>".
const myUser = render(<MyUser />, document.getElementById('root'));

// Sets the state, with a new "modified" value.
// Since the modified state has changed, the
// component will re-render.
myUser.setState({
  modified: new Date(),
  first: 'First1',
  last: 'Last1'
});

// The "first" and "last" states have changed,
// but the "modified" state has not. This means
// that the "First2" and "Last2" values will
// not be rendered.
myUser.setState({
  first: 'First2',
  last: 'Last2'
});

MyUser组件现在完全依赖于modified状态。如果它不大于先前的modified值,就不会发生渲染。

在渲染两次后,组件的外观如下:

在这个例子中,我没有使用不可变状态数据。在本书中,我将使用普通的 JavaScript 对象作为简单示例的状态。Immutable.js是这项工作的好工具,所以我会经常使用它。与此同时,我想明确指出Immutable.js并不需要在每种情况下都使用。

渲染命令式组件

到目前为止,在本书中,你渲染的所有内容都是直接的声明式 HTML。生活从来都不是那么简单:有时你的 React 组件需要在底层实现一些命令式的代码。

这就是关键——隐藏命令式操作,使渲染组件的代码不必触及它。在本节中,你将实现一个简单的 jQuery UI 按钮 React 组件,以便你可以看到相关的生命周期方法如何帮助你封装命令式代码。

渲染 jQuery UI 小部件

jQuery UI 小部件库在标准 HTML 之上实现了几个小部件。它使用渐进增强技术,在支持新功能的浏览器中增强基本 HTML。为了使这些小部件工作,你首先需要以某种方式将 HTML 渲染到 DOM 中;然后,进行命令式函数调用来创建和与小部件交互。

在这个例子中,你将创建一个 React 按钮组件,作为 jQuery UI 小部件的包装器。使用 React 组件的人不需要知道,在幕后,它正在进行命令式调用来控制小部件。让我们看看按钮组件的样子:

import React, { Component } from 'react';

// Import all the jQuery UI widget stuff...
import $ from 'jquery';
import 'jquery-ui/ui/widgets/button';
import 'jquery-ui/themes/base/all.css';

export default class MyButton extends Component {
  // When the component is mounted, we need to
  // call "button()" to initialize the widget.
  componentDidMount() {
    $(this.button).button(this.props);
  }

  // After the component updates, we need to use
  // "this.props" to update the options of the
  // jQuery UI button widget.
  componentDidUpdate() {
    $(this.button).button('option', this.props);
  }

  // Renders the "<button>" HTML element. The "onClick()"
  // handler will always be a assigned, even if it's a
  // noop function. The "ref" property is used to assign
  // "this.button". This is the DOM element itself, and
  // it's needed by the "componentDidMount()" and
  // "componentDidUpdate()" methods.
  render() {
    return (
      <button
        onClick={this.props.onClick}
        ref={button => {
          this.button = button;
        }}
      />
    );
  }
}

jQuery UI 按钮小部件期望<button>元素,因此组件呈现为此。还分配了来自组件属性的onClick()处理程序。这里还使用了ref属性,它将button参数分配给this.button。这样做的原因是,组件可以直接访问组件的底层 DOM 元素。通常,组件不需要访问任何 DOM 元素,但在这里,您需要向元素发出命令。

例如,在componentDidMount()方法中,调用了button()函数,并将其属性传递给组件。componentDidUpdate()方法执行类似的操作,当属性值更改时调用。现在,让我们看一下按钮容器组件:

import React, { Component } from 'react';
import { fromJS } from 'immutable';

import MyButton from './MyButton';

class MyButtonContainer extends Component {
  // The initial state is an empty Immutable map, because
  // by default, we won't pass anything to the jQuery UI
  // button widget.
  state = {
    data: fromJS({})
  };

  // Getter for "Immutable.js" state data...
  get data() {
    return this.state.data;
  }

  // Setter for "Immutable.js" state data...
  set data(data) {
    this.setState({ data });
  }

  // When the component is mounted for the first time,
  // we have to bind the "onClick()" handler to "this"
  // so that the handler can set the state.
  componentDidMount() {
    this.data = this.data.merge(this.props, {
      onClick: this.props.onClick.bind(this)
    });
  }

  // Renders the "<MyButton>" component with this
  // component's state as properties.
  render() {
    return <MyButton {...this.state.data.toJS()} />;
  }
}

// By default, the "onClick()" handler is a noop.
// This makes it easier because we can always assign
// the event handler to the "<button>".
MyButtonContainer.defaultProps = {
  onClick: () => {}
};

export default MyButtonContainer;

您有一个控制状态的容器组件,然后将其作为属性传递给<MyButton>

{...data}语法称为 JSX 扩展属性。这允许您将对象作为属性传递给元素。您可以在此处阅读更多关于此功能的信息。

该组件具有默认的onClick()处理函数。但是,您可以将不同的点击处理程序作为属性传递。此外,它会自动绑定到组件上下文,如果处理程序需要更改按钮状态,则这很有用。让我们看一个例子:

import React from 'react';
import { render } from 'react-dom';

import MyButtonContainer from './MyButtonContainer';

// Simple button event handler that changes the
// "disabled" state when clicked.
function onClick() {
  this.data = this.data.set('disabled', true);
}

render(
  <section>
    {/* A simple button with a simple label. */}
    <MyButtonContainer label="Text" />

    {/* A button with an icon, and a hidden label. */}
    <MyButtonContainer
      label="My Button"
      icon="ui-icon-person"
      showLabel={false}
    />

    {/* A button with a click event handler. */}
    <MyButtonContainer label="Disable Me" onClick={onClick} />
  </section>,
  document.getElementById('root')
);

在这里,您有三个 jQuery UI 按钮小部件,每个都由一个 React 组件控制,看不到任何命令式代码。按钮的外观如下:

在组件之后进行清理

在这一部分,您将学习如何在组件之后进行清理。您不必显式地从 DOM 中卸载组件-React 会为您处理。有一些 React 不知道的东西,因此在组件被移除后无法为您清理。

正是为了这些清理任务,componentWillUnmount()生命周期方法存在。清理 React 组件之后的一个用例是异步代码。

例如,想象一个组件,在组件首次挂载时发出 API 调用以获取一些数据。现在,想象一下,在 API 响应到达之前,该组件从 DOM 中移除。

清理异步调用

如果您的异步代码尝试设置已卸载的组件的状态,将不会发生任何事情。会记录一个警告,并且状态不会被设置。记录这个警告实际上非常重要;否则,您将很难解决微妙的竞争条件错误。

正确的方法是创建可取消的异步操作。这是你在本章前面实现的users() API 函数的修改版本:

// Adapted from:
// https://facebook.github.io/react/blog/2015/12/16/ismounted-antipattern.html
function cancellable(promise) {
  let cancelled = false;

  // Creates a wrapper promise to return. This wrapper is
  // resolved or rejected based on the wrapped promise, and
  // on the "cancelled" value.
  const promiseWrapper = new Promise((resolve, reject) => {
    promise.then(
      value => {
        return cancelled ? reject({ cancelled: true }) : resolve(value);
      },
      error => {
        return cancelled
          ? reject({ cancelled: true })
          : reject(error);
      }
    );
  });

  // Adds a "cancel()" method to the promise, for
  // use by the React component in "componentWillUnmount()".
  promiseWrapper.cancel = function cancel() {
    cancelled = true;
  };

  return promiseWrapper;
}

export function users(fail) {
  // Make sure that the returned promise is "cancellable", by
  // wrapping it with "cancellable()".
  return cancellable(
    new Promise((resolve, reject) => {
      setTimeout(() => {
        if (fail) {
          reject(fail);
        } else {
          resolve({
            users: [
              { id: 0, name: 'First' },
              { id: 1, name: 'Second' },
              { id: 2, name: 'Third' }
            ]
          });
        }
      }, 4000);
    })
  );
}

关键是cancellable()函数,它用新的 promise 包装了一个 promise。新的 promise 有一个cancel()方法,如果调用则拒绝 promise。它不会改变 promise 同步的实际异步行为。然而,它确实为在 React 组件中使用提供了一个通用和一致的接口。

现在让我们看一个具有取消异步行为能力的容器组件:

import React, { Component } from 'react';
import { fromJS } from 'immutable';
import { render } from 'react-dom';

import { users } from './api';
import UserList from './UserList';

// When the "cancel" link is clicked, we want to render
// a new element in "#app". This will unmount the
// "<UserListContainer>" component.
const onClickCancel = e => {
  e.preventDefault();

  render(<p>Cancelled</p>, document.getElementById('root'));
};

export default class UserListContainer extends Component {
  state = {
    data: fromJS({
      error: null,
      loading: 'loading...',
      users: []
    })
  };

  // Getter for "Immutable.js" state data...
  get data() {
    return this.state.data;
  }

  // Setter for "Immutable.js" state data...
  set data(data) {
    this.setState({ data });
  }

  componentDidMount() {
    // We have to store a reference to any async promises,
    // so that we can cancel them later when the component
    // is unmounted.
    this.job = users();

    this.job.then(
      result => {
        this.data = this.data
          .set('loading', null)
          .set('error', null)
          .set('users', fromJS(result.users));
      },

      // The "job" promise is rejected when it's cancelled.
      // This means that we need to check for the "cancelled"
      // property, because if it's true, this is normal
      // behavior.
      error => {
        if (!error.cancelled) {
          this.data = this.data
            .set('loading', null)
            .set('error', error);
        }
      }
    );
  }

  // This method is called right before the component
  // is unmounted. It is here, that we want to make sure
  // that any asynchronous behavior is cleaned up so that
  // it doesn't try to interact with an unmounted component.
  componentWillUnmount() {
    this.job.cancel();
  }

  render() {
    return (
      <UserList onClickCancel={onClickCancel} {...this.data.toJS()} />
    );
  }
}

onClickCancel()处理程序实际上替换了用户列表。这调用了componentWillUnmount()方法,在那里您可以取消this.job。值得注意的是,当在componentDidMount()中进行 API 调用时,会在组件中存储对 promise 的引用。否则,您将无法取消异步调用。

在进行挂起的 API 调用期间呈现组件时,组件的样子如下:

使用错误边界包含错误

React 16 的一个新功能——错误边界——允许您处理意外的组件失败。与其让应用程序的每个组件都知道如何处理可能遇到的任何错误,错误边界是一个机制,您可以使用它来包装具有错误处理行为的组件。最好将错误边界视为 JSX 的try/catch语法。

让我们重新访问本章中的第一个示例,其中您使用 API 函数获取了组件数据。users()函数接受一个布尔参数,当为 true 时,会导致 promise 被拒绝。这是您想要处理的事情,但不一定是在进行 API 调用的组件中。实际上,UserListContainerUserList组件已经设置好了处理这样的 API 错误。挑战在于,如果有很多组件,这将是大量的错误处理代码。此外,错误处理是特定于一个 API 调用的——如果其他地方出了问题怎么办?

以下是您可以用于此示例的UserListContainer的修改后源代码:

import React, { Component } from 'react';
import { fromJS } from 'immutable';

import { users } from './api';
import UserList from './UserList';

export default class UserListContainer extends Component {
  state = {
    data: fromJS({
      error: null,
      loading: 'loading...',
      users: []
    })
  };

  // Getter for "Immutable.js" state data...
  get data() {
    return this.state.data;
  }

  // Setter for "Immutable.js" state data...
  set data(data) {
    this.setState({ data });
  }

  // When component has been rendered, "componentDidMount()"
  // is called. This is where we should perform asynchronous
  // behavior that will change the state of the component.
  // In this case, we're fetching a list of users from
  // the mock API.
  componentDidMount() {
    users(true).then(
      result => {
        // Populate the "users" state, but also
        // make sure the "error" and "loading"
        // states are cleared.
        this.data = this.data
          .set('loading', null)
          .set('error', null)
          .set('users', fromJS(result.users));
      },
      error => {
        // When an error occurs, we want to clear
        // the "loading" state and set the "error"
        // state.
        this.data = this.data
          .set('loading', null)
          .set('error', error);
      }
    );
  }

  render() {
    // If the error state has a string value in it, it
    // means that something went wrong during the asynchronous
    // data fetching for this component. You can just throw an
    // error using this string instead of rendering.
    if (this.data.get('error') !== null) {
      throw new Error(this.data.get('error'));
    }
    return <UserList {...this.data.toJS()} />;
  }
}

这个组件大部分与第一个示例中的相同。第一个区别是对users()的调用,现在它传递了 true:

componentDidMount() {
  users(true).then(
    ...

这个调用将失败,导致错误状态被设置。第二个区别在于render()方法:

if (this.data.get('error') !== null) {
  throw new Error(this.data.get('error'));
}

它不是将错误状态转发到UserList组件,而是通过抛出错误而不是尝试渲染更多组件将错误传递回组件树。这里的关键设计变化是,该组件现在假设在组件树的更高位置有某种错误边界,将相应地处理这些错误。

您可能想知道为什么错误在渲染时抛出,而不是在componentDidMount()中拒绝承诺时抛出。问题在于像这样异步获取数据意味着 React 内部实际上无法捕获从异步承诺处理程序中抛出的异常。对于可能导致组件失败的异步操作,最简单的解决方案是将错误存储在组件状态中,但如果存在错误,则在实际渲染任何内容之前抛出错误。

现在让我们创建错误边界本身:

import React, { Component } from 'react';

// A basic error boundary used to display error messages.
export default class ErrorBoundary extends Component {
  state = {
    error: null
  };

  // This lifecycle method is only called if a component
  // lower in the tree than this component throws an error.
  // You can handle the error however you like in this method,
  // including setting it as a state value so that it can be used
  // for rendering.
  componentDidCatch(error) {
    this.setState({ error });
  }

  // If there's no error, you can just render the boundary's
  // children as usual. If there's an error, you can render
  // the error message while ignoring the child components.
  render() {
    if (this.state.error === null) {
      return this.props.children;
    } else {
      return <strong>{this.state.error.toString()}</strong>;
    }
  }
}

这就是componentDidCatch()生命周期方法的用法,当它捕获到错误时,设置该组件的错误状态。当渲染时,如果设置了error状态,则渲染错误消息。否则,像往常一样渲染子组件。

以下是如何使用这个ErrorBoundary组件:

import React from 'react';
import { render } from 'react-dom';

import ErrorBoundary from './ErrorBoundary';
import UserListContainer from './UserListContainer';

// The <ErrorBoundary> component can wrap any component you need.
// You can also create different error boundary components that
// render errors differently.
render(
  <ErrorBoundary>
    <UserListContainer />
  </ErrorBoundary>,
  document.getElementById('root')
);

UserListContainer或其任何子级抛出的任何错误都将被ErrorBoundary捕获和处理:

现在,您可以删除传递给UserListContainer中的users()的参数,以阻止其失败。在UserList组件中,假设您有一个错误,尝试在数字上调用toUpperCase()

import React from 'react';
import { Map } from 'immutable';

// This component displays the passed-in "loading"
// property as italic text. If it's null, then
// nothing is rendered.
const LoadingMessage = ({ loading }) =>
  Map([[null, null]]).get(loading, <em>{loading}</em>);

export default ({
  error, // eslint-disable-line react/prop-types
  loading, // eslint-disable-line react/prop-types
  users // eslint-disable-line react/prop-types
}) => (
  <section>
    {/* Displays any loading messages, while
         waiting for the API... */}
    <LoadingMessage loading={loading} />

    {/* Attempts to render the user list but throws an
        error by attempting to call toUpperCase() on a number. */}
    <ul>
      {users.map(i => <li key={i.id.toUpperCase()}>{i.name}</li>)}
    </ul>
  </section>
);

您将获得不同的错误抛出,但由于它位于与先前错误相同的边界下,它将以相同的方式处理:

如果您使用create-react-appreact-scripts运行项目,您可能会注意到应用程序中的每个错误都会有一个错误叠加层,即使这些错误已被错误边界处理。如果您使用右上角的x关闭叠加层,您可以看到您的组件如何处理应用程序中的错误。

总结

在本章中,您学到了很多关于 React 组件生命周期的知识。我们首先讨论了为什么 React 组件首先需要生命周期。原来 React 不能自动完成所有工作,所以我们需要编写一些代码,在组件生命周期的适当时间运行。

接下来,您实现了几个组件,它们能够从 JSX 属性中获取初始数据并初始化它们的状态。然后,您学会了通过提供shouldComponentRender()方法来实现更高效的 React 组件。

您学会了如何隐藏一些组件需要实现的命令式代码,以及如何在异步行为之后进行清理。最后,您学会了如何使用 React 16 的新错误边界功能。

在接下来的章节中,您将学习一些技术,以确保您的组件被传递了正确的属性。

测试您的知识

  1. render()是一个生命周期方法吗?

  2. 是的,render()与任何其他生命周期方法没有区别。

  3. 不,render()只是用来获取组件的内容。

  4. 以下哪项是componentWillUnmount()方法的有效用法?

  5. 删除组件添加的 DOM 元素。

  6. 取消组件卸载时将失败的异步操作。

  7. 组件即将卸载时记录日志。

  8. 哪个生命周期方法被错误边界组件使用?

  9. componentDidCatch()

  10. componentWillCatch()

  11. componentError()

进一步阅读

您可以访问以下链接获取更多信息:

第七章:验证组件属性

在本章中,你将学习关于 React 组件中的属性验证。乍一看,这可能看起来很简单,但这是一个重要的主题,因为它可以使组件无 bug。我将从讨论可预测的结果开始,以及如何使组件在整个应用程序中具有可移植性。

接下来,你将通过一些 React 自带的类型检查属性验证器的示例进行学习。然后,你将学习一些更复杂的属性验证场景。最后,我将用一个示例来结束本章,展示如何实现自定义验证器。

了解预期结果

在 React 组件中的属性验证就像 HTML 表单中的字段验证。验证表单字段的基本原则是让用户知道他们提供了一个不可接受的值。理想情况下,验证错误消息应该清晰明了,以便用户可以轻松地解决问题。通过 React 组件属性验证,你正在做同样的事情——让意外值的情况变得容易修复。属性验证增强了开发人员的体验,而不是用户体验。

属性验证的关键方面是了解作为属性值传递到组件的内容。例如,如果你期望一个数组,而实际传递了一个布尔值,可能会出现问题。如果你使用prop-types React 验证包来验证属性值,那么你就知道传递了一些意外的内容。如果组件期望一个数组以便调用map()方法,如果传递了布尔值,它将失败,因为布尔值没有map()方法。然而,在这种失败发生之前,你会看到属性验证警告。

这并不是要通过属性验证来快速失败,而是为开发人员提供信息。当属性验证失败时,你知道作为组件属性提供了一些不应该有的内容。这是要找到代码中传递值的位置并修复它的问题。

快速失败是软件架构的一个特性,系统会完全崩溃,而不是继续以不一致的状态运行。

推广可移植组件

当您知道组件属性可以期望什么时,组件使用的上下文变得不那么重要。这意味着只要组件能够验证其属性值,组件在哪里使用实际上并不重要;它可以轻松地被任何功能使用。

如果您想要一个通用组件,可以跨应用程序功能进行移植,您可以编写组件验证代码,也可以编写在渲染时运行的防御性代码。编程防御性的挑战在于它削弱了声明式 React 组件的价值。使用 React 风格的属性验证,您可以避免编写防御性代码。相反,属性验证机制在某些情况下会发出警告,通知您需要修复某些问题。

防御性代码是在生产环境中需要考虑许多边缘情况的代码。在开发过程中无法检测到潜在问题时,例如 React 组件属性验证,编写防御性代码是必要的。

简单属性验证器

在本节中,您将学习如何使用prop-types包中提供的简单属性类型验证器。然后,您将学习如何接受任何属性值,以及如何将属性必需而不是可选

基本类型验证

让我们来看看处理 JavaScript 值最基本类型的验证器。您将经常使用这些验证器,因为您想知道一个属性是字符串还是函数,例如。这个例子还将介绍您在组件上设置验证所涉及的机制。这是组件;它只是使用基本标记呈现一些属性:

import React from 'react';
import PropTypes from 'prop-types';

const MyComponent = ({
  myString,
  myNumber,
  myBool,
  myFunc,
  myArray,
  myObject
}) => (
  <section>
    {/* Strings and numbers can be rendered
         just about anywhere. */}
    <p>{myString}</p>
    <p>{myNumber}</p>

    {/* Booleans are typically used as property values. */}
    <p>
      <input type="checkbox" defaultChecked={myBool} />
    </p>

    {/* Functions can return values, or be assigned as
         event handler property values. */}
    <p>{myFunc()}</p>

    {/* Arrays are typically mapped to produce new JSX elements. */}
    <ul>{myArray.map(i => <li key={i}>{i}</li>)}</ul>

    {/* Objects typically use their properties in some way. */}
    <p>{myObject.myProp}</p>
  </section>
);

// The "propTypes" specification for this component.
MyComponent.propTypes = {
  myString: PropTypes.string,
  myNumber: PropTypes.number,
  myBool: PropTypes.bool,
  myFunc: PropTypes.func,
  myArray: PropTypes.array,
  myObject: PropTypes.object
};

export default MyComponent;

属性验证机制有两个关键部分。首先,您有静态的propTypes属性。这是一个类级别的属性,而不是实例属性。当 React 找到propTypes时,它将使用此对象作为组件的属性规范。其次,您有来自prop-types包的PropTypes对象,其中包含几个内置的验证器函数。

PropTypes对象曾经是内置在 React 中的。它从 React 核心中分离出来,并移动到prop-types包中,因此成为了一个可选择使用的内容 - 这是 React 开发人员的一个请求,他们不使用属性验证。

在这个例子中,MyComponent有六个属性,每个属性都有自己的类型。当您查看propTypes规范时,可以看到这个组件将接受什么类型的值。让我们使用一些属性值来渲染这个组件:

import React from 'react';
import { render as renderJSX } from 'react-dom';

import MyComponent from './MyComponent';

// The properties that we'll pass to the component.
// Each property is a different type, and corresponds
// to the "propTypes" spec of the component.
const validProps = {
  myString: 'My String',
  myNumber: 100,
  myBool: true,
  myFunc: () => 'My Return Value',
  myArray: ['One', 'Two', 'Three'],
  myObject: { myProp: 'My Prop' }
};

// These properties don't correspond to the "<MyComponent>"
// spec, and will cause warnings to be logged.
const invalidProps = {
  myString: 100,
  myNumber: 'My String',
  myBool: () => 'My Reaturn Value',
  myFunc: true,
  myArray: { myProp: 'My Prop' },
  myObject: ['One', 'Two', 'Three']
};

// Renders "<MyComponent>" with the given "props".
function render(props) {
  renderJSX(
    <MyComponent {...props} />,
    document.getElementById('root')
  );
}

render(validProps);
render(invalidProps);

第一次渲染<MyComponent>时,它使用validProps属性。这些值都符合组件属性规范,因此控制台中不会记录任何警告。第二次,使用invalidProps属性,这将导致属性验证失败,因为每个属性中都使用了错误的类型。控制台输出应该类似于以下内容:

Invalid prop `myString` of type `number` supplied to `MyComponent`, expected `string` 
Invalid prop `myNumber` of type `string` supplied to `MyComponent`, expected `number` 
Invalid prop `myBool` of type `function` supplied to `MyComponent`, expected `boolean` 
Invalid prop `myFunc` of type `boolean` supplied to `MyComponent`, expected `function` 
Invalid prop `myArray` of type `object` supplied to `MyComponent`, expected `array` 
Invalid prop `myObject` of type `array` supplied to `MyComponent`, expected `object` 
TypeError: myFunc is not a function 

最后一个错误很有趣。您可以清楚地看到属性验证正在抱怨无效的属性类型。这包括传递给myFunc的无效函数。因此,尽管在属性上进行了类型检查,但组件仍会尝试调用该值,就好像它是一个函数一样。

渲染输出如下所示:

再次强调,React 组件中属性验证的目的是帮助您在开发过程中发现错误。当 React 处于生产模式时,属性验证将完全关闭。这意味着您不必担心编写昂贵的属性验证代码;它永远不会在生产中运行。但是错误仍然会发生,所以要修复它。

要求值

让我们对前面的示例进行一些调整。组件属性规范需要特定类型的值,但只有在将属性作为 JSX 属性传递给组件时才会进行检查。例如,您可以完全省略myFunc属性,它也会通过验证。幸运的是,PropTypes函数有一个工具,让您可以指定必须提供属性并且必须具有特定类型。以下是修改后的组件:

import React from 'react';
import PropTypes from 'prop-types';

const MyComponent = ({
  myString,
  myNumber,
  myBool,
  myFunc,
  myArray,
  myObject
}) => (
  <section>
    <p>{myString}</p>
    <p>{myNumber}</p>
    <p>
      <input type="checkbox" defaultChecked={myBool} />
    </p>
    <p>{myFunc()}</p>
    <ul>{myArray.map(i => <li key={i}>{i}</li>)}</ul>
    <p>{myObject.myProp}</p>
  </section>
);

// The "propTypes" specification for this component. Every
// property is required, because they each have the
// "isRequired" property.
MyComponent.propTypes = {
  myString: PropTypes.string.isRequired,
  myNumber: PropTypes.number.isRequired,
  myBool: PropTypes.bool.isRequired,
  myFunc: PropTypes.func.isRequired,
  myArray: PropTypes.array.isRequired,
  myObject: PropTypes.object.isRequired
};

export default MyComponent; 

这个组件和前面部分实现的组件之间没有太多变化。主要区别在于propTypes中的规格。isRequired值被附加到每个使用的类型验证器上。因此,例如,string.isRequired表示属性值必须是字符串,并且属性不能为空。现在让我们测试一下这个组件:

import React from 'react';
import { render as renderJSX } from 'react-dom';

import MyComponent from './MyComponent';

const validProps = {
  myString: 'My String',
  myNumber: 100,
  myBool: true,
  myFunc: () => 'My Return Value',
  myArray: ['One', 'Two', 'Three'],
  myObject: { myProp: 'My Prop' }
};

// The same as "validProps", except it's missing
// the "myObject" property. This will trigger a
// warning.
const missingProp = {
  myString: 'My String',
  myNumber: 100,
  myBool: true,
  myFunc: () => 'My Return Value',
  myArray: ['One', 'Two', 'Three']
};

// Renders "<MyComponent>" with the given "props".
function render(props) {
  renderJSX(
    <MyComponent {...props} />,
    document.getElementById('root')
  );
}

render(validProps);
render(missingProp);

第一次渲染时,组件使用了所有正确的属性类型。第二次渲染时,组件没有使用 myObject 属性。控制台错误应该如下:

Required prop `myObject` was not specified in `MyComponent`. 
Cannot read property 'myProp' of undefined 

由于属性规范和后续对 myObject 的错误消息,很明显需要为 myObject 属性提供一个对象值。最后一个错误是因为组件假设存在一个具有 myProp 作为属性的对象。

理想情况下,在这个例子中,你应该验证 myProp 对象属性,因为它直接用在 JSX 中。在 JSX 标记中使用的特定属性可以验证对象的形状,正如你将在本章后面看到的那样。

任何属性值

本节的最后一个主题是 any 属性验证器。也就是说,它实际上并不关心它得到什么值——任何值都是有效的,包括根本不传递值。事实上,isRequired 验证器可以与 any 验证器结合使用。例如,如果你正在开发一个组件,你只想确保传递了某些东西,但还不确定你将需要哪种类型,你可以做类似这样的事情:myProp: PropTypes.any.isRequired

拥有 any 属性验证器的另一个原因是为了一致性。每个组件都应该有属性规范。在开始时,any 验证器是有用的,当你不确定属性类型时。你至少可以开始属性规范,然后随着事情的展开逐渐完善它。

现在让我们来看一些代码:

import React from 'react';
import PropTypes from 'prop-types';

// Renders a component with a header and a simple
// progress bar, using the provided property
// values.
const MyComponent = ({ label, value, max }) => (
  <section>
    <h5>{label}</h5>
    <progress {...{ max, value }} />
  </section>
);

// These property values can be anything, as denoted by
// the "PropTypes.any" prop type.
MyComponent.propTypes = {
  label: PropTypes.any,
  value: PropTypes.any,
  max: PropTypes.any
};

export default MyComponent;

这个组件实际上并不验证任何东西,因为它的属性规范中的三个属性将接受任何东西。然而,这是一个很好的起点,因为乍一看,我就可以看到这个组件使用的三个属性的名称。所以以后,当我决定这些属性应该具有哪些类型时,更改是简单的。现在让我们看看这个组件的实际效果:

import React from 'react';
import { render } from 'react-dom';

import MyComponent from './MyComponent';

render(
  <section>
    {/* Passes a string and two numbers to
         "<MyComponent>". Everything works as
         expected. */}
    <MyComponent label="Regular Values" max={20} value={10} />

    {/* Passes strings instead of numbers to the
         progress bar, but they're correctly
         interpreted as numbers. */}
    <MyComponent label="String Values" max="20" value="10" />

    {/* The "label" has no issue displaying
         "MAX_SAFE_INTEGER", but the date that's
         passed to "max" causes the progress bar
         to break. */}
    <MyComponent
      label={Number.MAX_SAFE_INTEGER}
      max={new Date()}
      value="10"
    />
  </section>,
  document.getElementById('root')
);

字符串和数字在几个地方是可以互换的。只允许其中一个似乎过于限制了。正如你将在下一节中看到的,React 还有其他属性验证器,允许你进一步限制组件允许的属性值。

我们的组件在渲染时是这样的:

类型和值验证器

在这一部分,你将学习 React prop-types包中更高级的验证功能。首先,你将学习检查可以在 HTML 标记内渲染的值的元素和节点验证器。然后,你将看到如何检查特定类型,超出了你刚刚学到的原始类型检查。最后,你将实现寻找特定值的验证。

可以渲染的东西

有时,你只想确保属性值是可以由 JSX 标记渲染的东西。例如,如果属性值是一组普通对象,这不能通过将其放在{}中来渲染。你必须将数组项映射到 JSX 元素。

这种检查特别有用,如果你的组件将属性值传递给其他元素作为子元素。让我们看一个例子,看看这是什么样子的:

import React from 'react';
import PropTypes from 'prop-types';

const MyComponent = ({ myHeader, myContent }) => (
  <section>
    <header>{myHeader}</header>
    <main>{myContent}</main>
  </section>
);

// The "myHeader" property requires a React
// element. The "myContent" property requires
// a node that can be rendered. This includes
// React elements, but also strings.
MyComponent.propTypes = {
  myHeader: PropTypes.element.isRequired,
  myContent: PropTypes.node.isRequired
};

export default MyComponent;

这个组件有两个属性,需要渲染数值。myHeader属性需要一个element,可以是任何 JSX 元素。myContent属性需要一个node,可以是任何 JSX 元素或任何字符串值。让我们给这个组件传递一些值并渲染它:

import React from 'react';
import { render } from 'react-dom';

import MyComponent from './MyComponent';

// Two React elements we'll use to pass to
// "<MyComponent>" as property values.
const myHeader = <h1>My Header</h1>;
const myContent = <p>My Content</p>;

render(
  <section>
    {/* Renders as expected, both properties are passed
         React elements as values. */}
    <MyComponent {...{ myHeader, myContent }} />

    {/* Triggers a warning because "myHeader" is expecting
         a React element instead of a string. */}
    <MyComponent myHeader="My Header" {...{ myContent }} />

    {/* Renders as expected. A string is a valid type for
         the "myContent" property. */}
    <MyComponent {...{ myHeader }} myContent="My Content" />

    {/* Renders as expected. An array of React elements
         is a valid type for the "myContent" property. */}
    <MyComponent
      {...{ myHeader }}
      myContent={[myContent, myContent, myContent]}
    />
  </section>,
  document.getElementById('root')
);

myHeader属性对其接受的值更加严格。myContent属性将接受一个字符串、一个元素或一个元素数组。当从属性中传递子数据时,这两个验证器非常重要,就像这个组件所做的那样。例如,尝试将一个普通对象或函数作为子元素传递将不起作用,最好使用验证器检查这种情况。

当渲染时,这个组件看起来是这样的:

需要特定类型

有时,你需要一个属性验证器,检查你的应用程序定义的类型。例如,假设你有以下用户类:

import cuid from 'cuid';

// Simple class the exposes an API that the
// React component expects.
export default class MyUser {
  constructor(first, last) {
    this.id = cuid();
    this.first = first;
    this.last = last;
  }

  get name() {
    return `${this.first} ${this.last}`;
  }
}

现在,假设你有一个组件想要使用这个类的实例作为属性值。你需要一个验证器来检查属性值是否是MyUser的实例。让我们实现一个做到这一点的组件:

import React from 'react';
import PropTypes from 'prop-types';

import MyUser from './MyUser';

const MyComponent = ({ myDate, myCount, myUsers }) => (
  <section>
    {/* Requires a specific "Date" method. */}
    <p>{myDate.toLocaleString()}</p>

    {/* Number or string works here. */}
    <p>{myCount}</p>
    <ul>
      {/* "myUsers" is expected to be an array of
           "MyUser" instances. So we know that it's
           safe to use the "id" and "name" property. */}
      {myUsers.map(i => <li key={i.id}>{i.name}</li>)}
    </ul>
  </section>
);

// The properties spec is looking for an instance of
// "Date", a choice between a string or a number, and
// an array filled with specific types.
MyComponent.propTypes = {
  myDate: PropTypes.instanceOf(Date),
  myCount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  myUsers: PropTypes.arrayOf(PropTypes.instanceOf(MyUser))
};

export default MyComponent; 

这个组件有三个需要特定类型的属性,每一个都超出了本章中到目前为止所见的基本类型验证器。让我们现在逐步了解这些:

  • myDate需要一个Date的实例。它使用instanceOf()函数来构建一个验证函数,确保值是Date的实例。

  • myCount 要求值要么是一个数字,要么是一个字符串。这个验证器函数是通过结合 oneOfTypePropTypes.number()PropTypes.string() 创建的。

  • myUsers 需要一个 MyUser 实例的数组。这个验证器是通过结合 arrayOf()instanceOf() 构建的。

这个例子说明了通过结合 React 提供的属性验证器可以处理的场景数量。渲染输出如下:

需要特定的值

到目前为止,我专注于验证属性值的类型,但这并不总是你想要检查的。有时候,特定的值很重要。让我们看看如何验证特定的属性值:

import React from 'react';
import PropTypes from 'prop-types';

// Any one of these is a valid "level"
// property value.
const levels = new Array(10).fill(null).map((v, i) => i + 1);

// This is the "shape" of the object we expect
// to find in the "user" property value.
const userShape = {
  name: PropTypes.string,
  age: PropTypes.number
};

const MyComponent = ({ level, user }) => (
  <section>
    <p>{level}</p>
    <p>{user.name}</p>
    <p>{user.age}</p>
  </section>
);

// The property spec for this component uses
// "oneOf()" and "shape()" to define the required
// property values.
MyComponent.propTypes = {
  level: PropTypes.oneOf(levels),
  user: PropTypes.shape(userShape)
};

export default MyComponent; 

level 属性预期是来自 levels 数组的数字。这很容易使用 oneOf() 函数进行验证。user 属性预期一个特定的形状。形状是对象的预期属性和类型。在这个例子中定义的 userShape 需要一个 name 字符串和一个 age 数字。shape()instanceOf() 之间的关键区别是你不一定关心类型。你可能只关心组件 JSX 中使用的值。

让我们看看这个组件是如何使用的:

import React from 'react';
import { render } from 'react-dom';

import MyComponent from './MyComponent';

render(
  <section>
    {/* Works as expected. */}
    <MyComponent level={10} user={{ name: 'Name', age: 32 }} />

    {/* Works as expected, the "online"
         property is ignored. */}
    <MyComponent user={{ name: 'Name', age: 32, online: false }} />

    {/* Fails. The "level" value is out of range,
         and the "age" property is expecting a
         number, not a string. */}
    <MyComponent level={11} user={{ name: 'Name', age: '32' }} />
  </section>,
  document.getElementById('root')
);

组件渲染时的样子如下:

编写自定义属性验证器

在这最后一节中,你将学习如何构建自己的自定义属性验证函数,并将它们应用在属性规范中。一般来说,只有在绝对必要的情况下才应该实现自己的属性验证器。prop-types 中提供的默认验证器涵盖了广泛的场景。

然而,有时候,你需要确保非常特定的属性值被传递给组件。记住,这些不会在生产模式下运行,所以验证器函数迭代集合是完全可以接受的。现在让我们实现一些自定义验证器函数:

import React from 'react';

const MyComponent = ({ myArray, myNumber }) => (
  <section>
    <ul>{myArray.map(i => <li key={i}>{i}</li>)}</ul>
    <p>{myNumber}</p>
  </section>
);

MyComponent.propTypes = {
  // Expects a property named "myArray" with a non-zero
  // length. If this passes, we return null. Otherwise,
  // we return a new error.
  myArray: (props, name, component) =>
    Array.isArray(props[name]) && props[name].length
      ? null
      : new Error(`${component}.${name}: expecting non-empty array`),

  // Expects a property named "myNumber" that's
  // greater than 0 and less than 99\. Otherwise,
  // we return a new error.
  myNumber: (props, name, component) =>
    Number.isFinite(props[name]) &&
    props[name] > 0 &&
    props[name] < 100
      ? null
      : new Error(
          `${component}.${name}: expecting number between 1 and 99`
        )
};

export default MyComponent;

myArray 属性预期一个非空数组,myNumber 属性预期一个大于 0 且小于 100 的数字。让我们尝试传递一些数据给这些验证器:

import React from 'react';
import { render } from 'react-dom';

import MyComponent from './MyComponent';

render(
  <section>
    {/* Renders as expected... */}
    <MyComponent
      myArray={['first', 'second', 'third']}
      myNumber={99}
    />

    {/* Both custom validators fail... */}
    <MyComponent myArray={[]} myNumber={100} />
  </section>,
  document.getElementById('root')
);

第一个元素渲染得很好,因为这两个验证器都返回 null。然而,空数组和数字 100 导致这两个验证器都返回错误:

MyComponent.myArray: expecting non-empty array 
MyComponent.myNumber: expecting number between 1 and 99 

渲染输出如下:

摘要

本章的重点是 React 组件属性验证。当您实施属性验证时,您知道可以期望什么;这有助于可移植性。组件不关心属性值是如何传递给它的,只要它们是有效的即可。

然后,您将使用基本的 React 验证器来处理几个示例,这些验证器检查原始 JavaScript 类型。您还了解到,如果属性是必需的,必须明确指出。接下来,您将学习如何通过组合 React 提供的内置验证器来验证更复杂的属性值。

最后,您将实现自己的自定义验证器函数,以执行超出prop-types验证器可能的验证。在下一章中,您将学习如何通过新数据和行为扩展 React 组件。

测试您的知识

  1. 以下是描述prop-types包的最佳描述之一?

  2. 用于编译 React 组件的强类型 JavaScript 实用程序。

  3. 用于在开发过程中验证传递给组件的 prop 值的工具。

  4. 用于在生产环境中验证传递给组件的 prop 值的工具。

  5. 如何验证属性值是否可以呈现?

  6. 如果它具有toString()函数,则足以呈现它。

  7. 使用PropTypes.node验证器。

  8. 使用PropTypes.renderable验证器。

  9. PropTypes.shape 验证器的目的是什么?

  10. 确保对象具有特定类型的特定属性,忽略任何其他属性。

  11. 确保作为 prop 传递的对象是特定类的对象。

  12. 确保对象具有特定的属性名称。

进一步阅读

第八章:扩展组件

在本章中,您将学习如何通过扩展现有组件来添加新的功能。有两种 React 机制可以用来扩展组件:

  • 组件继承

  • 使用高阶组件进行组合

您将首先学习基本组件继承,就像面向对象的类继承一样。然后,您将实现一些用于组合 React 组件的高阶组件。

组件继承

组件就是类。事实上,当您使用 ES2015 类语法实现组件时,您会从 React 扩展基类 Component。您可以继续像这样扩展您的类,以创建自己的基本组件。

在本节中,您将看到您的组件可以继承状态、属性,以及几乎任何其他东西,包括 JSX 标记和事件处理程序。

继承状态

有时,您有几个使用相同初始状态的 React 组件。您可以实现一个设置此初始状态的基本组件。然后,想要使用此作为其初始状态的任何组件都可以扩展此组件。让我们实现一个设置一些基本状态的基本组件:

import { Component } from 'react';
import { fromJS } from 'immutable';

export default class BaseComponent extends Component {
  state = {
    data: fromJS({
      name: 'Mark',
      enabled: false,
      placeholder: ''
    })
  };

  // Getter for "Immutable.js" state data...
  get data() {
    return this.state.data;
  }

  // Setter for "Immutable.js" state data...
  set data(data) {
    this.setState({ data });
  }

  // The base component doesn't actually render anything,
  // but it still needs a render method.
  render() {
    return null;
  }
}

状态是不可变的 Map。这个基本组件还实现了不可变数据的设置和获取方法。让我们实现一个扩展了这个组件的组件:

import React from 'react';
import BaseComponent from './BaseComponent';

// Extends "BaseComponent" to inherit the
// initial component state.
export default class MyComponent extends BaseComponent {
  // This is our chance to build on the initial state.
  // We change the "placeholder" text and mark it as
  // "enabled".
  componentDidMount() {
    this.data = this.data.merge({
      placeholder: 'Enter a name...',
      enabled: true
    });
  }

  // Used to set the name state whenever the input
  // value changes.
  onChange = ({ target: { value } }) => {
    this.data = this.data.set('name', value);
  };

  // Renders a simple input element, that uses the
  // state of this component as properties.
  render() {
    const { enabled, name, placeholder } = this.data.toJS();

    return (
      <label htmlFor="my-input">
        Name:
        <input
          type="text"
          id="my-input"
          disabled={!enabled}
          placeholder={placeholder}
          value={name}
          onChange={this.onChange}
        />
      </label>
    );
  }
}

这个组件实际上不需要设置任何初始状态,因为它已经被 BaseComponent 设置了。由于状态已经是不可变的 Map,您可以在 componentDidMount() 中使用 merge() 调整初始状态。渲染输出如下所示:

如果您删除输入元素中的默认文本,您会发现 MyComponent 添加到初始状态的占位文本会如预期般应用:

您还可以将文本更改为其他内容,onChange() 事件处理程序将相应地设置 name 状态。

继承属性

通过将默认属性值和属性类型定义为基类的静态属性,来实现属性继承。从这个基类继承的任何类也会继承属性值和属性规范。让我们来看一个基类的实现:

import { Component } from 'react';
import PropTypes from 'prop-types';

export default class BaseComponent extends Component {
  // The specifiction for these base properties.
  static propTypes = {
    users: PropTypes.array.isRequired,
    groups: PropTypes.array.isRequired
  };

  // The default values of these base properties.
  static defaultProps = {
    users: [],
    groups: []
  };

  render() {
    return null;
  }
} 

这个类本身实际上并没有做任何事情。定义它的唯一原因是为了声明默认的属性值和它们的类型约束的地方。分别是defaultPropspropTypes静态类属性。

现在,让我们看一个继承这些属性的组件:

import React from 'react';
import { Map } from 'immutable';

import BaseComponent from './BaseComponent';

// Renders the given "text" as a header, unless
// the given "length" is 0.
const SectionHeader = ({ text, length }) =>
  Map([[0, null]]).get(length, <h1>{text}>/h1>);

export default class MyComponent extends BaseComponent {
  render() {
    const { users, groups } = this.props;

    // Renders the "users" and "groups" arrays. There
    // are not property validators or default values
    // in this component, since these are declared in
    // "BaseComponent".
    return (
      <section>
        <SectionHeader text="Users" length={users.length} />
        <ul>{users.map(i => <li key={i}>{i}</li>)}</ul>

        <SectionHeader text="Groups" length={groups.length} />
        <ul>{groups.map(i => <li key={i}>{i}</li>)}</ul>
      </section>
    );
  }
}

让我们尝试渲染MyComponent以确保继承的属性按预期工作:

import React from 'react';
import { render } from 'react-dom';

import ErrorBoundary from './ErrorBoundary';
import MyComponent from './MyComponent';

const users = ['User 1', 'User 2'];

const groups = ['Group 1', 'Group 2'];

render(
  <section>
    {/* Renders as expected, using the defaults. */}
    <ErrorBoundary>
      <MyComponent />
    </ErrorBoundary>

    {/* Renders as expected, using the "groups" default. */}
    <ErrorBoundary>
      <MyComponent users={users} />
      <hr />
    </ErrorBoundary>

    {/* Renders as expected, using the "users" default. */}
    <ErrorBoundary>
      <MyComponent groups={groups} />
      <hr />
    </ErrorBoundary>

    {/* Renders as expected, providing property values. */}
    <ErrorBoundary>
      <MyComponent users={users} groups={groups} />
    </ErrorBoundary>

    {/* Fails to render, the property validators in the base
         component detect the invalid number type. */}
    <ErrorBoundary>
      <MyComponent users={0} groups={0} />
    </ErrorBoundary>
  </section>,
  document.getElementById('root')
);

尽管MyComponent没有定义任何属性默认值或类型,但你会得到预期的行为。当你尝试将数字传递给usersgroups属性时,你不会看到任何渲染。这是因为MyComponent期望这些属性值上有一个“map()”方法,而实际上并没有。

这里使用ErrorBoundary元素来隔离错误。如果没有它们,任何MyComponent元素失败都会导致页面上的其他组件也失败,例如,通过将数字值传递给用户和组。下面是ErrorBoundary组件的样子:

import { Component } from 'react';

// Uses the componentDidCatch() method to set the
// error state of this component. When rendering,
// if there's an error it gets logged and nothing
// is rendered.
export default class ErrorBoundary extends Component {
  state = { error: null };

  componentDidCatch(error) {
    this.setState({ error });
  }

  render() {
    if (this.state.error === null) {
      return this.props.children;
    } else {
      console.error(this.state.error);
      return null;
    }
  }
}

这个组件使用了你在第六章中学到的“componentDidCatch()”生命周期方法。如果捕获到错误,它会设置错误状态,以便“render()”方法知道不再渲染导致错误的组件。下面是渲染的内容:

继承 JSX 和事件处理程序

在本节中,你将学习如何继承 JSX 和事件处理程序。如果你有一个单一的 UI 组件,它具有相同的 UI 元素和事件处理逻辑,但在组件使用的位置上初始状态有所不同,那么你可能想使用这种方法。

例如,一个基类会定义 JSX 和事件处理程序方法,而更具体的组件会定义特定于功能的初始状态。下面是一个基类的例子:

import React, { Component } from 'react';
import { fromJS } from 'immutable';

export default class BaseComponent extends Component {
  state = {
    data: fromJS({
      items: []
    })
  };

  // Getter for "Immutable.js" state data...
  get data() {
    return this.state.data;
  }

  // Setter for "Immutable.js" state data...
  set data(data) {
    this.setState({ data });
  }

  // The click event handler for each item in the
  // list. The context is the lexically-bound to
  // this component.
  onClick = id => () => {
    this.data = this.data.update('items', items =>
      items.update(
        items.indexOf(items.find(i => i.get('id') === id)),
        item => item.update('done', d => !d)
      )
    );
  };

  // Renders a list of items based on the state
  // of the component. The style of the item
  // depends on the "done" property of the item.
  // Each item is assigned an event handler that
  // toggles the "done" state.
  render() {
    const { items } = this.data.toJS();

    return (
      <ul>
        {items.map(i => (
          <li
            key={i.id}
            onClick={this.onClick(i.id)}
            style={{
              cursor: 'pointer',
              textDecoration: i.done ? 'line-through' : 'none'
            }}
          >
            {i.name}
          </li>
        ))}
      </ul>
    );
  }
} 

这个基础组件渲染一个项目列表,当点击时,切换项目文本的样式。默认情况下,这个组件的状态有一个空的项目列表。这意味着可以安全地渲染这个组件,而不设置组件状态。然而,这并不是很有用,所以让我们通过继承基础组件并设置状态来给这个列表添加一些项目:

import BaseComponent from './BaseComponent';

export default class MyComponent extends BaseComponent {
  // Initializes the component state, by using the
  // "data" getter method from "BaseComponent".
  componentDidMount() {
    this.data = this.data.merge({
      items: [
        { id: 1, name: 'One', done: false },
        { id: 2, name: 'Two', done: false },
        { id: 3, name: 'Three', done: false }
      ]
    });
  }
} 

componentDidMount()生命周期方法可以安全地设置组件的状态。基本组件使用您的data设置器/获取器来改变组件的状态。这种方法的另一个方便之处是,如果您想要覆盖基本组件的事件处理程序之一,您可以在MyComponent中定义该方法。

渲染时,列表的样子如下:

当所有项目都被点击时,列表的样子如下:

使用高阶组件进行组合

在本节中,您将了解高阶组件。如果您熟悉函数式编程中的高阶函数,高阶组件的工作方式是相同的。高阶函数是一个以另一个函数作为输入的函数,并返回一个新函数作为输出。返回的函数以某种方式调用原始函数。其思想是通过现有行为组合新行为。

使用高阶 React 组件,您有一个以组件作为输入的函数,并返回一个新组件作为输出。这是在 React 应用程序中组合新行为的首选方式,而且似乎许多流行的 React 库正在朝着这个方向发展,如果它们还没有的话。通过这种方式组合功能时,您会获得更多的灵活性。

条件组件渲染

高阶组件的一个用例是条件渲染。例如,根据谓词的结果,渲染组件或不渲染任何内容。谓词可以是特定于应用程序的任何内容,比如权限或类似的东西。

假设您有以下组件:

import React from 'react';

// The world's simplest component...
export default () => <p>My component...</p>; 

现在,要控制此组件的显示,您可以用另一个组件包装它。包装由高阶函数处理。

如果在 React 的上下文中听到“包装器”这个术语,它可能指的是高阶组件。基本上,它的作用是包装您传递给它的组件。

现在,让我们创建一个高阶 React 组件:

import React from 'react';

// A minimal higher-order function is all it
// takes to create a component repeater. Here, we're
// returning a function that calls "predicate()".
// If this returns true, then the rendered
// "<Component>" is returned.
export default (Component, predicate) => props =>
  predicate() && <Component {...props} />; 

这个函数的两个参数是Component,即您要包装的组件,和要调用的predicate。如果对predicate()的调用返回true,那么将返回<Component>。否则,将不会渲染任何内容。

现在,让我们实际使用这个函数来组合一个新的组件,以及渲染一个段落文本的组件:

import React from 'react';
import { render } from 'react-dom';

import cond from './cond';
import MyComponent from './MyComponent';

// Two compositions of "MyComponent". The
// "ComposedVisible" version will render
// because the predicate returns true. The
// "ComposedHidden" version doesn't render.
const ComposedVisible = cond(MyComponent, () => true);
const ComposedHidden = cond(MyComponent, () => false);

render(
  <section>
    <h1>Visible</h1>
    <ComposedVisible />
    <h2>Hidden</h2>
    <ComposedHidden />
  </section>,
  document.getElementById('root')
); 

您刚刚使用MyComponentcond()predicate函数创建了两个新组件。这是渲染输出:

提供数据源

让我们通过查看一个更复杂的高阶组件示例来完成本章。您将实现一个数据存储函数,用数据源包装给定的组件。了解这种模式很有用,因为它被 React 库(如Redux)使用。这是用于包装组件的connect()函数:

import React, { Component } from 'react';
import { fromJS } from 'immutable';

// The components that are connected to this store.
let components = fromJS([]);

// The state store itself, where application data is kept.
let store = fromJS({});

// Sets the state of the store, then sets the
// state of every connected component.
export function setState(state) {
  store = state;

  for (const component of components) {
    component.setState({
      data: store
    });
  }
}

// Returns the state of the store.
export function getState() {
  return store;
}

// Returns a higher-order component that's connected
// to the "store".
export function connect(ComposedComponent) {
  return class ConnectedComponent extends Component {
    state = { data: store };

    // When the component is mounted, add it to "components",
    // so that it will receive updates when the store state
    // changes.
    componentDidMount() {
      components = components.push(this);
    }

    // Deletes this component from "components" when it is
    // unmounted from the DOM.
    componentWillUnmount() {
      const index = components.findIndex(this);
      components = components.delete(index);
    }

    // Renders "ComposedComponent", using the "store" state
    // as properties.
    render() {
      return <ComposedComponent {...this.state.data.toJS()} />;
    }
  };
} 

这个模块定义了两个内部不可变对象:componentsstorecomponents列表保存了监听store变化的组件的引用。store代表整个应用程序状态。

存储的概念源自Flux,这是一组用于构建大规模 React 应用程序的架构模式。我将在本书中介绍 Flux 的想法,但 Flux 远远超出了本书的范围。

这个模块的重要部分是导出的函数:setState()getState()connect()getState()函数简单地返回对数据存储的引用。setState()函数设置存储的状态,然后通知所有组件应用程序的状态已更改。connect()函数是一个高阶函数,用一个新的组件包装给定的组件。当组件被挂载时,它会在存储中注册自己,以便在存储更改状态时接收更新。它通过将store作为属性传递来呈现组合的组件。

现在,让我们使用这个实用程序来构建一个简单的过滤器和列表。首先是列表组件:

import React from 'react';
import PropTypes from 'prop-types';

// Renders an item list...
const MyList = ({ filterValue, items }) => {
  const filter = new RegExp(filterValue, 'i');

  return (
    <ul>
      {items
        .filter(item => filter.test(item))
        .map(item => <li key={item}>{item}>/li>)}
    </ul>
  );
};

MyList.propTypes = {
  items: PropTypes.array.isRequired
};

export default MyList; 

有两个状态片段作为属性传递给这个组件。第一个是来自过滤文本输入的filterValue字符串。第二个是要过滤的值数组items。通过构建一个不区分大小写的正则表达式并在filter()内部使用它来进行过滤。然后,只有与filterValue匹配的项目才是这个组件的 JSX 输出的一部分。接下来,让我们看一下MyInput

import React from 'react';
import PropTypes from 'prop-types';
import { getState, setState } from './store';

// When the filter input value changes.
function onChange(e) {
  // Updates the state of the store.
  setState(getState().set('filterValue', e.target.value));
}

// Renders a simple input element to filter a list.
const MyInput = ({ value, placeholder }) => (
  <input
    autoFocus
    value={value}
    placeholder={placeholder}
    onChange={onChange}
  />
);

MyInput.propTypes = {
  value: PropTypes.string,
  placeholder: PropTypes.string
};

export default MyInput;

MyInput组件呈现一个<input>元素。onChange()处理程序的目标是过滤用户列表,以便仅显示包含当前输入文本的项目。它通过在文本输入更改时设置filterValue状态来实现此目的。这将导致MyList组件使用新的过滤值重新呈现以过滤项目。

这是渲染的过滤输入和项目列表的样子:

摘要

在本章中,您了解了扩展现有组件的不同方法。您了解的第一种机制是继承。这是使用 ES2015 类语法完成的,对于实现常见方法或 JSX 标记非常有用。

然后,您了解了高阶组件,其中您使用函数来包装一个组件,以便为其提供新的功能。这是新的 React 应用程序正在向其移动的方向,而不是继承。

在下一章中,您将学习如何根据当前 URL 渲染组件。

测试你的知识

  1. 何时应该继承组件状态?

  2. 您不应该继承组件状态

  3. 只有当您有许多不同的组件都共享相同的状态结构,但呈现不同的输出时

  4. 只有当您想要在两个或更多组件之间共享状态时

  5. 什么是高阶组件?

  6. 由另一个组件渲染的组件

  7. 功能组件的另一个名称

  8. 返回另一个组件的组件

  9. 如果您从组件继承 JSX,您应该覆盖什么?

  10. 没有。您只是继承以为组件提供一个新名称。

  11. 您应该只覆盖状态。

  12. 您可以在componentDidMount()中将新的状态值传递给继承的组件。

进一步阅读

第九章:处理路由导航

几乎每个 Web 应用程序都需要路由:根据一组路由处理程序声明来响应 URL 的过程。换句话说,从 URL 到渲染内容的映射。然而,这个任务比起初看起来更加复杂。这就是为什么在本章中您将利用react-router包,这是 React 的事实上的路由工具。

首先,您将学习使用 JSX 语法声明路由的基础知识。然后,您将了解路由的动态方面,例如动态路径段和查询参数。接下来,您将使用react-router中的组件实现链接。

声明路由

使用react-router,您可以将路由与它们渲染的内容放在一起。在本节中,您将看到这是通过使用 JSX 语法来定义路由的。

您将创建一个基本的“hello world”示例路由,以便您可以看到在 React 应用程序中路由是什么样子的。然后,您将学习如何通过功能而不是在一个庞大的模块中组织路由声明。最后,您将实现一个常见的父子路由模式。

Hello route

让我们创建一个简单的路由,以渲染一个简单的组件。首先,当路由被激活时,您有一个小的 React 组件要渲染:

import React from 'react';

export default () => <p>Hello Route!</p>;

接下来,让我们看一下路由定义:

import React from 'react';
import { render } from 'react-dom';
import { BrowserRouter as Router, Route } from 'react-router-dom';

import MyComponent from './MyComponent';

// The "<Router>" is the root element of the app.
render(
  <Router>
    <Route exact path="/" component={MyComponent} />
  </Router>,
  document.getElementById('root')
);

Router组件是应用程序的顶层组件。让我们来分解一下,了解路由器内部发生了什么。

您已经将实际路由声明为<Route>元素。任何路由的两个关键属性是pathcomponent。当path与活动 URL 匹配时,将渲染component。但它到底是在哪里渲染的呢?Router组件实际上并不自己渲染任何内容;它负责根据当前 URL 管理其他组件的渲染方式。当您在浏览器中查看此示例时,<MyComponent>会如预期地被渲染:

path属性与当前 URL 匹配时,<Route>将被component属性值替换。在这个例子中,路由将被<MyComponent>替换。如果给定路由不匹配,则不会渲染任何内容。

路由声明的解耦

路由的困难在于当你的应用程序在单个模块中声明了数十个路由时,因为更难将路由映射到功能上。

为了帮助实现这一点,应用程序的每个顶级功能都可以定义自己的路由。这样,清楚地知道哪些路由属于哪个功能。所以,让我们从App组件开始:

import React, { Fragment } from 'react';
import {
  BrowserRouter as Router,
  Route,
  Redirect
} from 'react-router-dom';

// Import the routes from our features.
import One from './one';
import Two from './two';

// The feature routes are rendered as children of
// the main router.
export default () => (
  <Router>
    <Fragment>
      <Route exact path="/" render={() => <Redirect to="one" />} />
      <One />
      <Two />
    </Fragment>
  </Router>
); 

在这个例子中,应用程序有两个功能:onetwo。这些被导入为组件并在<Router>内呈现。您必须包含<Fragment>元素,因为<Router>不喜欢有多个子元素。通过使用片段,您可以传递一个子元素,而不必使用不必要的 DOM 元素。这个路由器中的第一个子元素实际上是一个重定向。这意味着当应用程序首次加载 URL / 时,<Redirect>组件将把用户发送到 /onerender属性是component属性的替代品,当您需要调用一个函数来呈现内容时。您在这里使用它是因为您需要将属性传递给<Redirect>

这个模块只会变得像应用程序功能的数量一样大,而不是路由的数量,后者可能会大得多。让我们来看看一个功能路由:

import React, { Fragment } from 'react';
import { Route, Redirect } from 'react-router';

// The pages that make up feature "one".
import First from './First';
import Second from './Second';

// The routes of our feature. The "<Redirect>"
// handles "/one" requests by redirecting to "/one/1".
export default () => (
  <Fragment>
    <Route
      exact
      path="/one"
      render={() => <Redirect to="/one/1" />}
    />
    <Route exact path="/one/1" component={First} />
    <Route exact path="/one/2" component={Second} />
  </Fragment>
);

这个模块,one/index.js,导出一个呈现带有三个路由的片段的组件:

  • 当匹配路径/one时,重定向到/one/1

  • 当匹配路径/one/1时,呈现First组件

  • 当匹配路径/one/2时,呈现Second组件

这遵循与路径/App组件相同的模式。通常,您的应用程序实际上没有要在功能的根或应用程序本身的根处呈现的内容。这种模式允许您将用户发送到适当的路由和适当的内容。这是您首次加载应用程序时会看到的内容:

第二个功能遵循与第一个完全相同的模式。以下是组件的初始外观:

import React from 'react';

export default () => (
  <p>Feature 1, page 1</p>
);

这个例子中的每个功能都使用相同的最小呈现内容。当用户导航到给定路由时,这些组件最终是用户需要看到的内容。通过以这种方式组织路由,您使得您的功能在路由方面是自包含的。

父级和子级路由

在前面的例子中,App组件是应用程序的主要组件。这是因为它定义了根 URL:/。然而,一旦用户导航到特定的功能 URL,App组件就不再相关了。

react-router版本 4 之前的版本中,您可以嵌套您的<Route>元素,以便随着路径继续匹配当前 URL,相关组件被渲染。例如,路径/users/8462将具有嵌套的<Route>元素。在版本 4 及以上,react-router不再使用嵌套路由来处理子内容。相反,您有您通常的App组件。然后,使用<Route>元素来匹配当前 URL 的路径,以渲染App中的特定内容。

让我们看一下一个父级App组件,它使用<Route>元素来渲染子组件:

import React from 'react';
import {
  BrowserRouter as Router,
  Route,
  NavLink
} from 'react-router-dom';

// The "User" components rendered with the "/users"
// route.
import UsersHeader from './users/UsersHeader';
import UsersMain from './users/UsersMain';

// The "Groups" components rendered with the "/groups"
// route.
import GroupsHeader from './groups/GroupsHeader';
import GroupsMain from './groups/GroupsMain';

// The "header" and "main" properties are the rendered
// components specified in the route. They're placed
// in the JSX of this component - "App".
const App = () => (
  <Router>
    <section>
      <nav>
        <NavLink
          exact
          to="/"
          style={{ padding: '0 10px' }}
          activeStyle={{ fontWeight: 'bold' }}
        >
          Home
        </NavLink>
        <NavLink
          exact
          to="/users"
          style={{ padding: '0 10px' }}
          activeStyle={{ fontWeight: 'bold' }}
        >
          Users
        </NavLink>
        <NavLink
          exact
          to="/groups"
          style={{ padding: '0 10px' }}
          activeStyle={{ fontWeight: 'bold' }}
        >
          Groups
        </NavLink>
      </nav>
      <header>
        <Route exact path="/" render={() => <h1>Home</h1>} />
        <Route exact path="/users" component={UsersHeader} />
        <Route exact path="/groups" component={GroupsHeader} />
      </header>
      <main>
        <Route exact path="/users" component={UsersMain} />
        <Route exact path="/groups" component={GroupsMain} />
      </main>
    </section>
  </Router>
);

export default App;

首先,App组件渲染一些导航链接。这些链接将始终可见。由于这些链接指向应用程序中的页面,您可以使用NavLink组件而不是Link组件。唯一的区别是,当其 URL 与当前 URL 匹配时,您可以使用activeStyle属性来改变链接的外观。

接下来,您有标题和主要部分。这是您使用Route组件来确定在App组件的这部分中渲染什么的地方。例如,<header>中的第一个路由使用render属性在用户位于应用程序的根目录时渲染标题。接下来的两个Route组件使用组件属性来渲染其他标题内容。在<main>中也使用相同的模式。

嵌套路由可能会很快变得混乱。通过声明路由的扁平结构,更容易扫描代码中的路由,以弄清发生了什么。

此应用程序有两个功能——usersgroups。它们各自都有自己的App组件定义。例如,UsersHeader用于<header>UsersMain用于<main>

这是UsersHeader组件的样子:

import React from 'react';

export default () => <h1>Users Header</h1>;

这是UsersMain组件的样子:

import React from 'react';

export default () => <p>Users content...</p>;

在组中使用的组件几乎与这些完全相同。如果您运行此示例并导航到/users,您可以期望看到:

处理路由参数

到目前为止,在本章中您所看到的 URL 都是静态的。大多数应用程序将同时使用静态和动态路由。在本节中,您将学习如何将动态 URL 段传递到您的组件中,如何使这些段可选,以及如何获取查询字符串参数。

路由中的资源 ID

一个常见的用例是将资源的 ID 作为 URL 的一部分。这样可以让您的代码轻松获取 ID,然后发出 API 调用以获取相关的资源数据。让我们实现一个渲染用户详细信息页面的路由。这将需要一个包含用户 ID 的路由,然后需要以某种方式将其传递给组件,以便它可以获取用户。

让我们从声明路由的App组件开始:

import React, { Fragment } from 'react';
import { BrowserRouter as Router, Route } from 'react-router-dom';

import UsersContainer from './UsersContainer';
import UserContainer from './UserContainer';

export default () => (
  <Router>
    <Fragment>
      <Route exact path="/" component={UsersContainer} />
      <Route path="/users/:id" component={UserContainer} />
    </Fragment>
  </Router>
); 

:语法标记了 URL 变量的开始。id变量将传递给UserContainer组件,下面是它的实现方式:

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { fromJS } from 'immutable';

import User from './User';
import { fetchUser } from './api';

export default class UserContainer extends Component {
  state = {
    data: fromJS({
      error: null,
      first: null,
      last: null,
      age: null
    })
  };

  // Getter for "Immutable.js" state data...
  get data() {
    return this.state.data;
  }

  // Setter for "Immutable.js" state data...
  set data(data) {
    this.setState({ data });
  }

  componentDidMount() {
    // The dynamic URL segment we're interested in, "id",
    // is stored in the "params" property.
    const { match: { params: { id } } } = this.props;

    // Fetches a user based on the "id". Note that it's
    // converted to a number first.
    fetchUser(Number(id)).then(
      // If the user was successfully fetched, then
      // merge the user properties into the state. Also,
      // make sure that "error" is cleared.
      user => {
        this.data = this.data.merge(user, { error: null });
      },

      // If the user fetch failed, set the "error" state
      // to the resolved error value. Also, make sure the
      // other user properties are restored to their defaults
      // since the component is now in an error state.
      error => {
        this.data = this.data.merge({
          error,
          first: null,
          last: null,
          age: null
        });
      }
    );
  }

  render() {
    return <User {...this.data.toJS()} />;
  }
}

// Params should always be there...
UserContainer.propTypes = {
  match: PropTypes.object.isRequired
};

match.params属性包含 URL 的任何动态部分。在这种情况下,您对id参数感兴趣。然后,将此值的数字版本传递给fetchUser()API 调用。如果 URL 完全缺少该段,那么这段代码将根本不运行;路由器将恢复到/路由。但是,在路由级别没有进行类型检查,这意味着您需要处理传递非数字的地方期望数字等情况。

在这个例子中,如果用户导航到,例如,/users/one,类型转换操作将导致 500 错误。您可以编写一个函数来对参数进行类型检查,并且在出现异常时不会失败,而是响应 404:未找到错误。无论如何,提供有意义的失败模式取决于应用程序,而不是react-router库。

现在让我们看一下这个示例中使用的 API 函数:

// Mock data...
const users = [
  { first: 'First 1', last: 'Last 1', age: 1 },
  { first: 'First 2', last: 'Last 2', age: 2 }
];

// Returns a promise that resolves the users array.
export function fetchUsers() {
  return new Promise((resolve, reject) => {
    resolve(users);
  });
}

// Returns a promise that resolves to a
// user from the "users" array, using the
// given "id" index. If nothing is found,
// the promise is rejected.
export function fetchUser(id) {
  const user = users[id];

  if (user === undefined) {
    return Promise.reject(`User ${id} not found`);
  } else {
    return Promise.resolve(user);
  }
}

fetchUsers()函数被UsersContainer组件使用来填充用户链接列表。fetchUser()函数将在模拟数据的users数组中查找并解析值,或者拒绝承诺。如果被拒绝,将调用UserContainer组件的错误处理行为。

这是负责渲染用户详细信息的User组件:

import React from 'react';
import PropTypes from 'prop-types';
import { Map } from 'immutable';

// Renders "error" text, unless "error" is
// null - then nothing is rendered.
const Error = ({ error }) =>
  Map([[null, null]]).get(
    error,
    <p>
      <strong>{error}</strong>
    </p>
  );

// Renders "children" text, unless "children"
// is null - then nothing is rendered.
const Text = ({ children }) =>
  Map([[null, null]]).get(children, <p>{children}</p>);

const User = ({ error, first, last, age }) => (
  <section>
    {/* If there's an API error, display it. */}
    <Error error={error} />

    {/* If there's a first, last, or age value,
         display it. */}
    <Text>{first}</Text>
    <Text>{last}</Text>
    <Text>{age}</Text>
  </section>
);

// Every property is optional, since we might
// have have to render them.
User.propTypes = {
  error: PropTypes.string,
  first: PropTypes.string,
  last: PropTypes.string,
  age: PropTypes.number
};

export default User;

当您运行此应用程序并导航到/时,您应该看到一个用户列表,看起来像这样:

点击第一个链接应该带您到/users/0,看起来像这样:

如果您导航到一个不存在的用户,/users/2,您将看到以下内容:

您看到这个错误消息而不是 500 错误的原因是因为 API 端点知道如何处理缺少的资源:

if (user === undefined) {
  reject(`User ${id} not found`);
}

这导致UserContainer设置其错误状态:

fetchUser(Number(id)).then(
  user => {
    this.data = this.data.merge(user, { error: null });
  },
  error => {
    this.data = this.data.merge({
      error,
      first: null,
      last: null,
      age: null
    });
  }
);

这样就导致User组件渲染错误消息:

const Error = ({ error }) =>
  Map([[null, null]]).get(
    error,
    <p>
      <strong>{error}</strong>
    </p>
  );

const User = ({ error, first, last, age }) => (
  <section>
    <Error error={error} />
    ...
  </section>
);

可选参数

有时,您需要可选的 URL 路径值和查询参数。URL 对于简单选项效果最佳,如果组件可以使用许多值,则查询参数效果最佳。

让我们实现一个用户列表组件,它渲染用户列表。可选地,您希望能够按降序对列表进行排序。让我们将这作为此页面的路由定义的可选路径段:

import React from 'react';
import { render } from 'react-dom';
import { BrowserRouter as Router, Route } from 'react-router-dom';

import UsersContainer from './UsersContainer';

render(
  <Router>
    <Route path="/users/:desc?" component={UsersContainer} />
  </Router>,
  document.getElementById('root')
); 

:语法标记一个变量,?后缀标记变量为可选。这意味着用户可以在/users/后提供任何他们想要的内容。这也意味着组件需要确保提供了字符串desc,并且忽略其他所有内容。

组件还需要处理提供给它的任何查询字符串。因此,虽然路由声明不提供定义接受的查询字符串的机制,但路由器仍将原始查询字符串传递给组件。现在让我们来看一下用户列表容器组件:

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { fromJS } from 'immutable';

import Users from './Users';
import { fetchUsers } from './api';

export default class UsersContainer extends Component {
  // The "users" state is an empty immutable list
  // by default.
  state = {
    data: fromJS({
      users: []
    })
  };

  // Getter for "Immutable.js" state data...
  get data() {
    return this.state.data;
  }

  // Setter for "Immutable.js" state data...
  set data(data) {
    this.setState({ data });
  }

  componentDidMount() {
    // The URL and query string data we need...
    const { match: { params }, location: { search } } = this.props;

    // If the "params.desc" value is "desc", it means that
    // "desc" is a URL segment. If "search.desc" is true, it
    // means "desc" was provided as a query parameter.
    const desc =
      params.desc === 'desc' ||
      !!new URLSearchParams(search).get('desc');

    // Tell the "fetchUsers()" API to sort in descending
    // order if the "desc" value is true.
    fetchUsers(desc).then(users => {
      this.data = this.data.set('users', users);
    });
  }

  render() {
    return <Users {...this.data.toJS()} />;
  }
}

UsersContainer.propTypes = {
  params: PropTypes.object.isRequired,
  location: PropTypes.object.isRequired
};

componentDidMount()方法中,此组件查找params.descsearch.desc。它将此作为fetchUsers() API 的参数,以确定排序顺序。

Users组件如下所示:

import React from 'react';
import PropTypes from 'prop-types';

// Renders a list of users...
const Users = ({ users }) => (
  <ul>{users.map(i => <li key={i}>{i}</li>)}</ul>
);

Users.propTypes = {
  users: PropTypes.array.isRequired
};

export default Users;

当您导航到/users时,将呈现如下内容:

如果您通过导航到/users/desc包含降序参数,我们会得到以下结果:

使用链接组件

在本节中,您将学习如何创建链接。您可能会尝试使用标准的<a>元素链接到由react-router控制的页面。这种方法的问题在于,这些链接将尝试通过发送 GET 请求在后端定位页面。这不是您想要的,因为路由配置已经在浏览器中。

首先,您将看到一个示例,说明<Link>元素在大多数方面都像<a>元素。然后,您将看到如何构建使用 URL 参数和查询参数的链接。

基本链接

在 React 应用程序中,链接的想法是它们指向指向渲染新内容的组件的路由。Link组件还负责浏览器历史 API 和查找路由/组件映射。这是一个渲染两个链接的应用程序组件:

import React from 'react';
import {
  BrowserRouter as Router,
  Route,
  Link
} from 'react-router-dom';

import First from './First';
import Second from './Second';

const App = () => (
  <Router>
    <section>
      <nav>
        <p>
          <Link to="first">First</Link>
        </p>
        <p>
          <Link to="second">Second</Link>
        </p>
      </nav>
      <section>
        <Route path="/first" component={First} />
        <Route path="/second" component={Second} />
      </section>
    </section>
  </Router>
);

export default App; 

to属性指定点击时要激活的路由。在这种情况下,应用程序有两个路由—/first/second。渲染的链接如下所示:

当您点击第一个链接时,页面内容会变成这样:

URL 和查询参数

构建传递给<Link>的路径的动态段涉及字符串操作。路径的所有部分都放在to属性中。这意味着您必须编写更多的代码来构建字符串,但也意味着在路由器中发生的幕后魔术更少。

让我们创建一个简单的组件,它将回显传递给回声 URL 段或echo查询参数的任何内容:

import React from 'react';
import { withRouter } from 'react-router';

// Simple component that expects either an "echo"
// URL segment parameter, or an "echo" query parameter.
export default withRouter(
  ({ match: { params }, location: { search } }) => (
    <h1>{params.msg || new URLSearchParams(search).get('msg')}</h1>
  )
); 

withRouter()实用程序函数是一个返回新组件的高阶函数。这个新组件将传递给它与路由相关的属性,如果你想要处理路径段变量或查询字符串,这些属性是必需的。你的Echo组件使用的两个属性是match.params用于 URL 路径变量和location.search用于查询字符串。

react-router版本 4 之前,查询字符串被解析并作为对象传递。现在必须在您的代码中处理。在这个例子中,使用了URLSearchParams

现在,让我们来看一下渲染两个链接的App组件。第一个将构建一个使用动态值作为 URL 参数的字符串。第二个将使用URLSearchParams来构建 URL 的查询字符串部分:

import React from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';

const App = ({ children }) => <section>{children}</section>;

App.propTypes = {
  children: PropTypes.node.isRequired
};

// Link parameter and query data...
const param = 'From Param';
const query = new URLSearchParams({ msg: 'From Query' });

App.defaultProps = {
  children: (
    <section>
      {/* This "<Link>" uses a paramter as part of
           the "to" property. */}
      <p>
        <Link to={`echo/${param}`}>Echo param</Link>
      </p>

      {/* This "<Link>" uses the "query" property
           to add query parameters to the link URL. */}
      <p>
        <Link to={`echo?${query.toString()}`} query={query}>
          Echo query
        </Link>
      </p>
    </section>
  )
};

export default App; 

当它们被渲染时,这两个链接看起来像这样:

参数链接将带您到/echo/From Param,看起来像这样:

查询链接将带您到/echo?echo=From+Query,看起来像这样:

总结

在本章中,您学习了 React 应用程序中的路由。路由的工作是渲染与 URL 对应的内容。react-router包是这项工作的标准工具。

您学会了路由是 JSX 元素,就像它们渲染的组件一样。有时,您需要将路由拆分为基于特性的模块。结构化页面内容的常见模式是有一个父组件,根据 URL 的变化来渲染动态部分。

您学会了如何处理 URL 段和查询字符串的动态部分。您还学会了如何使用<Link>元素在整个应用程序中构建链接。

在下一章中,您将学习如何在 Node.js 中呈现 React 组件。

测试您的知识

  1. react-router包是 React 应用程序中用于路由的官方包,因此是唯一的选择。

  2. 是的,react-router是官方的 React 路由解决方案。

  3. 不,react-router是多个路由选项之一,您应该花时间查看每个选项。

  4. 不,react-router是 React 的事实标准路由解决方案,除非您有充分的理由不使用它。

  5. RouteRouter组件之间有什么区别?

  6. Route用于根据 URL 匹配呈现组件,Router用于声明路由-组件映射。

  7. 没有区别。

  8. 每个组件都应该声明一个Router,以声明组件使用的路由。

  9. 当路由更改时,如何仅更改 UI 的某些部分?

  10. 您不能仅更改某些部分,必须重新呈现整个组件树,从根开始。

  11. 您使用Route组件根据提供的path属性呈现特定于任何给定部分的内容。您可以有多个具有相同path值的Route

  12. 您将部分名称作为属性值传递给Route组件,以及要为该部分呈现的组件。

  13. 何时应该使用NavLink组件?

  14. 当您希望react-router自动为您设置活动链接的样式时。

  15. 向用户显示哪些链接是导航链接,哪些是常规链接。

  16. 当您想要使用activeStyleactiveClassName属性为活动链接设置样式时。

  17. 如何从 URL 路径中获取值?

  18. 您可以通过传递段的索引来获取任何 URL 路径段的值。

  19. 您必须自己解析 URL 并找到值。

  20. 您使用:语法来指定这是一个变量,react-router将此值作为属性传递给您的组件。

进一步阅读

有关更多信息,请参考以下链接:

第十章:服务器端 React 组件

到目前为止,你在本书中学到的所有内容都是在 Web 浏览器中运行的 React 代码。React 并不局限于浏览器进行渲染,在本章中,你将学习如何从 Node.js 服务器渲染组件。

本章的第一部分简要介绍了高级服务器渲染概念。接下来的四个部分将深入探讨,教你如何使用 React 和 Next.js 实现服务器端渲染的最关键方面。

什么是同构 JavaScript?

服务器端渲染的另一个术语是同构 JavaScript。这是一种花哨的说法,表示 JavaScript 代码可以在浏览器和 Node.js 中运行,而无需修改。在本节中,你将学习同构 JavaScript 的基本概念,然后深入到代码中。

服务器是一个渲染目标

React 的美妙之处在于它是一个小的抽象层,位于渲染目标的顶部。到目前为止,目标一直是浏览器,但也可以是服务器。渲染目标可以是任何东西,只要在幕后实现了正确的翻译调用。

在服务器上进行渲染时,组件被渲染为字符串。服务器实际上无法显示渲染的 HTML;它所能做的就是将渲染的标记发送到浏览器。这个想法在下图中有所说明:

在服务器上渲染 React 组件并将渲染输出发送到浏览器是可能的。问题是,为什么你想在服务器上这样做,而不是在浏览器上呢?

初始加载性能

对我个人来说,服务器端渲染背后的主要动机是提高性能。特别是,初始渲染对用户来说感觉更快,这会转化为更好的用户体验。一旦应用程序加载并准备就绪,它有多快并不重要;初始加载时间对用户留下了深刻的印象。

这种方法有三个原因可以提高初始加载的性能:

  • 在服务器上进行的渲染生成了一个字符串;不需要计算差异或以任何方式与 DOM 交互。生成一串渲染标记的速度本质上比在浏览器中渲染组件要快。

  • 呈现的 HTML 一旦到达就会显示。任何需要在初始加载时运行的 JavaScript 代码都是在用户已经看到内容之后运行的。

  • 从 API 获取数据的网络请求更少,因为这些请求已经在服务器上发生,而服务器通常比单个客户端拥有更多的资源。

以下图表说明了这些性能思想:

在服务器和浏览器之间共享代码

你的应用程序很有可能需要与你无法控制的 API 端点进行通信,例如,由许多不同的微服务端点组成的应用程序。很少有可能直接使用这些服务的数据而不经过修改。相反,你需要编写代码来转换数据,以便 React 组件可以使用。

如果你在 Node.js 服务器上呈现你的组件,那么这个数据转换代码将被客户端和服务器同时使用,因为在初始加载时,服务器需要与 API 通信,而后来浏览器中的组件需要与 API 通信。

这不仅仅是关于转换从这些服务返回的数据。例如,你还需要考虑提供给它们的输入,比如创建或修改资源时。

作为 React 程序员,你需要做的基本调整是假设你实现的任何组件都需要在服务器上呈现。这可能看起来像是一个小的调整,但细节中藏着魔鬼。说到细节,现在让我们来看一些代码示例。

呈现为字符串

在 Node.js 中呈现组件意味着呈现为字符串,而不是试图找出将它们插入 DOM 的最佳方法。然后将字符串内容返回给浏览器,浏览器立即显示给用户。让我们来看一个例子。首先,要呈现的组件:

import React from 'react';
import PropTypes from 'prop-types';

const App = ({ items }) => (
  <ul>{items.map(i => <li key={i}>{i}</li>)}</ul>
);

App.propTypes = {
  items: PropTypes.arrayOf(PropTypes.string).isRequired
};

export default App;

接下来,让我们实现服务器,当浏览器请求时,它将呈现这个组件:

import React from 'react';

// The "renderToString()" function is like "render()",
// except it returns a rendered HTML string instead of
// manipulating the DOM.
import { renderToString } from 'react-dom/server';
import express from 'express';

// The component that we're going to render as a string.
import App from './App';

// The "doc()" function takes the rendered "content"
// of a React component and inserts it into an
// HTML document skeleton.
const doc = content =>
  `
  <!doctype html>
  <html>
    <head>
      <title>Rendering to strings</title>
    </head>
    <body>
      <div id="app">${content}</div>
    </body>
  </html>
  `;

const app = express();

// The root URL of the APP, returns the rendered
// React component.
app.get('/', (req, res) => {
  // Some properties to render...
  const props = {
    items: ['One', 'Two', 'Three']
  };

  // Render the "App" component using
  // "renderToString()"
  const rendered = renderToString(<App {...props} />);

  // Use the "doc()" function to build the final
  // HTML that is sent to the browser.
  res.send(doc(rendered));
});

app.listen(8080, () => {
  console.log('Listening on 127.0.0.1:8080');
});

现在,如果你在浏览器中访问127.0.0.1:8080,你会看到呈现的组件内容:

在这个例子中有两件事情需要注意。首先是doc()函数。它创建了带有渲染的 React 内容占位符的基本 HTML 文档模板。第二个是对renderToString()的调用,就像你习惯的render()调用一样。这是在服务器请求处理程序中调用的,渲染的字符串被发送到浏览器。

后端路由

在前面的例子中,你在服务器上实现了一个单一的请求处理程序,用于响应根 URL(/)的请求。你的应用程序需要处理不止一个路由。在上一章中,你学会了如何在路由中使用react-router包。现在,你将看到如何在 Node.js 中使用相同的包。

首先,让我们看一下主要的App组件:

import React from 'react';
import { Route, Link } from 'react-router-dom';

import FirstHeader from './first/FirstHeader';
import FirstContent from './first/FirstContent';
import SecondHeader from './second/SecondHeader';
import SecondContent from './second/SecondContent';

export default () => (
  <section>
    <header>
      <Route exact path="/" render={() => <h1>App</h1>} />
      <Route exact path="/first" component={FirstHeader} />
      <Route exact path="/second" component={SecondHeader} />
    </header>
    <main>
      <Route
        exact
        path="/"
        render={() => (
          <ul>
            <li>
              <Link to="first">First</Link>
            </li>
            <li>
              <Link to="second">Second</Link>
            </li>
          </ul>
        )}
      />
      <Route exact path="/first" component={FirstContent} />
      <Route exact path="/second" component={SecondContent} />
    </main>
  </section>
); 

这个应用程序处理三条路线:

  • /:首页

  • /first:第一页内容

  • /second:第二页内容

App内容分为<header><main>元素。在每个部分中,都有一个处理适当内容的<Route>组件。例如,/路由的主要内容由一个render()函数处理,该函数呈现到/first/second的链接。

这个组件在客户端上可以正常工作,但在服务器上会工作吗?让我们现在实现一下:

import React from 'react';
import { renderToString } from 'react-dom/server';
import { StaticRouter } from 'react-router';
import express from 'express';

import App from './App';

const app = express();

app.get('/*', (req, res) => {
  const context = {};
  const html = renderToString(
    <StaticRouter location={req.url} context={context}>
      <App />
    </StaticRouter>
  );

  if (context.url) {
    res.writeHead(301, {
      Location: context.url
    });
    res.end();
  } else {
    res.write(`
      <!doctype html>
      <div id="app">${html}</div>
    `);
    res.end();
  }
});

app.listen(8080, () => {
  console.log('Listening on 127.0.0.1:8080');
}); 

现在你有了前端和后端路由!这到底是如何工作的?让我们从请求处理程序路径开始。这已经改变了,现在是通配符(/*)。现在这个处理程序会对每个请求进行调用。

在服务器上,使用<StaticRouter>组件代替<BrowserRouter>组件。<App>组件是子组件,这意味着其中的<Route>组件将从<StaticRouter>传递数据。这就是<App>如何知道根据 URL 呈现正确的内容。调用renderToString()得到的html值可以作为发送给浏览器的响应文档的一部分。

现在你的应用程序开始看起来像一个真正的端到端的 React 渲染解决方案。这是服务器在你访问根 URL/时呈现的内容:

如果你访问/second URL,Node.js 服务器将呈现正确的组件:

如果您从主页导航到第一页,则请求将返回到服务器。我们需要弄清楚如何将前端代码传递到浏览器,以便它可以在初始呈现后接管。

前端协调

上一个示例中缺少的唯一内容是客户端 JavaScript 代码。用户希望使用应用程序,服务器需要传递客户端代码包。这将如何工作?路由必须在浏览器和服务器上工作,而不需要修改路由。换句话说,服务器处理初始请求的路由,然后浏览器在用户开始点击和在应用程序中移动时接管。

让我们为这个示例创建index.js模块:

import React from 'react';
import { hydrate } from 'react-dom';

import App from './App';

hydrate(<App />, document.getElementById('root')); 

这看起来像本书中迄今为止您所见过的大多数index.js文件。您在 HTML 文档的根元素中呈现<App>组件。在这种情况下,您使用hydrate()函数而不是render()函数。这两个函数的最终结果是相同的——在浏览器窗口中呈现的 JSX 内容。hydrate()函数不同,因为它期望已经放置了呈现的组件内容。这意味着它将执行更少的工作,因为它将假定标记是正确的,不需要在初始呈现时进行更新。

只有在开发模式下,React 才会检查服务器呈现内容的整个 DOM 树,以确保显示正确的内容。如果现有内容与 React 组件的输出之间存在不匹配,您将看到警告,显示出现不匹配的位置,以便您可以去修复它们。

这是您的应用程序将在浏览器和 Node.js 服务器上呈现的App组件:

import React, { Component } from 'react';

export default class App extends Component {
  state = { clicks: 0 };

  render() {
    return (
      <section>
        <header>
          <h1>Hydrating The Client</h1>
        </header>
        <main>
          <p>Clicks {this.state.clicks}</p>
          <button
            onClick={() =>
              this.setState(state => ({ clicks: state.clicks + 1 }))
            }
          >
            Click Me
          </button>
        </main>
      </section>
    );
  }
}

该组件呈现一个按钮,当点击时,将更新clicks状态。该状态在按钮上方的标签中呈现。当此组件在服务器上呈现时,将使用默认的点击值 0,并且onClick处理程序将被忽略,因为它只是呈现静态标记。让我们接下来看一下服务器端的代码:

import fs from 'fs';
import React from 'react';
import { renderToString } from 'react-dom/server';
import express from 'express';

import App from './App';

const app = express();
const doc = fs.readFileSync('./build/index.html');

app.use(express.static('./build', { index: false }));

app.get('/*', (req, res) => {
  const context = {};
  const html = renderToString(<App />);

  if (context.url) {
    res.writeHead(301, {
      Location: context.url
    });
    res.end();
  } else {
    res.write(
      doc
        .toString()
        .replace('<div id="root">', `<div id="root">${html}`)
    );
    res.end();
  }
});

app.listen(8080, () => {
  console.log('Listening on 127.0.0.1:8080');
});

让我们浏览一下这个源代码,看看发生了什么:

const doc = fs.readFileSync('./build/index.html');

这读取由您的 React 构建工具(如create-react-app/react-scripts)创建的index.html文件,并将其存储在doc中:

app.use(express.static('./build', { index: false }));

这告诉 Express 服务器将./build下的文件作为静态文件提供,除了index.html。相反,您将编写一个处理程序,以响应站点根目录的请求:

app.get('/*', (req, res) => {
  const context = {};
  const html = renderToString(<App />);

  if (context.url) {
    res.writeHead(301, {
      Location: context.url
    });
    res.end();
  } else {
    res.write(
      doc
        .toString()
        .replace('<div id="root">', `<div id="root">${html}`)
    );
    res.end();
  }
});

这是 html 常量被填充为渲染的 React 内容的地方。然后,它被插入到 HTML 字符串中使用 replace(),并作为响应发送。因为你使用了基于构建的 index.html 文件,它包含了一个链接到捆绑的 React 应用程序,当在浏览器中加载时将运行。

获取数据

如果你的某个组件在完全渲染其内容之前需要获取 API 数据怎么办?这对于在服务器上渲染来说是一个挑战,因为没有简单的方法来定义一个组件,它知道何时在服务器上以及在浏览器中获取数据。

这就是像 Next.js 这样的最小化框架发挥作用的地方。Next.js 将服务器渲染和浏览器渲染视为相等。这意味着组件获取数据的麻烦被抽象化了 - 你可以在浏览器和服务器上使用相同的代码。

本书的上一版没有使用任何框架来在服务器上获取 React 组件数据。我认为,如果你要走这条路,不使用框架是一个错误。有太多事情可能会出错,而且没有框架,最终你将对它们负责。

为了处理路由,Next.js 使用页面的概念。页面 是一个导出 React 组件的 JavaScript 模块。组件的渲染内容变成页面内容。以下是 pages 目录的样子:

└── pages
 ├── first.js ├── index.js └── second.js

index.js 模块是应用程序的根页面:Next.js 根据文件名知道这一点。以下是源代码的样子:

import Layout from '../components/MyLayout.js';

export default () => (
  <Layout>
    <p>Fetching component data on the server and on the client...</p>
  </Layout>
);

这个页面使用了 <Layout> 组件来确保通用组件被渲染,而不需要重复代码。渲染后页面的样子如下:

除了段落之外,整个应用程序布局还包括导航链接到其他页面。以下是 Layout 的源代码样子:

import Header from './Header';

const layoutStyle = {
  margin: 20,
  padding: 20,
  border: '1px solid #DDD'
};

const Layout = props => (
  <div style={layoutStyle}>
    <Header />
    {props.children}
  </div>
);

export default Layout;

Layout 组件渲染一个 Header 组件和 props.childrenchildren 属性是你在页面中传递给 Layout 组件的值。现在让我们来看一下 Header 组件:

import Link from 'next/link';

const linkStyle = {
  marginRight: 15
};

const Header = () => (
  <div>
    <Link href="/">
      <a style={linkStyle}>Home</a>
    </Link>
    <Link href="/first">
      <a style={linkStyle}>First</a>
    </Link>
    <Link href="/second">
      <a style={linkStyle}>Second</a>
    </Link>
  </div>
);

export default Header;

这里使用的 Link 组件来自于 Next.js。这样,链接就可以按照 Next.js 自动设置的路由正常工作。现在让我们看一个有数据获取要求的页面 - pages/first.js

import fetch from 'isomorphic-unfetch';
import Layout from '../components/MyLayout.js';
import { fetchFirstItems } from '../api';

const First = ({ items }) => (
  <Layout>{items.map(i => <li key={i}>{i}</li>)}</Layout>
);

First.getInitialProps = async () => {
  const res = await fetchFirstItems();
  const items = await res.json();

  return { items };
};

export default First;

fetch() 函数用于获取数据,来自于 isomorphic-unfetch 包。这个版本的 fetch() 在服务器和浏览器上都可以使用,你不需要检查任何东西。再次强调,Layout 组件用于包装页面内容,以保持与其他页面的一致性。

getInitialProps() 函数是 Next.js 获取数据的方式——在浏览器和服务器上。这是一个异步函数,意味着你可以花费尽可能长的时间来获取组件属性的数据,而 Next.js 将确保在数据准备好之前不呈现任何标记。让我们来看看 fetchFirstItems() API 函数:

export default () =>
  new Promise(resolve =>
    setTimeout(() => {
      resolve({
        json: () => Promise.resolve(['One', 'Two', 'Three'])
      });
    }, 1000)
  );

这个函数通过返回一个在 1 秒后解析出组件数据的 promise 来模拟 API 的行为。如果你导航到 /first,你将在 1 秒后看到以下内容:

通过点击第一个链接,你导致了在浏览器中调用 getInitialProps() 函数,因为应用程序已经被交付。如果你在 /first 页面重新加载页面,你将触发在服务器上调用 getInitialProps(),因为这是 Next.js 在服务器上处理的页面。

摘要

在本章中,你了解到 React 除了在客户端上渲染外,还可以在服务器上渲染。这样做的原因有很多,比如在前端和后端之间共享通用代码。服务器端渲染的主要优势是在初始页面加载时获得的性能提升。这将转化为更好的用户体验,因此也是更好的产品。

然后,你逐步改进了一个服务器端的 React 应用程序,从单页面渲染开始。然后介绍了路由、客户端协调和组件数据获取,以使用 Next.js 实现完整的后端渲染解决方案。

在接下来的章节中,你将学习如何实现 React Bootstrap 组件来实现移动优先设计。

测试你的知识

  1. react-dom 中的 render() 函数和 react-dom/server 中的 renderToString() 函数有什么区别?

  2. render() 函数仅用于在浏览器中将 React 组件内容与 DOM 同步。renderToString() 函数不需要 DOM,因为它将标记呈现为字符串。

  3. 这两个函数是可以互换的。

  4. render() 函数在服务器上速度较慢,所以 renderToString() 是一个更好的选择。

  5. 如果必须,应该只在浏览器中使用render()。在大多数情况下,renderToString()函数更可取。

  6. 在服务器上进行路由是必要的,因为:

  7. 在服务器上没有路由,实际上无法渲染组件。

  8. 您不需要担心在服务器上进行渲染,因为路由将在浏览器中处理。

  9. 服务器上的路由将根据请求的 URL 确定渲染的内容。然后将此内容发送到浏览器,以便用户感知到更快的加载时间。

  10. 在服务器上进行路由应该手动完成,而不是使用 react-router 中的组件。

  11. 在调和服务器渲染的 React 标记与浏览器中的 React 组件时,应该使用哪个函数?

  12. 始终在浏览器中使用render()。它知道如何对现有标记进行必要的更改。

  13. 始终在服务器发送渲染的 React 组件时使用hydrate()。与render()不同,hydrate()期望渲染的组件标记并且可以高效处理它。

进一步阅读

查看以下链接以获取更多信息:

第十一章:移动优先 React 组件

在本章中,您将学习如何使用react-bootstrap包。该包通过利用 Bootstrap CSS 框架提供移动优先的 React 组件。这不是进行移动优先 React 的唯一选择,但这是一个不错的选择,并且它将网络上最流行的两种技术结合在一起。

我将从采用移动优先设计策略的动机开始。然后您将在本章的其余部分中实现一些react-bootstrap组件。

移动优先设计背后的原理

移动优先设计是一种将移动设备视为用户界面的主要目标的策略。较大的屏幕,如笔记本电脑或大型显示器,是次要目标。这并不一定意味着大多数用户在手机上访问您的应用程序。这只是意味着移动设备是缩放用户界面的起点。

例如,当移动浏览器首次出现时,习惯上是为普通桌面屏幕设计用户界面,然后在必要时缩小到较小的屏幕。该方法如下所示:

这里的想法是,您设计 UI 时要考虑较大的屏幕,以便一次性将尽可能多的功能放在屏幕上。当使用较小的设备时,您的代码必须在运行时使用不同的布局或不同的组件。

这在许多方面都是非常有限的。首先,对于不同的屏幕分辨率,维护大量特殊情况处理的代码非常困难。其次,更具有说服力的反对这种方法的论点是,几乎不可能在不同设备上提供类似的用户体验。如果大屏幕一次显示大量功能,您简单无法在较小的屏幕上复制这一点。不仅是屏幕空间较小,而且较小设备的处理能力和网络带宽也是限制因素。

UI 设计的移动优先方法通过放大 UI 来解决这些问题,而不是试图缩小 UI,如下所示:

这种方法以前是没有意义的,因为你会限制你的应用程序的功能;周围没有很多平板电脑或手机。但今天情况不同了,人们期望用户能够在他们的移动设备上与应用程序进行交互而不会出现任何问题。现在有更多的移动设备了,移动浏览器完全能够处理你提出的任何要求。

一旦你在移动环境中实现了应用程序功能,将其扩展到更大的屏幕尺寸就是一个相对容易解决的问题。现在,让我们看看如何在 React 应用程序中实现移动优先。

使用 react-bootstrap 组件

虽然可以通过自己编写 CSS 来实现移动优先的 React 用户界面,但我建议不要这样做。有许多 CSS 库可以为你处理看似无穷无尽的边缘情况。在这一部分,我将介绍react-bootstrap包——Bootstrap 的 React 组件。

react-bootstrap包公开了许多组件,它们在你的应用程序和 Bootstrap HTML/CSS 之间提供了一个薄的抽象层。

现在让我们实现一些示例。我向你展示如何使用react-bootstrap组件的另一个原因是它们与react-native组件相似,你将在下一章中学习到。

以下示例的重点不是深入覆盖react-bootstrap,或者 Bootstrap 本身。相反,重点是让你感受一下通过从容器传递状态等方式在 React 中使用移动优先组件的感觉。现在,先看一下react-bootstrap文档(react-bootstrap.github.io/)了解具体内容。

实现导航

移动优先设计的最重要方面是导航。在移动设备上很难做到这一点,因为几乎没有足够的空间来放置功能内容,更别提从一个功能到另一个功能的工具了。幸运的是,Bootstrap 为你处理了许多困难。

在这一部分,你将学习如何实现两种类型的导航。你将从工具栏导航开始,然后构建一个侧边栏导航部分。这构成了你将开始的 UI 骨架的一部分。我发现这种方法真的很有用,因为一旦导航机制就位,我在构建应用程序时很容易添加新页面和在应用程序中移动。

让我们从Navbar.开始。这是大多数应用程序中的一个组件,静态地位于屏幕顶部。在这个栏中,你将添加一些导航链接。这是这个 JSX 的样子:

{/* The "NavBar" is statically-placed across the
   top of every page. It contains things like the
   title of the application, and menu items. */}
<Navbar className="navbar-top" fluid>
  <Navbar.Header>
    <Navbar.Brand>
      <Link to="/">Mobile-First React</Link>
    </Navbar.Brand>

    {/* The "<Navbar.Taggle>" coponent is used to replace any
       navigation links with a drop-down menu for smaller
       screens. */}
    <Navbar.Toggle />
  </Navbar.Header>

  {/* The actual menu with links to makes. It's wrapped
     in the "<Navbar.Collapse>"" component so that it
     work properly when the links have been collapsed. */}
  <Navbar.Collapse>
    <Nav pullRight>
      <IndexLinkContainer to="/">
        <MenuItem>Home</MenuItem>
      </IndexLinkContainer>
      <LinkContainer to="forms">
        <MenuItem>Forms</MenuItem>
      </LinkContainer>
      <LinkContainer to="lists">
        <MenuItem>Lists</MenuItem>
      </LinkContainer>
    </Nav>
  </Navbar.Collapse>
</Navbar> 

导航栏的样子如下:

<Navbar.Header>组件定义了应用程序的标题,并放置在导航栏的左侧。链接本身放在<Nav>元素中,pullRight属性将它们对齐到导航栏的右侧。你可以看到,你没有使用react-router包中的<Link>,而是使用了<LinkContainer><IndexLinkContainer>。这些组件来自react-router-bootstrap包。它们是必要的,以使 Bootstrap 链接与路由器正常工作。

<Nav>元素被包裹在<Navbar.Collapse>元素中,头部包含一个<Navbar.Toggle>按钮。这些组件是必要的,用于将链接折叠成下拉菜单以适应较小的屏幕。由于它是基于浏览器的宽度,你可以调整浏览器窗口大小来看它的效果:

显示的链接现在已经折叠成了一个标准菜单按钮。当点击这个按钮时,相同的链接以垂直方式显示。这在较小的设备上效果更好。但是在较大的屏幕上,将所有导航显示在顶部导航栏可能不是理想的。标准的方法是实现一个带有垂直堆叠导航链接的左侧边栏。让我们现在来实现这个:

{/* This navigation menu has the same links
   as the top navbar. The difference is that
   this navigation is a sidebar. It's completely
   hidden on smaller screens. */}
<Col sm={3} md={2} className="sidebar">
  <Nav stacked>
    <IndexLinkContainer to="/">
      <NavItem>Home</NavItem>
    </IndexLinkContainer>
    <LinkContainer to="forms">
      <NavItem>Forms</NavItem>
    </LinkContainer>
    <LinkContainer to="lists">
      <NavItem>Lists</NavItem>
    </LinkContainer>
  </Nav>
</Col> 

<Col>元素是<Nav>的容器,你已经给它添加了自己的类名。你马上就会明白为什么要这样做。在<Nav>元素内部,事情看起来和导航工具栏中一样,有链接容器和菜单项。这就是侧边栏的样子:

现在,我们需要给包含元素添加自定义的sidebar类名的原因是为了在较小的设备上完全隐藏它。让我们来看一下涉及的 CSS:

.sidebar { 
  display: none; 
} 

@media (min-width: 768px) { 
  .sidebar { 
    display: block; 
    position: fixed; 
    top: 60px; 
  } 
} 

这个 CSS,以及这个示例的整体结构,都是从 Bootstrap 示例中调整而来:getbootstrap.com/examples/dashboard/。这个媒体查询的背后思想是,如果最小浏览器宽度为768px,那么在固定位置显示侧边栏。否则,完全隐藏它,因为我们在一个较小的屏幕上。

在这一点上,您有两个导航组件相互协作,根据屏幕分辨率改变它们的显示方式。

列表

在移动和桌面环境中,一个常见的 UI 元素是渲染项目列表。这很容易在没有 CSS 库的支持下完成,但库有助于保持外观和感觉一致。让我们实现一个由一组过滤器控制的列表。首先,您有渲染react-bootstrap组件的组件:

import React from 'react';
import PropTypes from 'prop-types';

import {
  Button,
  ButtonGroup,
  ListGroupItem,
  ListGroup,
  Glyphicon
} from 'react-bootstrap';

import './FilteredList.css';

// Utility function to get the bootstrap style
// for an item, based on the "done" value.
const itemStyle = done => (done ? { bsStyle: 'success' } : {});

// Utility component for rendering a bootstrap
// icon based on the value of "done".
const ItemIcon = ({ done }) =>
  done ? <Glyphicon glyph="ok" className="item-done" /> : null;

// Renders a list of items, and a set of filter
// controls to change what's displayed in the
// list.
const FilteredList = props => (
  <section>
    {/* Three buttons that control what's displayed
         in the list below. Clicking one of these
         buttons will toggle the state of the others. */}
    <ButtonGroup className="filters">
      <Button active={props.todoFilter} onClick={props.todoClick}>
        Todo
      </Button>
      <Button active={props.doneFilter} onClick={props.doneClick}>
        Done
      </Button>
      <Button active={props.allFilter} onClick={props.allClick}>
        All
      </Button>
    </ButtonGroup>

    {/* Renders the list of items. It passes the
         "props.filter()" function to "items.filter()".
         When the buttons above are clicked, the "filter"
         function is changed. */}
    <ListGroup>
      {props.items.filter(props.filter).map(i => (
        <ListGroupItem
          key={i.name}
          onClick={props.itemClick(i)}
          href="#"
          {...itemStyle(i.done)}
        >
          {i.name}
          <ItemIcon done={i.done} />
        </ListGroupItem>
      ))}
    </ListGroup>
  </section>
);

FilteredList.propTypes = {
  todoFilter: PropTypes.bool.isRequired,
  doneFilter: PropTypes.bool.isRequired,
  allFilter: PropTypes.bool.isRequired,
  todoClick: PropTypes.func.isRequired,
  doneClick: PropTypes.func.isRequired,
  allClick: PropTypes.func.isRequired,
  itemClick: PropTypes.func.isRequired,
  filter: PropTypes.func.isRequired,
  items: PropTypes.array.isRequired
};

export default FilteredList;

首先,您有<ButtonGroup><Button>元素。这些是用户可以应用于列表的过滤器。默认情况下,只显示待办事项。但是,他们可以选择按已完成项目进行过滤,或者显示所有项目。

列表本身是一个<ListGroup>元素,其子元素是<ListGroupItem>元素。该项目根据项目的done状态而呈现不同。最终结果如下:

您可以通过单击“完成”按钮来切换列表项的完成状态。这个组件的好处在于,如果您正在查看待办事项并将其标记为已完成,它将从列表中删除,因为它不再符合当前的过滤条件。组件重新呈现,因此重新评估过滤器。以下是标记为已完成的项目的外观:

现在让我们看一下处理过滤器按钮和项目列表状态的容器组件:

import React, { Component } from 'react';
import { fromJS } from 'immutable';

import FilteredList from './FilteredList';

class FilteredListContainer extends Component {
  // Controls the state of the the filter buttons
  // as well as the state of the function that
  // filters the item list.
  state = {
    data: fromJS({
      // The items...
      items: [
        { name: 'First item', done: false },
        { name: 'Second item', done: false },
        { name: 'Third item', done: false }
      ],

      // The filter button states...
      todoFilter: true,
      doneFilter: false,
      allFilter: false,

      // The default filter...
      filter: i => !i.done,

      // The "todo" filter button was clicked.
      todoClick: () => {
        this.data = this.data.merge({
          todoFilter: true,
          doneFilter: false,
          allFilter: false,
          filter: i => !i.done
        });
      },

      // The "done" filter button was clicked.
      doneClick: () => {
        this.data = this.data.merge({
          todoFilter: false,
          doneFilter: true,
          allFilter: false,
          filter: i => i.done
        });
      },

      // The "all" filter button was clicked.
      allClick: () => {
        this.data = this.data.merge({
          todoFilter: false,
          doneFilter: false,
          allFilter: true,
          filter: () => true
        });
      },

      // When the item is clicked, toggle it's
      // "done" state.
      itemClick: item => e => {
        e.preventDefault();

        this.data = this.data.update('items', items =>
          items.update(
            items.findIndex(i => i.get('name') === item.name),
            i => i.update('done', done => !done)
          )
        );
      }
    })
  };

  // Getter for "Immutable.js" state data...
  get data() {
    return this.state.data;
  }

  // Setter for "Immutable.js" state data...
  set data(data) {
    this.setState({ data });
  }

  render() {
    return <FilteredList {...this.state.data.toJS()} />;
  }
}

export default FilteredListContainer;

这个组件有四个状态和四个事件处理程序函数。三个状态仅仅是跟踪哪个过滤器按钮被选中。filter状态是由<FilteredList>使用的回调函数,用于过滤项目。策略是根据过滤器选择向子视图传递不同的过滤器函数。

表单

在本章的最后一节中,您将从react-bootstrap实现一些表单组件。就像您在前一节中创建的过滤按钮一样,表单组件也有需要从容器组件传递下来的状态。

然而,即使是简单的表单控件也有许多组成部分。首先,您将了解文本输入。有输入本身,还有标签,占位符,错误文本,验证函数等等。为了帮助将所有这些部分粘合在一起,让我们创建一个封装了所有 Bootstrap 部分的通用组件:

import React from 'react';
import PropTypes from 'prop-types';
import {
  FormGroup,
  FormControl,
  ControlLabel,
  HelpBlock
} from 'react-bootstrap';

// A generic input element that encapsulates several
// of the react-bootstrap components that are necessary
// for event simple scenarios.
const Input = ({
  type,
  label,
  value,
  placeholder,
  onChange,
  validationState,
  validationText
}) => (
  <FormGroup validationState={validationState}>
    <ControlLabel>{label}</ControlLabel>
    <FormControl
      type={type}
      value={value}
      placeholder={placeholder}
      onChange={onChange}
    />
    <FormControl.Feedback />
    <HelpBlock>{validationText}</HelpBlock>
  </FormGroup>
);

Input.propTypes = {
  type: PropTypes.string.isRequired,
  label: PropTypes.string,
  value: PropTypes.any,
  placeholder: PropTypes.string,
  onChange: PropTypes.func,
  validationState: PropTypes.oneOf([
    undefined,
    'success',
    'warning',
    'error'
  ]),
  validationText: PropTypes.string
};

export default Input; 

这种方法有两个关键优势。一个是,不需要使用<FormGroup><FormControl><HelpBlock>等,只需要您的<Input>元素。另一个优势是,只需要type属性,这意味着<Input>可以用于简单和复杂的控件。

现在让我们看看这个组件的实际效果:

import React from 'react';
import PropTypes from 'prop-types';
import { Panel } from 'react-bootstrap';

import Input from './Input';

const InputsForm = props => (
  <Panel header={<h3>Inputs</h3>}>
    <form>
      {/* Uses the <Input> element to render
           a simple name field. There's a lot of
           properties passed here, many of them
           come from the container component. */}
      <Input
        type="text"
        label="Name"
        placeholder="First and last..."
        value={props.nameValue}
        onChange={props.nameChange}
        validationState={props.nameValidationState}
        validationText={props.nameValidationText}
      />

      {/* Uses the "<Input>" element to render a
           password input. */}
      <Input
        type="password"
        label="Password"
        value={props.passwordValue}
        onChange={props.passwordChange}
      />
    </form>
  </Panel>
);

InputsForm.propTypes = {
  nameValue: PropTypes.any,
  nameChange: PropTypes.func,
  nameValidationState: PropTypes.oneOf([
    undefined,
    'success',
    'warning',
    'error'
  ]),
  nameValidationText: PropTypes.string,
  passwordValue: PropTypes.any,
  passwordChange: PropTypes.func
};

export default InputsForm;

只有一个组件用于创建所有必要的 Bootstrap 部分。所有内容都通过属性传入。这个表单看起来是这样的:

现在让我们来看看控制这些输入状态的容器组件:

import React, { Component } from 'react';
import { fromJS } from 'immutable';

import InputsForm from './InputsForm';

// Validates the given "name". It should have a space,
// and it should have more than 3 characters. There are
// many scenarios not accounted for here, but are easy
// to add.
function validateName(name) {
  if (name.search(/ /) === -1) {
    return 'First and last name, separated with a space';
  } else if (name.length < 4) {
    return 'Less than 4 characters? Srsly?';
  }

  return null;
}

class InputsFormContainer extends Component {
  state = {
    data: fromJS({
      // "Name" value and change handler.
      nameValue: '',
      // When the name changes, we use "validateName()"
      // to set "nameValidationState" and
      // "nameValidationText".
      nameChange: e => {
        this.data = this.data.merge({
          nameValue: e.target.value,
          nameValidationState:
            validateName(e.target.value) === null
              ? 'success'
              : 'error',
          nameValidationText: validateName(e.target.value)
        });
      },
      // "Password" value and change handler.
      passwordValue: '',
      passwordChange: e => {
        this.data = this.data.set('passwordValue', e.target.value);
      }
    })
  };

  // Getter for "Immutable.js" state data...
  get data() {
    return this.state.data;
  }

  // Setter for "Immutable.js" state data...
  set data(data) {
    this.setState({ data });
  }

  render() {
    return <InputsForm {...this.data.toJS()} />;
  }
}

export default InputsFormContainer;

输入的事件处理程序是作为状态的一部分传递给InputsForm作为属性。现在让我们来看看一些复选框和单选按钮。您将使用<Radio><Checkbox> react-bootstrap 组件:

import React from 'react';
import PropTypes from 'prop-types';
import { Panel, Radio, Checkbox, FormGroup } from 'react-bootstrap';

const RadioForm = props => (
  <Panel header={<h3>Radios & Checkboxes</h3>}>
    {/* Renders a group of related radio buttons. Note
         that each radio needs to hae the same "name"
         property, otherwise, the user will be able to
         select multiple radios in the same group. The
         "checked", "disabled", and "onChange" properties
         all come from the container component. */}
    <FormGroup>
      <Radio
        name="radio"
        onChange={props.checkboxEnabledChange}
        checked={props.checkboxEnabled}
        disabled={!props.radiosEnabled}
      >
        Checkbox enabled
      </Radio>
      <Radio
        name="radio"
        onChange={props.checkboxDisabledChange}
        checked={!props.checkboxEnabled}
        disabled={!props.radiosEnabled}
      >
        Checkbox disabled
      </Radio>
    </FormGroup>

    {/* Reanders a checkbox and uses the same approach
         as the radios above: setting it's properties from
         state that's passed in from the container. */}
    <FormGroup>
      <Checkbox
        onChange={props.checkboxChange}
        checked={props.radiosEnabled}
        disabled={!props.checkboxEnabled}
      >
        Radios enabled
      </Checkbox>
    </FormGroup>
  </Panel>
);

RadioForm.propTypes = {
  checkboxEnabled: PropTypes.bool.isRequired,
  radiosEnabled: PropTypes.bool.isRequired,
  checkboxEnabledChange: PropTypes.func.isRequired,
  checkboxDisabledChange: PropTypes.func.isRequired,
  checkboxChange: PropTypes.func.isRequired
};

export default RadioForm; 

单选按钮切换复选框的enabled状态,复选框切换单选按钮的enabled状态。请注意,尽管两个<Radio>元素在同一个<FormGroup>中,它们需要具有相同的name属性值。否则,您将能够同时选择两个单选按钮。这个表单看起来是这样的:

最后,让我们来看看处理单选按钮和复选框状态的容器组件:

import React, { Component } from 'react';
import { fromJS } from 'immutable';

import RadioForm from './RadioForm';

class RadioFormContainer extends Component {
  // Controls the enabled state of a group of
  // radio buttons and a checkbox. The radios
  // toggle the state of the checkbox while the
  // checkbox toggles the state of the radios.
  state = {
    data: fromJS({
      checkboxEnabled: false,
      radiosEnabled: true,
      checkboxEnabledChange: () => {
        this.data = this.data.set('checkboxEnabled', true);
      },
      checkboxDisabledChange: () => {
        this.data = this.data.set('checkboxEnabled', false);
      },
      checkboxChange: () => {
        this.data = this.data.update(
          'radiosEnabled',
          enabled => !enabled
        );
      }
    })
  };

  // Getter for "Immutable.js" state data...
  get data() {
    return this.state.data;
  }

  // Setter for "Immutable.js" state data...
  set data(data) {
    this.setState({ data });
  }

  render() {
    return <RadioForm {...this.data.toJS()} />;
  }
}

export default RadioFormContainer; 

总结

本章向您介绍了移动优先设计的概念。您简要了解了为什么要使用移动优先策略。归根结底,这是因为将移动设计扩展到更大的设备要比相反方向的扩展容易得多。

接下来,你了解了这在 React 应用程序的上下文中意味着什么。特别是,你希望使用处理我们的缩放细节的框架,比如 Bootstrap。然后,你使用了react-bootstrap包中的几个组件来实现了几个示例。

这结束了本书的第一部分。现在你已经准备好处理在网络上运行的 React 项目,包括移动浏览器!移动浏览器变得越来越好,但它们无法与移动平台的本机功能相媲美。本书的第二部分将教你如何使用 React Native。

测试你的知识

  1. React 开发者为什么要考虑移动优先的设计方法呢?

  2. 因为大多数用户使用移动设备,考虑较大的显示屏并不值得。

  3. 因为将移动设备作为应用程序的主要显示屏,可以确保你可以处理移动设备,并且向较大设备的扩展比另一种方式更容易。

  4. 这没有意义。你应该首先针对较大的显示屏,然后缩小应用程序以适应移动设备。

  5. 如果你使用react-bootstrap这样的库,你甚至不需要考虑移动优先的概念。

  6. react-routerreact-bootstrap集成良好吗?

  7. 是的。尽管你会想要使用react-router-bootstrap包,以确保你可以向NavItemMenuItem组件添加链接。

  8. 不,你应该在react-bootstrap组件中使用常规链接。

  9. 是的,但是你应该考虑编写自己的抽象,以便所有类型的react-bootstrap按钮与Link组件一起工作。

  10. 你会如何使用react-bootstrap渲染项目列表?

  11. 使用react-bootstrap中的<ListGroup>组件包装<ul>元素。

  12. 只需使用<ul>并将 Bootstrap 类应用于该元素。

  13. 使用react-bootstrap中的ListGroupListGroupItem组件。

  14. 为什么你要为react-bootstrap表单组件创建一个抽象?

  15. 因为react-bootstrap表单组件在功能上缺乏。

  16. 因为有许多相关组件需要用于基本输入,创建这种抽象会让生活更轻松。

  17. 因为这是使输入验证工作的唯一方法。

进一步阅读

更多信息可以查看以下链接:

第十二章:为什么选择 React Native?

Facebook 创建了 React Native 来构建其移动应用程序。这样做的动机源于 React 在 Web 上非常成功的事实。如果 React 是如此适合 UI 开发的工具,并且您需要一个本机应用程序,那么为什么要反对呢?只需使 React 与本机移动操作系统 UI 元素配合工作即可!

在本章中,您将了解使用 React Native 构建本机移动 Web 应用程序的动机。

什么是 React Native?

在本书的前面,我介绍了渲染目标的概念-React 组件渲染到的东西。就 React 程序员而言,渲染目标是抽象的。例如,在 React 中,渲染目标可以是字符串,也可以是 DOM。这就是为什么您的组件从不直接与渲染目标进行交互的原因,因为您永远不能假设渲染发生的位置。

移动平台具有开发人员可以利用的 UI 小部件库,以构建该平台的应用程序。在 Android 上,开发人员实现 Java 应用程序,而在 iOS 上,开发人员实现 Swift 应用程序。如果您想要一个功能齐全的移动应用程序,您将不得不选择一个。但是,您需要学习两种语言,因为仅支持两个主要平台中的一个对于成功来说是不现实的。

对于 React 开发人员来说,这不是问题。您构建的相同 React 组件可以在各个地方使用,甚至可以在移动浏览器上使用!必须学习两种新编程语言来构建和发布移动应用程序是成本和时间上的障碍。解决此问题的方法是引入一个支持新渲染目标-本机移动 UI 小部件的新 React。

React Native 使用一种技术,该技术对底层移动操作系统进行异步调用,该操作系统调用本机小部件 API。有一个 JavaScript 引擎,React API 与 Web 上的 React 大部分相同。不同之处在于目标;而不是 DOM,这里有异步 API 调用。该概念在这里可视化:

这过于简化了底层发生的一切,但基本思想如下:

  • 在 Web 上使用的 React 库与 React Native 使用的相同,并在 JavaScriptCore 中运行

  • 发送到本机平台 API 的消息是异步的,并且为了性能目的而批处理

  • React Native 附带了为移动平台实现的组件,而不是 HTML 元素的组件

有关 React Native 的历史和机制的更多信息,请访问code.facebook.com/posts/1014532261909640

React 和 JSX 很熟悉

为 React 实现一个新的渲染目标并不简单。这本质上就像在 iOS 和 Android 上发明一个新的 DOM。那么为什么要经历这么多麻烦呢?

首先,移动应用程序的需求非常大。原因是移动网络浏览器的用户体验不如原生应用程序体验好。其次,JSX 是构建用户界面的绝佳工具。与其学习新技术,使用自己已经掌握的知识要容易得多。

后一点对你来说最相关。如果你正在阅读这本书,你可能对在 Web 应用程序和原生移动应用程序中使用 React 感兴趣。我无法用言语表达 React 在开发资源方面有多么宝贵。与其有一个团队做 Web UI,一个团队做 iOS,一个团队做 Android 等等,只需要一个了解 React 的 UI 团队。

移动浏览器体验

移动浏览器缺乏许多移动应用程序的功能。这是因为浏览器无法复制与 HTML 元素相同的本机平台小部件。你可以尝试这样做,但通常最好只使用本机小部件,而不是尝试复制它。部分原因是这样做需要更少的维护工作,部分原因是使用与平台一致的小部件意味着它们与平台的其他部分一致。例如,如果应用程序中的日期选择器看起来与用户在手机上与之交互的所有日期选择器不同,这不是一件好事。熟悉是关键,使用本机平台小部件使熟悉成为可能。

移动设备上的用户交互与通常为 Web 设计的交互基本不同。例如,Web 应用程序假设存在鼠标,并且按钮上的点击事件只是一个阶段。但是,当用户用手指与屏幕交互时,事情变得更加复杂。移动平台有所谓的手势系统来处理这些。React Native 比 Web 上的 React 更适合处理手势,因为它处理了在 Web 应用程序中不必过多考虑的这些类型的事情。

随着移动平台的更新,您希望您的应用程序的组件也保持更新。这对于 React Native 来说并不是问题,因为它们使用的是来自平台的实际组件。一次,一致性和熟悉度对于良好的用户体验至关重要。因此,当您的应用程序中的按钮看起来和行为方式与设备上的其他每个应用程序中的按钮完全相同时,您的应用程序就会感觉像设备的一部分。

Android 和 iOS,不同但相同

当我第一次听说 React Native 时,我自动地认为它会是一种跨平台解决方案,可以让您编写一个单一的 React 应用程序,可以在任何设备上本地运行。在开始使用 React Native 之前,请摆脱这种思维方式。iOS 和 Android 在许多基本层面上是不同的。甚至它们的用户体验理念也不同,因此试图编写一个可以在两个平台上运行的单一应用程序是完全错误的。

此外,这并不是 React Native 的目标。目标是React 组件无处不在,而不是一次编写,随处运行。在某些情况下,您可能希望您的应用程序利用 iOS 特定的小部件或 Android 特定的小部件。这为特定平台提供了更好的用户体验,并应该超越组件库的可移植性。

在后面的章节中,您将学习有关组织特定于平台的模块的不同策略。

iOS 和 Android 之间有几个领域存在重叠,差异微不足道。这两个小部件旨在以大致相同的方式为用户完成相同的事情。在这些情况下,React Native 将为您处理差异并提供统一的组件。

移动 Web 应用的情况

在上一章中,您学会了如何实现移动优先的 React 组件。您的用户中并非每个人都愿意安装应用程序,特别是如果您的下载量和评分还不高的话。通过 Web 应用程序,用户的准入门槛要低得多——用户只需要一个浏览器。

尽管无法复制原生平台 UI 所提供的一切,但您仍然可以在移动 Web UI 中实现出色的功能。也许拥有一个良好的 Web UI 是提高移动应用程序下载量和评分的第一步。

理想情况下,您应该瞄准以下目标:

  • 标准 Web(笔记本/台式机浏览器)

  • 移动 Web(手机/平板浏览器)

  • 移动应用(手机/平板原生平台)

在这三个领域中投入同样的努力可能并不明智,因为你的用户可能更偏爱其中一个领域。一旦你知道,例如,相对于 Web 版本,你的移动应用程序需求非常高,那么你就应该在那里投入更多的努力。

总结

在本章中,你了解到 React Native 是 Facebook 的一项努力,旨在重用 React 来创建本机移动应用程序。React 和 JSX 非常擅长声明 UI 组件,而现在对移动应用程序的需求非常大,因此使用你已经了解的 Web 知识是有意义的。

移动应用程序比移动浏览器更受欢迎的原因是它们的体验更好。Web 应用程序缺乏处理移动手势的能力,而且通常在外观和感觉上不像移动体验的一部分。

React Native 并不试图实现一个组件库,让你可以构建一个在任何移动平台上运行的单个 React 应用程序。iOS 和 Android 在许多重要方面都有根本的不同。在有重叠的地方,React Native 确实尝试实现共同的组件。现在我们可以使用 React 进行本地构建,那么我们是否会放弃移动 Web 应用程序?这可能永远不会发生,因为用户只能安装那么多应用程序。

现在你知道了 React Native 的主要目标是什么以及它的优势,接下来你将在下一章学习如何开始新的 React Native 项目。

测试你的知识

  1. React Native 的主要目标是什么?

  2. 消除构建移动 Web 应用程序的需求。

  3. 使 React 开发人员能够轻松将他们已经了解的构建 UI 组件的知识应用于构建本机移动应用程序。

  4. 提供统一的用户体验跨所有移动平台。

  5. React Native 在 iOS 和 Android 上提供完全相同的体验吗?

  6. 不,iOS 和 Android 有根本不同的用户体验。

  7. 是的,你希望你的应用在 iOS 和 Android 上的功能完全相同。

  8. React Native 是否消除了对移动 Web 应用程序的需求?

  9. 是的,如果你可以构建本机移动应用程序,就不需要移动 Web 应用程序。

  10. 不,总会有移动 Web 应用程序的需求。当你需要本机移动应用程序时,React Native 就在那里。

进一步阅读

访问以下链接以获取更多信息:

第十三章:启动 React Native 项目

在本章中,您将开始使用 React Native。幸运的是,create-react-native-app命令行工具已经为您处理了创建新项目所涉及的大部分样板。我将解释当您初始化一个空项目时实际为您创建了什么。然后,我将向您展示如何在 iOS 和 Android 模拟器上运行项目。

安装和使用create-react-native-app

创建 React Native 项目的首选工具是create-react-native-app。这个命令行工具是由 React Native 开发者社区创建的,并且遵循了create-react-app工具的步伐。create-react-appcreate-react-native-app的目标是使开发人员能够快速启动他们的项目。您应该能够发出一个命令,生成运行您的 React 或 React Native 应用程序所必需的所有样板。

没有这种类型的工具,您最终会花费大量时间来配置项目的各个方面。首先,开发人员想要构建应用程序。您可以稍后进行配置和优化。

您应该全局安装create-react-native-app,因为这个工具不是针对您正在工作的任何一个项目的特定工具——它为您启动了项目。以下是您可以这样做的方法:

npm install -g create-react-native-app

安装完成后,您将在终端中获得一个新的create-react-native-app命令。您可以使用这个命令来启动您的新 React Native 项目。

创建一个 React Native 应用程序

使用create-react-native-app启动一个新的 React Native 项目涉及调用create-react-native-app命令,并将应用程序的名称作为参数传递进去。例如:

create-react-native-app my-project

这将导致创建一个my-project目录。这里将包含create-react-native-app为您创建的所有样板代码和其他文件。这也是您将找到node_modules目录的地方,其中安装了所有的依赖项。

当您运行此命令时,您将看到类似于以下内容的输出:

Creating a new React Native app in Chapter13/my-project. Using package manager as npm with npm interface. Installing packages. This might take a couple minutes. Installing react-native-scripts... + react-native-scripts@1.14.0 added 442 packages from 477 contributors and audited 1178 packages in 19.128s Installing dependencies using npm... Success! Created my-project at Chapter13/my-project Inside that directory, you can run several commands:
  npm start
 Starts the development server so you can open your app in the Expo app on your phone.  npm run ios
 (Mac only, requires Xcode) Starts the development server and loads your app in an iOS simulator.  npm run android
 (Requires Android build tools) Starts the development server and loads your app on a connected Android device or emulator.  npm test
 Starts the test runner.  npm run eject
 Removes this tool and copies build dependencies, configuration files and scripts into the app directory. If you do this, you can’t go back! We suggest that you begin by typing:
  cd my-project
  npm start Happy hacking!

输出显示了安装依赖项时正在进行的操作,以及准备立即运行的命令。此时,您已经准备好启动您的应用程序。

运行您的应用程序

当您使用create-react-native-app来引导您的 React Native 项目时,会将几个命令添加到您的package.json文件中。这些列在命令输出中(请参阅前一节,了解此输出的外观)。您将使用的最常见的命令是start

npm start

这个命令将启动打包进程。当您更新源代码时,此过程将构建原生 UI 组件。它不会为实际的目标平台执行本机构建,因为这在性能上会太昂贵。相反,它将高效地构建您的应用程序,以便与各种模拟器一起使用开发:

Here's what the output of npm start looks like:
Starting packager... Packager started!
Your app is now running at URL: exp://192.168.86.21:19000 View your app with live reloading:
 Android device:
    -> Point the Expo app to the QR code above.
       (You'll find the QR scanner on the Projects tab of the app.)
  iOS device:
    -> Press s to email/text the app URL to your phone.   Emulator:
    -> Press a (Android) or i (iOS) to start an emulator. Your phone will need to be on the same local network as this computer.
For links to install the Expo app, please visit https://expo.io. Logs from serving your app will appear here. Press Ctrl+C at any time to stop.
 › Press a to open Android device or emulator, or i to open iOS emulator.
 › Press s to send the app URL to your phone number or email address
 › Press q to display QR code.  › Press r to restart packager, or R **to restart packager and clear cache.**
 **› Press d to toggle development mode. (current mode: development)** 

有许多选项可供您模拟您的原生应用程序。默认情况下,您处于开发模式 - 您可能会保持在开发模式。在前面的输出中没有显示的是,输出还包括一个 QR 码,您可以使用 Expo 移动应用程序扫描。

安装和使用 Expo

Expo移动应用程序是一个工具,您可以用它来辅助 React Native 开发。npm start命令启动 React Native 包,它与 Expo 无缝集成(前提是设备与打包程序在同一网络上)。这使您能够在开发过程中在真实移动设备上查看和交互您的应用程序。当您对源代码进行更改时,它甚至支持实时重新加载。

Expo 与移动设备模拟器不同,它使您能够以与用户体验相同的方式体验应用程序。虚拟设备模拟器给出了一个粗略的近似值,但这并不等同于手持设备。此外,并非每个人都有 Macbook,这是模拟 iOS 设备的要求。

您可以通过在 Android 设备上搜索 Play 商店或在 iOS 设备上搜索 App Store 来找到 Expo 应用程序。

当您启动 Expo 时,您将看到一个扫描 QR 码的选项:

当您选择扫描 QR 码时,您手机的摄像头可以扫描终端中打印的 QR 码。这是您将计算机上运行的 React Native 打包程序与您的设备连接的方式。如果您无法扫描 QR 码,您可以通过电子邮件将 Expo 链接发送到您的手机上,在手机上点击它与扫描 QR 码是一样的。

当在 Expo 中打开my-project应用程序时,应该是这样的:

让我们来看看由create-react-native-app为您创建的App.js模块:

import React from 'react';
import { StyleSheet, Text, View } from 'react-native';

export default class App extends React.Component {
  render() {
    return (
      <View style={styles.container}>
        <Text>Open up App.js to start working on your app!</Text>
        <Text>Changes you make will automatically reload.</Text>
        <Text>Shake your phone to open the developer menu.</Text>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center'
  }
});

这个App组件将在屏幕上呈现三行文本,并对View组件应用一些样式。让我们对第一行进行更改,使文本加粗:

import React from 'react';
import { StyleSheet, Text, View } from 'react-native';

export default class App extends React.Component {
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.bold}>
          Open up App.js to start working on your app!
        </Text>
        <Text>Changes you make will automatically reload.</Text>
        <Text>Shake your phone to open the developer menu.</Text>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center'
  },
  bold: {
    fontWeight: 'bold'
  }
});

现在样式中有一个加粗样式,并且这被应用到了第一个Text组件的样式属性上。如果您再次查看手机,您会注意到应用程序已更新:

更改立即反映在您设备上的应用程序中。

使用模拟器

您并不总是随身携带手机,事实上,在开发过程中并不总是需要在物理移动设备上查看您的应用程序。另一个选择是使用模拟物理移动设备的虚拟设备模拟器。React Native 打包程序与模拟器通信的方式与其与 Expo 应用程序通信的方式相同,以支持实时重新加载。

iOS 模拟器

启动 React Native 打包程序后,按“i”键即可启动 iOS 模拟器。您将看到类似于这样的输出:

2:06:04 p.m.: Starting iOS... 2:06:22 p.m.: Finished building JavaScript bundle in 1873ms 2:06:23 p.m.: Running app on Adam in development mode

然后,您将看到一个新窗口打开,模拟设备正在运行您的应用程序:

对应用程序源的实时更新与 Expo 应用程序的工作方式相同。更改会在模拟器中自动反映。

Android 模拟器

Android 模拟器的启动方式与 iOS 模拟器相同。在运行 React Native 打包程序的终端中,按“A”键。但是,有一个警告 - 您必须在启动 React Native 包内的应用程序之前启动 Android 设备模拟器。如果不这样做,当您按“A”键时,您将看到类似于这样的消息:

2:37:02 p.m.: Starting Android... Error running adb: No Android device found.

这在过去一直是 Android 上难以做到的。现在,借助 Android Studio 的帮助,启动 Android 设备模拟器变得简单得多。一旦安装了 Android Studio,您可以打开 Android 虚拟设备管理器并添加任何您喜欢的设备:

您可以单击“创建虚拟设备”按钮来创建一个新设备:

一旦您创建了要在其上测试 React Native 应用程序的设备,您可以单击绿色播放按钮。这将启动模拟器:

如果你回到运行 React Native 打包程序的终端并按下"a",你应该会看到以下输出:

2:49:07 p.m.: Starting Android... 2:49:08 p.m.: Finished building JavaScript bundle in 17ms 2:49:10 p.m.: Running app on Android SDK built for x86 in development mode

如果你回到你的 Android 模拟器,你的 React Native 应用应该已经启动了:

就像 Expo 应用程序和 iOS 模拟器一样,这个模拟器将随着应用程序源代码的更改而实时重新加载,这要归功于 React Native 打包程序。

总结

在本章中,你学会了如何使用create-react-native-app工具启动你的 React。你学会了如何在系统上安装该工具,并使create-react-native-app命令对你创建的任何 React Native 项目可用。然后,你使用该命令启动了一个基本项目。接下来,你在项目中启动了 React Native 打包程序进程。

你学会了如何在移动设备上安装 Expo 应用程序以及如何将其与 React Native 打包程序连接。然后,你进行了代码更改,以演示实时重新加载的工作原理。最后,你学会了如何使用 React Native 打包程序启动 iOS 和 Android 模拟器。

在下一章中,你将学习如何在 React Native 应用程序中构建灵活的布局。

测试你的知识

  1. create-react-native-app工具是由 Facebook 创建的

  2. 是的,create-react-native-app从一开始就存在

  3. 不,这是一个社区支持的工具,跟随create-react-app的脚步

  4. 为什么你应该全局安装create-react-native-app

  5. 因为没有办法在本地安装它

  6. 你不应该。只在本地安装它

  7. 因为这是一个用于生成项目样板的工具,实际上并不是项目的一部分

  8. 一切都应该全局安装。

  9. Expo 应用程序在移动设备上的作用是什么?

  10. 这是一个增强 React Native 应用程序的库

  11. 这是一个帮助开发人员在开发过程中在移动设备上运行他们的应用程序的工具,开销非常小

  12. 这是一个可以在目标设备上本地构建项目并安装的工具

  13. React Native 打包程序能够模拟 iOS 和 Android 设备

  14. 它不会这样做,但它会与 iOS 和 Android 模拟器通信以运行应用程序

  15. 是的,模拟器是 React Native 的一部分

进一步阅读

查看以下链接以了解更多信息:

第十四章:使用 Flexbox 构建响应式布局

在本章中,您将体会到在移动设备屏幕上布局组件的感觉。幸运的是,React Native 为许多您过去可能在 Web 应用程序中使用的 CSS 属性提供了 polyfill。您将学习如何使用 flexbox 模型来布局我们的 React Native 屏幕。

在深入实现布局之前,您将简要介绍 flexbox 和在 React Native 应用程序中使用 CSS 样式属性——这与常规 CSS 样式表不太一样。然后,您将使用 flexbox 实现几个 React Native 布局。

Flexbox 是新的布局标准

在 CSS 引入灵活的盒子布局模型之前,用于构建布局的各种方法都感觉很巧妙,并且容易出错。Flexbox 通过抽象化许多通常需要提供的属性来修复这一问题,以使布局正常工作。

实质上,flexbox 就是其字面意思——一个灵活的盒子模型。这就是 flexbox 的美妙之处——它的简单性。您有一个充当容器的盒子,以及该盒子内的子元素。容器和子元素在屏幕上的呈现方式都是灵活的,如下所示:

Flexbox 容器有一个方向,可以是列(上/下)或行(左/右)。当我第一次学习 flexbox 时,这实际上让我感到困惑:我的大脑拒绝相信行是从左到右移动的。行是堆叠在彼此上面的!要记住的关键是,这是盒子伸展的方向,而不是盒子在屏幕上放置的方向。

有关 flexbox 概念的更深入的处理,请查看此页面:css-tricks.com/snippets/css/a-guide-to-flexbox/

介绍 React Native 样式

是时候实现您的第一个 React Native 应用程序了,超出了create-react-native-app生成的样板。我希望在您开始在下一节中实现 flexbox 布局之前,您能够确保在使用 React Native 样式表时感到舒适。以下是 React Native 样式表的样子:

import { Platform, StyleSheet, StatusBar } from 'react-native';

// Exports a "stylesheet" that can be used
// by React Native components. The structure is
// familiar for CSS authors.
const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'ghostwhite',
    ...Platform.select({
      ios: { paddingTop: 20 },
      android: { paddingTop: StatusBar.currentHeight }
    })
  },

  box: {
    width: 100,
    height: 100,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'lightgray'
  },

  boxText: {
    color: 'darkslategray',
    fontWeight: 'bold'
  }
});

export default styles; 

这是一个 JavaScript 模块,而不是 CSS 模块。如果要声明 React Native 样式,需要使用普通对象。然后,调用StyleSheet.create()并从样式模块导出它。

正如你所看到的,这个样式表有三种样式:containerboxboxText。在容器样式中,有一个调用Platform.select()的方法:

...Platform.select({
  ios: { paddingTop: 20 },
  android: { paddingTop: StatusBar.currentHeight }
})

这个函数将根据移动设备的平台返回不同的样式。在这里,你正在处理顶层容器视图的顶部填充。你可能会在大多数应用中使用这段代码,以确保你的 React 组件不会渲染在设备的状态栏下面。根据平台的不同,填充将需要不同的值。如果是 iOS,paddingTop20。如果是 Android,paddingTop将是StatusBar.currentHeight的值。

前面的Platform.select()代码是一个例子,说明你需要为平台的差异实现一个解决方法。例如,如果StatusBar.currentHeight在 iOS 和 Android 上都可用,你就不需要调用Platform.select()

让我们看看这些样式是如何被导入并应用到 React Native 组件的:

import React from 'react';
import { Text, View } from 'react-native';

// Imports the "styles" stylesheet from the
// "styles" module.
import styles from './styles';

// Renders a view with a square in the middle, and
// some text in the middle of that. The "style" property
// is passed a value from the "styles" stylesheet.
export default () => (
  <View style={styles.container}>
    <View style={styles.box}>
      <Text style={styles.boxText}>I'm in a box</Text>
    </View>
  </View>
); 

这些样式通过style属性分配给每个组件。你正在尝试渲染一个带有一些文本的框在屏幕中间。让我们确保这看起来和我们期望的一样:

太棒了!现在你已经知道如何在 React Native 元素上设置样式,是时候开始创建一些屏幕布局了。

构建 flexbox 布局

在这一部分,你将了解在 React Native 应用中可以使用的几种潜在布局。我想远离一个布局比其他布局更好的想法。相反,我会向你展示 flexbox 布局模型对于移动屏幕有多么强大,这样你就可以设计最适合你的应用的布局。

简单的三列布局

首先,让我们实现一个简单的布局,其中有三个部分在列的方向上弹性伸缩(从上到下)。让我们先来看一下结果屏幕:

这个例子的想法是,你已经为三个屏幕部分设置了样式和标签,使它们突出显示。换句话说,在真实应用中,这些组件不一定会有任何样式,因为它们用于在屏幕上排列其他组件。

让我们来看一下用于创建此屏幕布局的组件:

import React from 'react';
import { Text, View } from 'react-native';

import styles from './styles';

// Renders three "column" sections. The "container"
// view is styled so that it's children flow from
// the top of the screen, to the bottom of the screen.
export default () => (
  <View style={styles.container}>
    <View style={styles.box}>
      <Text style={styles.boxText}>#1</Text>
    </View>
    <View style={styles.box}>
      <Text style={styles.boxText}>#2</Text>
    </View>
    <View style={styles.box}>
      <Text style={styles.boxText}>#3</Text>
    </View>
  </View>
); 

容器视图(最外层的 <View> 组件)是列,子视图是行。<Text> 组件用于标记每一行。在 HTML 元素方面,<View> 类似于 <div>,而 <Text> 类似于 <p>

也许这个例子本来可以被称为“三行布局”,因为它有三行。但与此同时,三个布局部分都在其所在的列的方向上伸展。使用对你来说最有概念意义的命名约定。

现在让我们看一下用于创建此布局的样式:

import { Platform, StyleSheet, StatusBar } from 'react-native';

// Exports a "stylesheet" that can be used
// by React Native components. The structure is
// familiar for CSS authors.
export default StyleSheet.create({
  // The "container" for the whole screen.
  container: {
    // Enables the flexbox layout model...
    flex: 1,
    // Tells the flexbox to render children from
    // top to bottom...
    flexDirection: 'column',
    // Aligns children to the center on the container...
    alignItems: 'center',
    // Defines the spacing relative to other children...
    justifyContent: 'space-around',
    backgroundColor: 'ghostwhite',
    ...Platform.select({
      ios: { paddingTop: 20 },
      android: { paddingTop: StatusBar.currentHeight }
    })
  },

  box: {
    width: 300,
    height: 100,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'lightgray',
    borderWidth: 1,
    borderStyle: 'dashed',
    borderColor: 'darkslategray'
  },

  boxText: {
    color: 'darkslategray',
    fontWeight: 'bold'
  }
}); 

containerflexflexDirection 属性使得行的布局从上到下流动。alignItemsjustifyContent 属性将子元素对齐到容器的中心,并在它们周围添加空间。

让我们看看当你将设备从竖屏旋转到横屏时,这个布局是什么样子的:

flexbox 自动找到了如何为你保留布局。但是,你可以稍微改进一下。例如,横屏模式现在左右有很多浪费的空间。你可以为渲染的盒子创建自己的抽象。

改进后的三列布局

我认为你可以从上一个例子中改进一些东西。让我们修复样式,使得 flexbox 的子元素能够充分利用可用空间。还记得上一个例子中,当你将设备从竖屏旋转到横屏时发生了什么吗?有很多空间被浪费了。让组件自动调整会很好。下面是新样式模块的样子:

import { Platform, StyleSheet, StatusBar } from 'react-native';

const styles = StyleSheet.create({
  container: {
    flex: 1,
    flexDirection: 'column',
    backgroundColor: 'ghostwhite',
    alignItems: 'center',
    justifyContent: 'space-around',
    ...Platform.select({
      ios: { paddingTop: 20 },
      android: { paddingTop: StatusBar.currentHeight }
    })
  },

  box: {
    height: 100,
    justifyContent: 'center',
    // Instead of given the flexbox child a width, we
    // tell it to "stretch" to fill all available space.
    alignSelf: 'stretch',
    alignItems: 'center',
    backgroundColor: 'lightgray',
    borderWidth: 1,
    borderStyle: 'dashed',
    borderColor: 'darkslategray'
  },

  boxText: {
    color: 'darkslategray',
    fontWeight: 'bold'
  }
});

export default styles; 

这里的关键变化是 alignSelf 属性。这告诉具有 box 样式的元素改变宽度或高度(取决于其容器的 flexDirection)以填充空间。此外,box 样式不再定义 width 属性,因为现在将动态计算它。在竖屏模式下,各个部分的样子如下:

现在每个部分都占据了屏幕的整个宽度,这正是你希望发生的。浪费空间的问题实际上在横屏模式下更为突出,所以让我们旋转设备,看看这些部分现在会发生什么:

现在你的布局利用了整个屏幕的宽度,不管方向如何。最后,让我们实现一个适当的Box组件,可以被App.js使用,而不是在原地重复样式属性。Box组件的样子如下:

import React from 'react';
import { PropTypes } from 'prop-types';
import { View, Text } from 'react-native';

import styles from './styles';

// Exports a React Native component that
// renders a "<View>" with the "box" style
// and a "<Text>" component with the "boxText"
// style.
const Box = ({ children }) => (
  <View style={styles.box}>
    <Text style={styles.boxText}>{children}</Text>
  </View>
);

Box.propTypes = {
  children: PropTypes.node.isRequired
};

export default Box; 

现在你已经有了一个不错的布局的开端。接下来,你将学习如何在另一个方向上进行弹性布局——从左到右。

灵活的行

在这一节中,你将学习如何使屏幕布局部分从上到下延伸。为此,你需要一个灵活的行。这个屏幕的样式如下:

import { Platform, StyleSheet, StatusBar } from 'react-native';

const styles = StyleSheet.create({
  container: {
    flex: 1,
    // Tells the child elements to flex from left to
    // right...
    flexDirection: 'row',
    backgroundColor: 'ghostwhite',
    alignItems: 'center',
    justifyContent: 'space-around',
    ...Platform.select({
      ios: { paddingTop: 20 },
      android: { paddingTop: StatusBar.currentHeight }
    })
  },

  box: {
    width: 100,
    justifyContent: 'center',
    alignSelf: 'stretch',
    alignItems: 'center',
    backgroundColor: 'lightgray',
    borderWidth: 1,
    borderStyle: 'dashed',
    borderColor: 'darkslategray'
  },

  boxText: {
    color: 'darkslategray',
    fontWeight: 'bold'
  }
});

export default styles; 

这是App组件,使用了你在上一节中实现的Box组件:

import React from 'react';
import { Text, View, StatusBar } from 'react-native';

import styles from './styles';
import Box from './Box';

// Renders a single row with two boxes that stretch
// from top to bottom.
export default () => (
  <View style={styles.container}>
    <Box>#1</Box>
    <Box>#2</Box>
  </View>
); 

这是纵向模式下的屏幕效果:

这两列从屏幕顶部一直延伸到屏幕底部,这是因为alignSelf属性,它实际上并没有指定要延伸的方向。这两个Box组件从上到下延伸,因为它们显示在一个弹性行中。注意这两个部分之间的间距是从左到右的吗?这是因为容器的flexDirection属性,它的值是row

现在让我们看看当屏幕旋转到横向方向时,这种弹性方向对布局的影响:

由于弹性盒模型具有justifyContent样式属性值为space-around,空间被比例地添加到左侧、右侧和部分之间。

灵活的网格

有时,你需要一个像网格一样流动的屏幕布局。例如,如果你有几个宽度和高度相同的部分,但你不确定会渲染多少个这样的部分呢?弹性盒模型使得从左到右流动的行的构建变得容易,直到屏幕的末端。然后,它会自动继续从左到右在下一行渲染元素。

这是纵向模式下的一个布局示例:

这种方法的美妙之处在于,你不需要提前知道每一行有多少列。每个子元素的尺寸决定了每一行可以容纳多少个元素。让我们来看一下用于创建这个布局的样式:

import { Platform, StyleSheet, StatusBar } from 'react-native';

export default StyleSheet.create({
  container: {
    flex: 1,
    flexDirection: 'row',
    flexWrap: 'wrap',
    backgroundColor: 'ghostwhite',
    alignItems: 'center',
    ...Platform.select({
      ios: { paddingTop: 20 },
      android: { paddingTop: StatusBar.currentHeight }
    })
  },

  box: {
    height: 100,
    width: 100,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'lightgray',
    borderWidth: 1,
    borderStyle: 'dashed',
    borderColor: 'darkslategray',
    margin: 10
  },

  boxText: {
    color: 'darkslategray',
    fontWeight: 'bold'
  }
}); 

这是渲染每个部分的App组件:

import React from 'react';
import { View, StatusBar } from 'react-native';

import styles from './styles';
import Box from './Box';

// An array of 10 numbers, representing the grid
// sections to render.
const boxes = new Array(10).fill(null).map((v, i) => i + 1);

export default () => (
  <View style={styles.container}>
    <StatusBar hidden={false} />
    {/* Renders 10 "<Box>" sections */}
    {boxes.map(i => <Box key={i}>#{i}</Box>)}
  </View>
); 

最后,让我们确保横向方向与这个布局兼容:

你可能已经注意到右侧有一些多余的空间。请记住,这些部分只在本书中可见,因为我们希望它们可见。在真实的应用中,它们只是其他 React Native 组件的分组。但是,如果屏幕右侧的空间成为问题,请尝试调整子组件的边距和宽度。

灵活的行和列

在本章的最后一节中,您将学习如何将行和列组合在一起,为应用程序创建复杂的布局。例如,有时您需要能够在行内嵌套列或在列内嵌套行。让我们看看一个应用程序的App组件,它在行内嵌套列:

import React from 'react';
import { View, StatusBar } from 'react-native';

import styles from './styles';
import Row from './Row';
import Column from './Column';
import Box from './Box';

export default () => (
  <View style={styles.container}>
    <StatusBar hidden={false} />
    {/* This row contains two columns. The first column
        has boxes "#1" and "#2". They will be stacked on
        top of one another. The next column has boxes "#3"
        and "#4", which are also stacked on top of one
        another */}
    <Row>
      <Column>
        <Box>#1</Box>
        <Box>#2</Box>
      </Column>
      <Column>
        <Box>#3</Box>
        <Box>#4</Box>
      </Column>
    </Row>
    <Row>
      <Column>
        <Box>#5</Box>
        <Box>#6</Box>
      </Column>
      <Column>
        <Box>#7</Box>
        <Box>#8</Box>
      </Column>
    </Row>
    <Row>
      <Column>
        <Box>#9</Box>
        <Box>#10</Box>
      </Column>
      <Column>
        <Box>#11</Box>
        <Box>#12</Box>
      </Column>
    </Row>
  </View>
); 

你已经为布局部分(<Row><Column>)和内容部分(<Box>)创建了抽象。让我们看看这个屏幕是什么样子的:

这个布局可能看起来很熟悉,因为你在本章中已经做过了。关键区别在于这些内容部分的排序方式。例如,#2 不会放在#1 的左侧,而是放在下面。这是因为我们将#1 和#2 放在了<Column>中。#3 和#4 也是一样。这两列放在了一行中。然后下一行开始,依此类推。

通过嵌套行 flexbox 和列 flexbox,您可以实现许多可能的布局之一。现在让我们看看Row组件:

import React from 'react';
import PropTypes from 'prop-types';
import { View } from 'react-native';

import styles from './styles';

// Renders a "View" with the "row" style applied to
// it. It's "children" will flow from left to right.
const Row = ({ children }) => (
  <View style={styles.row}>{children}</View>
);

Row.propTypes = {
  children: PropTypes.node.isRequired
};

export default Row; 

这个组件将<View>组件应用了row样式。最终结果是在创建复杂布局时,App组件中的 JSX 标记更清晰。最后,让我们看看Column组件:

import React from 'react';
import PropTypes from 'prop-types';
import { View } from 'react-native';

import styles from './styles';

// Renders a "View" with the "column" style applied
// to it. It's children will flow from top-to-bottom.
const Column = ({ children }) => (
  <View style={styles.column}>{children}</View>
);

Column.propTypes = {
  children: PropTypes.node.isRequired
};

export default Column; 

这看起来就像Row组件,只是应用了不同的样式。它也和Row有相同的作用 - 为其他组件的布局提供更简单的 JSX 标记。

总结

本章向您介绍了 React Native 中的样式。虽然您可以使用许多您习惯的 CSS 样式属性,但在 Web 应用程序中使用的 CSS 样式表看起来非常不同。换句话说,它们由普通的 JavaScript 对象组成。

然后,您学习了如何使用主要的 React Native 布局机制 - flexbox。这是如今布局大多数 Web 应用程序的首选方式,因此能够在原生应用中重用这种方法是有意义的。您创建了几种不同的布局,并看到它们在纵向和横向方向上的外观。

在接下来的章节中,你将开始为你的应用实现导航。

测试你的知识

  1. CSS 样式和 React Native 组件使用的样式有什么区别?

  2. React Native 与 CSS 共享许多样式属性。样式属性在 React Native 中以普通对象属性的形式表达

  3. 没有区别——你可以像其他 React 组件一样样式化 React Native 组件

  4. 它们完全不同——React Native 不与 CSS 共享任何样式属性

  5. 为什么在设计布局时需要考虑状态栏?

  6. 你不需要考虑状态栏

  7. 因为状态栏可能会干扰你的 iOS 组件

  8. 因为状态栏可能会干扰你的 Android 组件

  9. 什么是弹性盒模型?

  10. 它是用于控制 View 组件如何伸缩以占据布局中的水平空间的模型

  11. 它提供了灵活的列,可以响应屏幕方向的变化

  12. 弹性盒布局模型用于以一种抽象方式布置组件,并在布局变化时自动伸缩

  13. 在考虑布局选项时,屏幕方向是否是一个因素?

  14. 是的,你总是需要确保在开发过程中,横向或纵向方向没有意外

  15. 不,方向细节会被处理,这样你就可以专注于应用功能

进一步阅读

点击以下链接获取更多信息:

第十五章:在屏幕之间导航

本章的重点是在 React Native 应用程序中导航到组成应用程序的屏幕之间。原生应用程序中的导航与 Web 应用程序中的导航略有不同——主要是因为用户没有任何 URL 的概念。在之前的 React Native 版本中,有原始的导航器组件,可以用来控制屏幕之间的导航。这些组件存在一些挑战,导致需要更多的代码来完成基本的导航任务。

最近的 React Native 版本鼓励你使用react-navigation包,这将是本章的重点,尽管还有其他几个选项。你将学习导航基础知识,向屏幕传递参数,更改标题内容,使用选项卡和抽屉导航,以及处理导航状态。

导航基础知识

让我们从使用react-navigation进行从一个页面到另一个页面的基础知识开始。App组件的外观如下:

import { createStackNavigator } from 'react-navigation';
import Home from './Home';
import Settings from './Settings';

export default createStackNavigator(
  {
    Home,
    Settings
  },
  { initialRouteName: 'Home' }
);

createStackNavigator()函数是设置导航所需的全部内容。这个函数的第一个参数是一个屏幕组件的映射,可以进行导航。第二个参数是更一般的导航选项——在这种情况下,你告诉导航器Home应该是默认的屏幕组件。

Home组件的外观如下:

import React from 'react';
import { View, Text, Button } from 'react-native';

import styles from './styles';

export default ({ navigation }) => (
  <View style={styles.container}>
    <Text>Home Screen</Text>
    <Button
      title="Settings"
      onPress={() => navigation.navigate('Settings')}
    />
  </View>
);

这是您典型的功能性 React 组件。你可以在这里使用基于类的组件,但没有必要,因为没有生命周期方法或状态。它呈现了一个应用了容器样式的View组件。接下来是一个标记屏幕的Text组件,后面是一个Button组件。屏幕可以是任何你想要的东西——它只是一个常规的 React Native 组件。导航器组件为你处理路由和屏幕之间的过渡。

这个按钮的onPress处理程序在点击时导航到Settings屏幕。这是通过调用navigation.navigate('Settings')来实现的。navigation属性是由react-navigation传递给屏幕组件的,并包含你需要的所有路由功能。与在 React web 应用程序中使用 URL 不同,在这里你调用导航器 API 函数并传递屏幕的名称。

接下来,让我们来看看Settings组件:

import React from 'react';
import { View, Text, Button } from 'react-native';

import styles from './styles';

export default ({ navigation }) => (
  <View style={styles.container}>
    <Text>Settings Screen</Text>
    <Button
      title="Home"
      onPress={() => navigation.navigate('Home')}
    />
  </View>
);

这个组件就像主页组件一样,只是文本不同,当点击按钮时,您会被带回到主页屏幕。

这就是主页屏幕的样子:

您可以单击设置按钮,然后将被带到设置屏幕,看起来像这样:

这个屏幕看起来几乎和主页屏幕一样。它有不同的文本和一个不同的按钮,当点击时会带您回到主页屏幕。但是,还有另一种方法可以回到主页屏幕。看一下屏幕顶部,您会注意到一个白色的导航栏。在导航栏的左侧,有一个返回箭头。这就像 Web 浏览器中的返回按钮一样,会带您回到上一个屏幕。react-navigation的好处在于它会为您渲染这个导航栏。

有了这个导航栏,您不必担心布局样式如何影响状态栏。您只需要担心每个屏幕内的布局。

如果您在 Android 上运行此应用程序,您将在导航栏中看到相同的返回按钮。但您也可以使用大多数 Android 设备上应用程序外部找到的标准返回按钮。

路由参数

当您开发 React Web 应用程序时,一些路由中有动态数据。例如,您可以链接到一个详情页面,在 URL 中,您会有某种标识符。然后组件就有了渲染特定详细信息所需的内容。相同的概念也存在于react-navigation中。您不仅可以指定要导航到的屏幕的名称,还可以传递额外的数据。

让我们看看路由参数的实际应用,从App组件开始:

import { createStackNavigator } from 'react-navigation';
import Home from './Home';
import Details from './Details';

export default createStackNavigator(
  {
    Home,
    Details
  },
  { initialRouteName: 'Home' }
);

这看起来和前面的例子一样,只是没有设置页面,而是有一个详情页面。这是您想要动态传递数据的页面,以便它可以呈现适当的信息。首先,让我们看看主页屏幕组件:

import React from 'react';
import { View, Text, Button } from 'react-native';

import styles from './styles';

export default ({ navigation }) => (
  <View style={styles.container}>
    <Text>Home Screen</Text>
    <Button
      title="First Item"
      onPress={() =>
        navigation.navigate('Details', { title: 'First Item' })
      }
    />
    <Button
      title="Second Item"
      onPress={() =>
        navigation.navigate('Details', { title: 'Second Item' })
      }
    />
    <Button
      title="Third Item"
      onPress={() =>
        navigation.navigate('Details', { title: 'Third Item' })
      }
    />
  </View>
);

“主页”屏幕有三个Button组件,每个都导航到“详情”屏幕。注意navigation.navigate()的调用。除了屏幕名称,它们每个都有第二个参数。这些是包含特定数据的对象,这些数据将传递给“详情”屏幕。接下来,让我们看看“详情”屏幕,并了解它如何使用这些路由参数:

import React from 'react';
import { View, Text, Button } from 'react-native';

import styles from './styles';

export default ({ navigation }) => (
  <View style={styles.container}>
    <Text>{navigation.getParam('title')}</Text>
  </View>
);

尽管此示例只传递了一个参数—title—您可以根据需要向屏幕传递尽可能多的参数。您可以使用navigator.getParam()函数来查找值来访问这些参数。

渲染时,“主页”屏幕如下所示:

如果您点击第一项按钮,您将进入使用路由参数数据呈现的详情屏幕:

在导航栏中,您可以点击返回按钮返回到“主页”屏幕。如果您点击“主页”屏幕上的任何其他按钮,您将被带回到带有更新数据的“详情”屏幕。路由参数是必要的,以避免编写重复的组件。您可以将参数传递给navigator.navigate(),就像将 props 传递给 React 组件一样。

导航头

到目前为止,在本章中创建的导航栏都有点普通。这是因为您还没有配置它们执行任何操作,所以react-navigation只会渲染一个带有返回按钮的普通栏。您创建的每个屏幕组件都可以配置特定的导航头内容。

让我们在之前使用按钮导航到详情页面的示例上进行扩展。App组件保持不变,所以让我们先看看Home组件:

import React from 'react';
import { View, Button } from 'react-native';

import styles from './styles';

const Home = ({ navigation }) => (
  <View style={styles.container}>
    <Button
      title="First Item"
      onPress={() =>
        navigation.navigate('Details', {
          title: 'First Item',
          content: 'First Item Content',
          stock: 1
        })
      }
    />
    <Button
      title="Second Item"
      onPress={() =>
        navigation.navigate('Details', {
          title: 'Second Item',
          content: 'Second Item Content',
          stock: 0
        })
      }
    />
    <Button
      title="Third Item"
      onPress={() =>
        navigation.navigate('Details', {
          title: 'Third Item',
          content: 'Third Item Content',
          stock: 200
        })
      }
    />
  </View>
);

Home.navigationOptions = {
  title: 'Home'
};

export default Home;

您将注意到的第一件事是,每个按钮都向“详情”组件传递了更多的路由参数:contentstock。您马上就会明白为什么。正是Home.navigationOptions的值为您配置了导航头。在这种情况下,“主页”屏幕正在设置“标题”。

“主页”屏幕是一个功能性组件,所以您可以将navigationOptions设置为函数的属性。如果您的组件是基于类的,因为它具有生命周期方法的状态,您可以将其定义为静态类属性:

class MyScreen extends Component { static navigationOptions = {...} ... }

接下来,让我们看看“详情”组件:

import React from 'react';
import { View, Text, Button } from 'react-native';

import styles from './styles';

const Details = ({ navigation }) => (
  <View style={styles.container}>
    <Text>{navigation.getParam('content')}</Text>
  </View>
);

Details.navigationOptions = ({ navigation }) => ({
  title: navigation.getParam('title'),
  headerRight: (
    <Button
      title="Buy"
      onPress={() => {}}
      disabled={navigation.getParam('stock') === 0}
    />
  )
});

export default Details;

这一次,Details组件呈现内容路由参数。像Home组件一样,它也有一个navigationOptions属性。在这种情况下,它是一个函数,而不是一个对象。这是因为您根据传递给屏幕的参数动态更改导航头内容。该函数传递了一个navigation属性 - 这与传递给Details组件的值相同。您可以调用navigation.getParam()来获取标题,以根据路由参数更改导航头。

接下来,使用headerRight选项将Button组件添加到导航栏的右侧。这就是股票参数发挥作用的地方。如果这个值是 0,因为没有任何库存,你想要禁用购买按钮。

现在让我们看看所有这些是如何工作的,从“主页”屏幕开始:

导航栏中的标题文本是由“主页”屏幕组件设置的。接下来,尝试点击第一项按钮:

导航栏中的标题是根据传递给Details组件的title参数设置的。导航栏右侧呈现的购买按钮也由Details组件呈现。它是启用的,因为stock参数值为 1。现在尝试返回到“主页”屏幕,并点击第二项按钮:

标题和页面内容都反映了传递给Details的新参数值。但购买按钮也是如此。它处于禁用状态,因为股票参数值为 0,这意味着它无法购买。

选项卡和抽屉导航

到目前为止,本章中的每个示例都使用了Button组件来链接到应用程序中的其他屏幕。您可以使用react-navigation中的函数,根据您提供的屏幕组件自动为您创建选项卡或抽屉导航。

让我们创建一个示例,在 iOS 上使用底部选项卡导航,在 Android 上使用抽屉导航。

您不仅限于在 iOS 上使用选项卡导航或在 Android 上使用抽屉导航。我只是选择这两个来演示如何根据平台使用不同的导航模式。如果您愿意,您可以在两个平台上使用完全相同的导航模式。这是App组件的外观:

import {
  createBottomTabNavigator,
  createDrawerNavigator
} from 'react-navigation';
import { Platform } from 'react-native';
import Home from './Home';
import News from './News';
import Settings from './Settings';

const { createNavigator } = Platform.select({
  ios: { createNavigator: createBottomTabNavigator },
  android: { createNavigator: createDrawerNavigator }
});

export default createNavigator(
  {
    Home,
    News,
    Settings
  },
  { initialRouteName: 'Home' }
);

不要使用createStackNavigator()函数来创建你的导航器,而是从react-navigation中导入createBottomTabNavigator()createDrawerNavigator()函数:

import {
  createBottomTabNavigator,
  createDrawerNavigator
} from 'react-navigation';

然后,你使用react-native中的Platform实用程序来决定使用这两个函数中的哪一个。根据平台的不同,结果被分配给createNavigator()

const { createNavigator } = Platform.select({
  ios: { createNavigator: createBottomTabNavigator },
  android: { createNavigator: createDrawerNavigator }
});

现在你可以调用createNavigator()并将其传递给你的屏幕。生成的选项卡或抽屉导航将被创建和渲染给你:

export default createNavigator(
  {
    Home,
    News,
    Settings
  },
  { initialRouteName: 'Home' }
);

接下来,让我们看一下Home屏幕组件:

import React from 'react';
import { View, Text } from 'react-native';

import styles from './styles';

const Home = ({ navigation }) => (
  <View style={styles.container}>
    <Text>Home Content</Text>
  </View>
);

Home.navigationOptions = {
  title: 'Home'
};

export default Home;

它在导航栏中设置title并呈现一些基本内容。NewsSettings组件本质上与Home相同。

iOS 上的底部选项卡导航如下所示:

组成你的应用程序的三个屏幕在底部列出。当前屏幕被标记为活动状态,你可以点击其他选项卡来移动。

现在,让我们看看 Android 上的抽屉布局是什么样子的:

要打开抽屉,你需要从屏幕的左侧滑动。一旦打开,你将看到按钮,可以带你到应用程序的各个屏幕。

从屏幕左侧滑动打开抽屉是默认模式。你可以配置抽屉从任何方向滑动打开。

处理状态

React 应用程序具有传递给呈现功能并需要状态数据的组件的状态。例如,想象一下,你正在设计一个使用react-navigation的应用程序,不同的屏幕依赖于相同的状态数据。你如何将状态数据传递给这些屏幕组件?它们如何更新应用程序状态?

首先,让我们考虑将应用程序状态放在哪里。最自然的地方是App组件。到目前为止,在本章中,示例直接导出了对createStackNavigator()的调用。这个函数是一个高阶函数 - 它返回一个新的 React 组件。这意味着你可以在由createStackNavigator()返回的导航组件周围包装自己的有状态组件。

为了说明这个想法,让我们重新访问之前的例子,其中你有一个列出导航到Details屏幕的项目按钮的Home屏幕。下面是新的App组件的样子:

import React, { Component } from 'react';
import { createStackNavigator } from 'react-navigation';
import Home from './Home';
import Details from './Details';

const Nav = createStackNavigator(
  {
    Home,
    Details
  },
  { initialRouteName: 'Home' }
);

export default class App extends Component {
  state = {
    stock: {
      first: 1,
      second: 0,
      third: 200
    }
  };

  updateStock = id => {
    this.setState(({ stock }) => ({
      stock: {
        ...stock,
        [id]: stock[id] === 0 ? 0 : stock[id] - 1
      }
    }));
  };

  render() {
    const props = {
      ...this.state,
      updateStock: this.updateStock
    };

    return <Nav screenProps={props} />;
  }
}

首先,你使用createStackNavigator()函数来创建你的导航器组件:

const Nav = createStackNavigator(
  {
    Home,
    Details
  },
  { initialRouteName: 'Home' }
);

现在您有一个可以渲染的Nav组件。接下来,您可以创建一个带有状态的常规 React 组件:

export default class App extends Component {
  state = {
    stock: {
      first: 1,
      second: 0,
      third: 200
    }
  };
  ...
}

这个组件中使用的状态表示每个物品可供购买的数量。接下来,您有updateStock()函数,用于更新给定物品 ID 的库存状态:

updateStock = id => {
  this.setState(({ stock }) => ({
    stock: {
      ...stock,
      [id]: stock[id] === 0 ? 0 : stock[id] - 1
    }
  }));
};

传递给这个函数的 ID 的库存状态会减少 1,除非已经为 0。当单击物品的“购买”按钮时,可以使用这个函数来检查其库存数量是否减少 1。最后,您有render()方法,它可以渲染Nav组件:

render() {
  const props = {
    ...this.state,
    updateStock: this.updateStock
  };

  return <Nav screenProps={props} />;
}

App的状态作为 props 传递给Nav。还将updateStock()函数作为 prop 传递,以便屏幕组件可以使用它。现在让我们来看一下Home屏幕:

import React from 'react';
import { View, Button } from 'react-native';

import styles from './styles';

const Home = ({ navigation, screenProps: { stock } }) => (
  <View style={styles.container}>
    <Button
      title={`First Item (${stock.first})`}
      onPress={() =>
        navigation.navigate('Details', {
          id: 'first',
          title: 'First Item',
          content: 'First Item Content'
        })
      }
    />
    <Button
      title={`Second Item (${stock.second})`}
      onPress={() =>
        navigation.navigate('Details', {
          id: 'second',
          title: 'Second Item',
          content: 'Second Item Content'
        })
      }
    />
    <Button
      title={`Third Item (${stock.third})`}
      onPress={() =>
        navigation.navigate('Details', {
          id: 'third',
          title: 'Third Item',
          content: 'Third Item Content'
        })
      }
    />
  </View>
);

Home.navigationOptions = {
  title: 'Home'
};

export default Home;

再次,您有三个Button组件,用于导航到Details屏幕并传递路由参数。在这个版本中添加了一个新参数:id。每个按钮的标题都反映了给定物品的库存数量。这个值是应用程序状态的一部分,并通过属性传递给屏幕组件。然而,所有这些属性都是通过screenProps属性访问的。

经验法则:如果将 prop 传递给导航组件,则可以通过screenProps属性访问它。如果通过navigator.navigate()将值传递给屏幕,则可以通过调用navigator.getParam()来访问它。

接下来让我们来看一下Details组件:

import React from 'react';
import { View, Text, Button } from 'react-native';

import styles from './styles';

const Details = ({ navigation }) => (
  <View style={styles.container}>
    <Text>{navigation.getParam('content')}</Text>
  </View>
);

Details.navigationOptions = ({
  navigation,
  screenProps: { stock, updateStock }
}) => {
  const id = navigation.getParam('id');
  const title = navigation.getParam('title');

  return {
    title,
    headerRight: (
      <Button
        title="Buy"
        onPress={() => updateStock(id)}
        disabled={stock[id] === 0}
      />
    )
  };
};

export default Details;

idtitle路由参数用于操作导航栏中的内容。title参数设置标题。id被“Buy”按钮的onPress处理程序使用,通过将其传递给updateStock(),当按钮被按下时,适当的物品库存数量会更新。disabled属性也依赖于id参数来查找库存数量。就像Home屏幕一样,从App组件传递下来的库存和updateStock()props 都可以通过 screenProps 应用程序访问。

这是Home屏幕在首次渲染时的样子:

每个物品按钮上的库存数量都反映了一个数字。让我们按下“First Item”按钮并导航到Details页面:

导航栏中的购买按钮已启用,因为库存数量为 1。让我们继续按下购买按钮,看看会发生什么:

按下购买按钮后,它变为禁用状态。这是因为该商品的库存值为 1。通过按下购买按钮,你调用了updateStock()函数,将该值更新为 0。由于状态改变,App组件重新渲染了Nav组件,进而使用新的属性值重新渲染了你的Details屏幕组件。

让我们回到“主页”屏幕,看看由于状态更新而发生了什么变化:

正如预期的那样,第一项按钮文本旁边呈现的库存数量为 0,反映了刚刚发生的状态变化。

这个例子表明,你可以让顶层的App组件处理应用程序状态,同时将其传递给各个应用程序屏幕,以及发出状态更新的函数。

总结

在本章中,你学会了移动 web 应用程序和 web 应用程序一样需要导航。尽管有所不同,但是移动应用程序和 web 应用程序的导航有足够的概念上的相似之处,使得移动应用程序的路由和导航不必成为一个麻烦。

早期版本的 React Native 尝试提供组件来帮助管理移动应用程序中的导航,但这些从未真正生效。相反,React Native 社区主导了这一领域。其中一个例子就是react-navigation库,本章的重点。

你学会了如何使用 react-navigation 进行基本导航。然后,你学会了如何控制导航栏中的标题组件。接下来,你学会了选项卡和抽屉导航。这两个导航组件可以根据屏幕组件自动渲染应用的导航按钮。最后,你学会了如何在保持导航的同时,仍然能够从顶层应用向屏幕组件传递状态数据。

在下一章中,你将学习如何渲染数据列表。

测试你的知识

  1. 在 React web 应用和 React Native 应用中,导航的主要区别是什么?

  2. 在导航方面,Web 应用和移动应用之间没有实质性的区别。

  3. Web 应用程序依赖 URL 作为移动的中心概念。原生应用程序没有这样的概念,所以开发人员和他们使用的导航库来管理他们的屏幕。

  4. 原生应用代码与 web 应用程序一样使用 URL,但这些 URL 对用户来说是不可见的。

  5. 应该使用什么函数来导航到新的屏幕?

  6. 屏幕组件会被传递一个导航属性。你应该使用navigation.navigate()来切换到另一个屏幕。

  7. 屏幕组件会自动添加导航方法。

  8. 有一个全局导航对象,其中包含可以使用的导航方法。

  9. react-navigation 是否为你处理返回按钮功能?

  10. 是的。包括在安卓系统上内置的返回按钮。

  11. 不,你必须自己实现所有返回按钮的行为。

  12. 你如何将数据传递给屏幕?

  13. 你可以将一个普通对象作为navigation.navigate()的第二个参数。然后可以通过navigation.getParam()在屏幕上访问这些属性。

  14. 你必须重新渲染屏幕组件,将从导航中作为属性获取的参数传递给它。

  15. 你不会将数据传递给屏幕。设置应用级别的状态是将数据传递给屏幕组件的唯一方法。

进一步阅读

查看以下链接获取更多信息:

第十六章:渲染项目列表

在本章中,你将学习如何处理项目列表。列表是常见的 Web 应用程序组件。虽然使用<ul><li>元素构建列表相对比较简单,但在原生移动平台上做类似的事情要复杂得多。

幸运的是,React Native 提供了一个隐藏所有复杂性的项目列表接口。首先,通过一个例子来了解项目列表的工作原理。然后,学习如何构建改变列表中显示的数据的控件。最后,你将看到一些从网络获取项目的例子。

渲染数据集合

让我们从一个例子开始。你将用来渲染列表的 React Native 组件是FlatList,它在 iOS 和 Android 上的工作方式相同。列表视图接受一个data属性,它是一个对象数组。这些对象可以有任何你喜欢的属性,但它们确实需要一个键属性。这类似于在<ul>元素内部渲染<li>元素时对键属性的要求。这有助于列表在列表数据发生变化时高效地渲染列表。

现在让我们实现一个基本的列表。以下是渲染基本 100 个项目列表的代码:

import React from 'react';
import { Text, View, FlatList } from 'react-native';

import styles from './styles';

const data = new Array(100)
  .fill(null)
  .map((v, i) => ({ key: i.toString(), value: `Item ${i}` }));

export default () => (
  <View style={styles.container}>
    <FlatList
      data={data}
      renderItem={({ item }) => (
        <Text style={styles.item}>{item.value}</Text>
      )}
    />
  </View>
); 

让我们从这里开始,首先是data常量。这是一个包含 100 个项目的数组。它是通过用 100 个空值填充一个新数组,然后将其映射到一个你想要传递给<FlatList>的新数组来创建的。每个对象都有一个键属性,因为这是一个要求。其他任何东西都是可选的。在这种情况下,你决定添加一个值属性,这个值稍后会被使用或在列表被渲染时使用。

接下来,你渲染<FlatList>组件。它位于一个<View>容器内,因为列表视图需要一个高度才能正确地进行滚动。datarenderItem属性被传递给<FlatList>,最终确定了渲染的内容。

乍一看,似乎FlatList组件并没有做太多事情。你必须弄清楚项目的外观?是的,FlatList组件应该是通用的。它应该擅长处理更新,并为我们嵌入滚动功能到列表中。以下是用于渲染列表的样式:

import { StyleSheet } from 'react-native';

export default StyleSheet.create({
  container: {
    // Flexing from top to bottom gives the
    // container a height, which is necessary
    // to enable scrollable content.
    flex: 1,
    flexDirection: 'column',
    paddingTop: 20,
  },

  item: {
    margin: 5,
    padding: 5,
    color: 'slategrey',
    backgroundColor: 'ghostwhite',
    textAlign: 'center',
  },
}); 

在这里,你正在为列表中的每个项目设置样式。否则,每个项目将只是文本,并且很难区分其他列表项目。container样式通过将flexDirection设置为column来给列表设置高度。没有高度,你将无法正确滚动。

现在让我们看看这个东西现在是什么样子的:

如果你在模拟器中运行这个例子,你可以点击并按住鼠标按钮在屏幕的任何地方,就像手指一样,然后通过项目上下滚动。

对列表进行排序和过滤

现在你已经学会了FlatList组件的基础知识,包括如何向它们传递数据,让我们在之前实现的列表中添加一些控件。FlatList组件帮助你为列表控件渲染固定位置的内容。你还将看到如何操作数据源,最终驱动屏幕上的渲染内容。

在实现列表控件组件之前,可能有必要回顾一下这些组件的高层结构,以便代码有更多的上下文。以下是你将要实现的组件结构的示例:

这些组件各自负责什么:

  • ListContainer: 列表的整体容器;它遵循熟悉的 React 容器模式

  • List: 一个无状态组件,将相关的状态片段传递给ListControls和 React Native 的ListView组件

  • ListControls: 一个包含改变列表状态的各种控件的组件

  • ListFilter: 用于过滤项目列表的控件

  • ListSort: 用于改变列表排序顺序的控件

  • FlatList: 实际的 React Native 组件,用于渲染项目

在某些情况下,像这样拆分列表的实现可能有些过度。然而,我认为如果你的列表需要控件,你可能正在实现一些将受益于有一个经过深思熟虑的组件架构的东西。

现在,让我们深入到这个列表的实现中,从ListContainer组件开始:

import React, { Component } from 'react';

import List from './List';

const mapItems = items =>
  items.map((value, i) => ({ key: i.toString(), value }));

// Performs sorting and filtering on the given "data".
const filterAndSort = (data, text, asc) =>
  data
    .filter(
      i =>
        // Items that include the filter "text" are returned.
        // Unless the "text" argument is an empty string,
        // then everything is included.
        text.length === 0 || i.includes(text)
    )
    .sort(
      // Sorts either ascending or descending based on "asc".
      asc
        ? (a, b) => (b > a ? -1 : a === b ? 0 : 1)
        : (a, b) => (a > b ? -1 : a === b ? 0 : 1)
    );

class ListContainer extends Component {
  state = {
    data: filterAndSort(
      new Array(100).fill(null).map((v, i) => `Item ${i}`),
      '',
      true
    ),
    asc: true,
    filter: ''
  };

  render() {
    return (
      <List
        data={mapItems(this.state.data)}
        asc={this.state.asc}
        onFilter={text => {
          // Updates the "filter" state, the actualy filter text,
          // and the "source" of the list. The "data" state is
          // never actually touched - "filterAndSort()" doesn't
          // mutate anything.
          this.setState({
            filter: text,
            data: filterAndSort(this.state.data, text, this.state.asc)
          });
        }}
        onSort={() => {
          this.setState({
            // Updates the "asc" state in order to change the
            // order of the list. The same principles as used
            // in the "onFilter()" handler are applied here,
            // only with diferent arguments passed to
            // "filterAndSort()"
            asc: !this.state.asc,
            data: filterAndSort(
              this.state.data,
              this.state.filter,
              !this.state.asc
            )
          });
        }}
      />
    );
  }
}

export default ListContainer;

如果这看起来有点多,那是因为确实如此。这个容器组件有很多状态需要处理。它还有一些需要提供给其子组件的非平凡行为。如果从封装状态的角度来看,它会更容易理解。它的工作是使用状态数据填充列表并提供操作此状态的函数。

在理想的情况下,此容器的子组件应该很简单,因为它们不必直接与状态进行交互。让我们接下来看一下List组件:

import React from 'react';
import PropTypes from 'prop-types';
import { Text, FlatList } from 'react-native';

import styles from './styles';
import ListControls from './ListControls';

const List = ({ Controls, data, onFilter, onSort, asc }) => (
  <FlatList
    data={data}
    ListHeaderComponent={<Controls {...{ onFilter, onSort, asc }} />}
    renderItem={({ item }) => (
      <Text style={styles.item}>{item.value}</Text>
    )}
  />
);

List.propTypes = {
  Controls: PropTypes.func.isRequired,
  data: PropTypes.array.isRequired,
  onFilter: PropTypes.func.isRequired,
  onSort: PropTypes.func.isRequired,
  asc: PropTypes.bool.isRequired
};

List.defaultProps = {
  Controls: ListControls
};

export default List; 

此组件将ListContainer组件的状态作为属性,并呈现FlatList组件。相对于之前的示例,这里的主要区别是ListHeaderComponent属性。这会呈现列表的控件。这个属性特别有用的地方在于它在可滚动的列表内容之外呈现控件,确保控件始终可见。

还要注意,您正在将自己的ListControls组件指定为controls属性的默认值。这样可以方便其他人传入自己的列表控件。接下来让我们看一下ListControls组件:

import React from 'react';
import PropTypes from 'prop-types';
import { View } from 'react-native';

import styles from './styles';
import ListFilter from './ListFilter';
import ListSort from './ListSort';

// Renders the "<ListFilter>" and "<ListSort>"
// components within a "<View>". The
// "styles.controls" style lays out the controls
// horizontally.
const ListControls = ({ onFilter, onSort, asc }) => (
  <View style={styles.controls}>
    <ListFilter onFilter={onFilter} />
    <ListSort onSort={onSort} asc={asc} />
  </View>
);

ListControls.propTypes = {
  onFilter: PropTypes.func.isRequired,
  onSort: PropTypes.func.isRequired,
  asc: PropTypes.bool.isRequired
};

export default ListControls; 

此组件将ListFilterListSort控件组合在一起。因此,如果要添加另一个列表控件,可以在此处添加。现在让我们来看一下ListFilter的实现:

import React from 'react';
import PropTypes from 'prop-types';
import { View, TextInput } from 'react-native';

import styles from './styles';

// Renders a "<TextInput>" component which allows the
// user to type in their filter text. This causes
// the "onFilter()" event handler to be called.
// This handler comes from "ListContainer" and changes
// the state of the list data source.
const ListFilter = ({ onFilter }) => (
  <View>
    <TextInput
      autoFocus
      placeholder="Search"
      style={styles.filter}
      onChangeText={onFilter}
    />
  </View>
);

ListFilter.propTypes = {
  onFilter: PropTypes.func.isRequired
};

export default ListFilter; 

过滤控件是一个简单的文本输入,当用户输入时过滤项目列表。处理此操作的onChange函数来自ListContainer组件。

接下来让我们看一下ListSort组件:

import React from 'react';
import PropTypes from 'prop-types';
import { Text } from 'react-native';

// The arrows to render based on the state of
// the "asc" property. Using a Map let's us
// stay declarative, rather than introducing
// logic into the JSX.
const arrows = new Map([[true, '▼'], [false, '▲']]);

// Renders the arrow text. When clicked, the
// "onSort()" function that's passed down from
// the container.
const ListSort = ({ onSort, asc }) => (
  <Text onPress={onSort}>{arrows.get(asc)}</Text>
);

ListSort.propTypes = {
  onSort: PropTypes.func.isRequired,
  asc: PropTypes.bool.isRequired
};

export default ListSort; 

以下是生成的列表的样子:

默认情况下,整个列表按升序排列。当用户尚未提供任何内容时,您可以看到占位文本搜索。让我们看看当您输入过滤器并更改排序顺序时的效果:

此搜索包括其中包含 1 的项目,并按降序排序结果。请注意,您可以先更改顺序,也可以先输入过滤器。过滤器和排序顺序都是ListContainer状态的一部分。

获取列表数据

通常,你会从某个 API 端点获取列表数据。在本节中,你将学习如何从 React Native 组件中发出 API 请求。好消息是,fetch() API 在 React Native 中是由 React Native 进行了填充,因此你的移动应用程序中的网络代码应该看起来和感觉上很像在 Web 应用程序中一样。

首先,让我们使用返回 promise 的函数构建一个模拟 API 来处理我们的列表项,就像fetch()一样。

import fetchMock from 'fetch-mock';
import querystring from 'querystring';

// A mock item list...
const items = new Array(100).fill(null).map((v, i) => `Item ${i}`);

// The same filter and sort functionality
// as the previous example, only it's part of the
// API now, instead of part of the React component.
const filterAndSort = (data, text, asc) =>
  data
    .filter(i => text.length === 0 || i.includes(text))
    .sort(
      asc
        ? (a, b) => (b > a ? -1 : a === b ? 0 : 1)
        : (a, b) => (a > b ? -1 : a === b ? 0 : 1)
    );

export const fetchItems = (filter, asc) =>
  new Promise(resolve => {
    resolve({
      json: () =>
        Promise.resolve({
          items: filterAndSort(items, filter, asc)
        })
    });
  }); 

有了模拟 API 函数,让我们对列表容器组件进行一些更改。现在可以使用fetchItems()函数从 API 模拟中加载数据,而不是使用本地数据源:

import React, { Component } from 'react';

import { fetchItems } from './api';
import List from './List';

const mapItems = items =>
  items.map((value, i) => ({ key: i.toString(), value }));

class ListContainer extends Component {
  // The "source" state is empty because we need
  // to fetch the data from the API.
  state = {
    asc: true,
    filter: '',
    data: []
  };

  // When the component is first mounted, fetch the initial
  // items from the API, then
  componentDidMount() {
    fetchItems(this.state.filter, this.state.asc)
      .then(resp => resp.json())
      .then(({ items }) => {
        this.setState({ data: mapItems(items) });
      });
  }

  render() {
    return (
      <List
        data={this.state.data}
        asc={this.state.asc}
        onFilter={text => {
          // Makes an API call when the filter changes...
          fetchItems(text, this.state.asc)
            .then(resp => resp.json())
            .then(({ items }) =>
              this.setState({
                filter: text,
                data: mapItems(items)
              })
            );
        }}
        onSort={() => {
          // Makes an API call when the sort order changes...
          fetchItems(this.state.filter, !this.state.asc)
            .then(resp => resp.json())
            .then(({ items }) =>
              this.setState({
                asc: !this.state.asc,
                data: mapItems(items)
              })
            );
        }}
      />
    );
  }
}

export default ListContainer; 

任何修改列表状态的操作都需要调用fetchItems(),并在 promise 解析后设置适当的状态。

懒加载列表

在本节中,你将实现一种不同类型的列表,即无限滚动的列表。有时,用户实际上并不知道他们在寻找什么,因此过滤或排序是没有帮助的。想想当你登录你的 Facebook 账户时看到的新闻动态;这是应用程序的主要功能,很少有你在寻找特定的东西。你需要通过滚动列表来看看发生了什么。

要使用FlatList组件实现这一点,需要在用户滚动到列表末尾时能够获取更多的 API 数据。为了了解这是如何工作的,你需要大量的 API 数据来进行操作。生成器非常适合这个!所以让我们修改你在上一个示例中创建的模拟,使其只是不断地响应新数据:

// Items...keep'em coming!
function* genItems() {
  let cnt = 0;

  while (true) {
    yield `Item ${cnt++}`;
  }
}

const items = genItems();

export const fetchItems = () =>
  Promise.resolve({
    json: () =>
      Promise.resolve({
        items: new Array(20).fill(null).map(() => items.next().value)
      })
  }); 

有了这个,现在你可以在列表末尾到达时每次发出 API 请求获取新数据。嗯,最终当内存用尽时这将失败,但我只是想以一般的术语向你展示你可以采取的方法来在 React Native 中实现无限滚动。ListContainer组件如下所示:

import React, { Component } from 'react';

import * as api from './api';
import List from './List';

class ListContainer extends Component {
  state = {
    data: [],
    asc: true,
    filter: ''
  };

  fetchItems = () =>
    api
      .fetchItems()
      .then(resp => resp.json())
      .then(({ items }) =>
        this.setState(state => ({
          data: [...state.data, ...items.map((value, i) => ({
            key: i.toString(),
            value
          }))]
        })
      );

  // Fetches the first batch of items once the
  // component is mounted.
  componentDidMount() {
    this.fetchItems();
  }

  render() {
    return (
      <List data={this.state.data} fetchItems={this.fetchItems} />
    );
  }
}

export default ListContainer; 

每次调用fetchItems()时,响应都会与data数组连接起来。这将成为新的列表数据源,而不是像之前的示例中那样替换它。现在,让我们看看List组件如何响应到达列表末尾:

import React from 'react';
import PropTypes from 'prop-types';
import { Text, FlatList } from 'react-native';

import styles from './styles';

// Renders a "<FlatList>" component, and
// calls "fetchItems()" and the user scrolls
// to the end of the list.
const List = ({ data, fetchItems }) => (
  <FlatList
    data={data}
    renderItem={({ item }) => (
      <Text style={styles.item}>{item.value}</Text>
    )}
    onEndReached={fetchItems}
  />
);

List.propTypes = {
  data: PropTypes.array.isRequired,
  fetchItems: PropTypes.func.isRequired
};

export default List; 

如果你运行这个示例,你会发现当你滚动到屏幕底部时,列表会不断增长。

总结

在本章中,您了解了 React Native 中的FlatList组件。该组件是通用的,因为它不会对呈现的项目施加任何特定的外观。相反,列表的外观取决于您,而FlatList组件有助于高效地呈现数据源。FlatList组件还为其呈现的项目提供了可滚动的区域。

您实现了一个利用列表视图中的部分标题的示例。这是呈现静态内容(如列表控件)的好地方。然后,您了解了在 React Native 中进行网络调用;这就像在任何其他 Web 应用程序中使用fetch()一样。最后,您实现了无限滚动的懒加载列表,只有在滚动到已呈现内容的底部后才加载新项目。

在下一章中,您将学习如何显示诸如网络调用之类的进度。

测试你的知识

  1. FlatList组件可以呈现什么类型的数据?

  2. FlatList期望一个对象数组。renderItem属性接受一个负责呈现每个项目的函数。

  3. FlatList期望一个对象。

  4. 它期望一个返回可迭代对象的函数。

  5. 为什么key属性是传递给FlatList的每个数据项的要求?

  6. 这不是一个要求。

  7. 这样列表就知道如何对数据值进行排序。

  8. 这样列表就可以进行高效的相等性检查,有助于在列表数据更新期间提高渲染性能。

  9. 如何在滚动期间保持固定位置的列表控件呈现?

  10. 通过将自定义控件组件作为FlatList的子组件。

  11. 您可以使用FlatListListHeaderComponent属性。

  12. 您不能拥有静态定位的列表控件。

  13. 当用户滚动列表时,如何懒加载更多数据?

  14. 您可以为FlatListonEndReached属性提供一个函数。当用户接近列表的末尾时,将调用此函数,并且该函数可以使用更多数据填充列表数据。

  15. 您必须扩展FlatList类并响应滚动事件,以确定列表的末尾是否已经到达。

进一步阅读

点击以下链接了解更多信息:

第十七章:显示进度

本章主要讨论向用户传达进度的问题。React Native 有不同的组件来处理您想要传达的不同类型的进度。首先,您将学习为什么首先需要这样传达进度。然后,您将学习如何实现进度指示器和进度条。之后,您将看到具体的示例,向您展示如何在数据加载时使用进度指示器与导航,以及如何使用进度条来传达一系列步骤中的当前位置。

进度和可用性

想象一下,您有一台没有窗户也不发出声音的微波炉。与它互动的唯一方式是按下标有“烹饪”的按钮。尽管这个设备听起来很荒谬,但许多软件用户面临的情况就是如此——没有进度的指示。微波炉在烹饪什么?如果是的话,我们如何知道什么时候会完成?

改善微波炉情况的一种方法是添加声音。这样,用户在按下烹饪按钮后会得到反馈。您已经克服了一个障碍,但用户仍然在猜测——我的食物在哪里?在您破产之前,最好添加某种进度测量显示,比如一个计时器。

并不是 UI 程序员不理解这种可用性问题的基本原则;只是我们有事情要做,这种事情在优先级方面往往被忽略。在 React Native 中,有一些组件可以向用户提供不确定的进度反馈,也可以提供精确的进度测量。如果您想要良好的用户体验,将这些事情作为首要任务总是一个好主意。

指示进度

在本节中,您将学习如何使用ActivityIndicator组件。顾名思义,当您需要向用户指示发生了某事时,您会渲染此组件。实际进度可能是不确定的,但至少您有一种标准化的方式来显示发生了某事,尽管尚无结果可显示。

让我们创建一个示例,这样你就可以看到这个组件是什么样子的。这里是App组件:

import React from 'react';
import { View, ActivityIndicator } from 'react-native';

import styles from './styles';

// Renders an "<ActivityIndicator>" component in the
// middle of the screen. It will animate on it's own
// while displayed.
export default () => (
  <View style={styles.container}>
    <ActivityIndicator size="large" />
  </View>
); 

<ActivityIndicator>组件是跨平台的。在 iOS 上它是这样的:

它在屏幕中间渲染一个动画旋转器。这是大旋转器,如size属性中指定的那样。ActivityIndicator旋转器也可以很小,如果你将其渲染在另一个较小的元素内,这更有意义。现在让我们看看这在 Android 设备上是什么样子:

旋转器看起来不同,这是应该的,但你的应用在两个平台上传达的是同样的事情——你在等待某些东西。

这个例子只是永远旋转。别担心,接下来会有一个更现实的进度指示器示例,向你展示如何处理导航和加载 API 数据。

测量进度

指示正在取得进展的缺点是用户看不到尽头。这会导致一种不安的感觉,就像在没有定时器的微波炉中等待食物一样。当你知道已经取得了多少进展,还有多少要做时,你会感觉更好。这就是为什么尽可能使用确定性进度条总是更好的原因。

ActivityIndicator组件不同,React Native 中没有用于进度条的跨平台组件。因此,我们必须自己制作一个。我们将创建一个组件,在 iOS 上使用ProgressViewIOS,在 Android 上使用ProgressBarAndroid

首先处理跨平台问题。React Native 知道根据文件扩展名导入正确的模块。下面是ProgressBarComponent.ios.js模块的样子:

// Exports the "ProgressViewIOS" as the 
// "ProgressBarComponent" component that 
// our "ProgressBar" expects. 
export { 
  ProgressViewIOS as ProgressBarComponent, 
} from 'react-native'; 

// There are no custom properties needed. 
export const progressProps = {}; 

你直接从 React Native 中导出了ProgressViewIOS组件。你还导出了特定于平台的组件属性。在这种情况下,它是一个空对象,因为没有特定于<ProgressViewIOS>的属性。现在,让我们看看ProgressBarComponent.android.js模块:

// Exports the "ProgressBarAndroid" component as 
// "ProgressBarComponent" that our "ProgressBar" 
// expects. 
export { 
  ProgressBarAndroid as ProgressBarComponent, 
} from 'react-native'; 

// The "styleAttr" and "indeterminate" props are 
// necessary to make "ProgressBarAndroid" look like 
// "ProgressViewIOS". 
export const progressProps = { 
  styleAttr: 'Horizontal', 
  indeterminate: false, 
}; 

这个模块使用与ProgressBarComponent.ios.js模块完全相同的方法。它导出了特定于 Android 的组件以及传递给它的特定于 Android 的属性。现在,让我们构建应用程序将使用的ProgressBar组件:

import React from 'react';
import PropTypes from 'prop-types';
import { View, Text } from 'react-native';

// Imports the "ProgressBarComponent" which is the
// actual react-native implementation. The actual
// component that's imported is platform-specific.
// The custom props in "progressProps" is also
// platform-specific.
import {
  ProgressBarComponent,
  progressProps
} from './ProgressBarComponent';

import styles from './styles';

// The "ProgressLabel" component determines what to
// render as a label, based on the boolean "label"
// prop. If true, then we render some text that shows
// the progress percentage. If false, we render nothing.
const ProgressLabel = ({ show, progress }) =>
  show && (
    <Text style={styles.progressText}>
      {Math.round(progress * 100)}%
    </Text>
  );

// Our generic progress bar component...
const ProgressBar = ({ progress, label }) => (
  <View style={styles.progress}>
    <ProgressLabel show={label} progress={progress} />
    {/* "<ProgressBarComponent>" is really a ""<ProgressViewIOS>"
         or a "<ProgressBarAndroid>". */}
    <ProgressBarComponent
      {...progressProps}
      style={styles.progress}
      progress={progress}
    />
  </View>
);

ProgressBar.propTypes = {
  progress: PropTypes.number.isRequired,
  label: PropTypes.bool.isRequired
};

ProgressBar.defaultProps = {
  progress: 0,
  label: true
};

export default ProgressBar; 

让我们逐步了解这个模块中发生了什么,从导入开始。ProgressBarComponentprogressProps的值是从我们的ProgressBarComponent模块中导入的。React Native 确定从哪个模块导入这些值。

接下来,你有ProgressLabel实用组件。它根据show属性决定为进度条呈现什么标签。如果是false,则不呈现任何内容。如果是true,它会呈现一个显示进度的<Text>组件。

最后,你有ProgressBar组件本身,当我们的应用程序导入和使用。这将呈现标签和适当的进度条组件。它接受一个progress属性,这是一个介于01之间的值。现在让我们在App组件中使用这个组件:

import React, { Component } from 'react';
import { View } from 'react-native';

import styles from './styles';
import ProgressBar from './ProgressBar';

export default class MeasuringProgress extends Component {
  // Initially at 0% progress. Changing this state
  // updates the progress bar.
  state = {
    progress: 0
  };

  componentDidMount() {
    // Continuously increments the "progress" state
    // every 300MS, until we're at 100%.
    const updateProgress = () => {
      this.setState({
        progress: this.state.progress + 0.01
      });

      if (this.state.progress < 1) {
        setTimeout(updateProgress, 300);
      }
    };

    updateProgress();
  }

  render() {
    return (
      <View style={styles.container}>
        {/* This is awesome. A simple generic
             "<ProgressBar>" component that works
             on Android and on iOS. */}
        <ProgressBar progress={this.state.progress} />
      </View>
    );
  }
} 

最初,<ProgressBar>组件以 0%的进度呈现。在componentDidMount()方法中,updateProgress()函数使用定时器模拟一个真实的进程,你想要显示进度。这是 iOS 屏幕的样子:

这是相同的进度条在 Android 上的样子:

导航指示器

在本章的前面,你已经了解了ActivityIndicator组件。在本节中,你将学习在导航加载数据的应用程序中如何使用它。例如,用户从页面(屏幕)一导航到页面二。然而,页面二需要从 API 获取数据来显示给用户。因此,在进行这个网络调用时,显示进度指示器而不是一个缺乏有用信息的屏幕更有意义。

这样做实际上有点棘手,因为你必须确保屏幕所需的数据在用户每次导航到屏幕时都从 API 获取。你的目标应该是以下几点:

  • 使Navigator组件自动为即将呈现的场景获取 API 数据。

  • 使用 API 调用返回的 promise 来显示旋转器,并在 promise 解析后隐藏它。

由于你的组件可能不关心是否显示旋转器,让我们将其实现为一个通用的高阶组件:

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { View, ActivityIndicator } from 'react-native';

import styles from './styles';

// Wraps the "Wrapped" component with a stateful component
// that renders an "<ActivityIndicator>" when the "loading"
// state is true.
const loading = Wrapped =>
  class LoadingWrapper extends Component {
    static propTypes = {
      promise: PropTypes.instanceOf(Promise)
    };

    state = {
      loading: true
    };

    // Adds a callback to the "promise" that was
    // passed in. When the promise resolves, we set
    // the "loading" state to false.
    componentDidMount() {
      this.props.promise.then(
        () => this.setState({ loading: false }),
        () => this.setState({ loading: false })
      );
    }

    // If "loading" is true, render the "<ActivityIndicator>"
    // component. Otherwise, render the "<Wrapped>" component.
    render() {
      return new Map([
        [
          true,
          <View style={styles.container}>
            <ActivityIndicator size="large" />
          </View>
        ],
        [false, <Wrapped {...this.props} />]
      ]).get(this.state.loading);
    }
  };

export default loading; 

这个loading()函数接受一个组件——Wrapped参数,并返回一个LoadingWrapper组件。返回的包装器接受一个promise属性,当它解析或拒绝时,它会将loading状态更改为false。正如你在render()方法中所看到的,loading状态决定了是呈现旋转器还是Wrapped组件。

有了loading()高阶函数,让我们来看看您将与react-navigation一起使用的第一个屏幕组件:

import React from 'react';
import { View, Text } from 'react-native';

import styles from './styles';
import loading from './loading';

const First = loading(({ navigation }) => (
  <View style={styles.container}>
    <Text
      style={styles.item}
      onPress={() => navigation.navigate('Second')}
    >
      Second
    </Text>
    <Text
      style={styles.item}
      onPress={() => navigation.navigate('Third')}
    >
      Third
    </Text>
  </View>
));

export default First; 

该模块导出了一个组件,该组件使用之前创建的loading()函数进行包装。它包装了First组件,以便在promise属性挂起时显示旋转器。最后一步是在用户导航到给定页面时将该 promise 传递到组件中。这发生在App组件中的路由配置中:

import React from 'react';
import { createStackNavigator } from 'react-navigation';

import First from './First';
import Second from './Second';
import Third from './Third';

export default createStackNavigator(
  {
    First: {
      screen: props => (
        <First
          promise={new Promise(resolve => setTimeout(resolve, 1000))}
          {...props}
        />
      )
    },
    Second: {
      screen: props => (
        <Second
          promise={new Promise(resolve => setTimeout(resolve, 1000))}
          {...props}
        />
      )
    },
    Third: {
      screen: props => (
        <First
          promise={new Promise(resolve => setTimeout(resolve, 1000))}
          {...props}
        />
      )
    }
  },
  { initialRouteName: 'First' }
); 

您不是直接将屏幕组件传递给createStackNavigator()的路由配置参数,而是为每个屏幕传递一个对象。screen属性允许您提供要渲染的实际屏幕组件。在这种情况下,通过调用解析组件所需数据的 API 函数来传递promise属性。这就是loading()函数能够在等待 promise 解析时显示旋转器的方式。第一个屏幕不必担心显示加载屏幕。

步骤进度

在这个最后的例子中,您将构建一个应用程序,该应用程序显示用户在预定义步骤中的进度。例如,将表单分成几个逻辑部分,并以用户完成一个部分后移动到下一步的方式组织它们可能是有意义的。进度条对用户来说将是有用的反馈。

您将在导航栏中插入一个进度条,就在标题下方,以便用户知道他们已经走了多远,还有多远要走。您还将重用在本章中早些时候实现的ProgressBar组件。

让我们先看一下结果。这个应用程序中有四个屏幕,用户可以导航到其中。以下是第一页(场景)的样子:

标题下方的进度条反映了用户在导航中已经完成了 25%。让我们看看第三个屏幕是什么样子的:

进度已更新,以反映用户在路由堆栈中的位置。让我们来看看App组件:

import React from 'react';
import { createStackNavigator } from 'react-navigation';

import First from './First';
import Second from './Second';
import Third from './Third';
import Fourth from './Fourth';

const routes = [First, Second, Third, Fourth];

export default createStackNavigator(
  routes.reduce(
    (result, route) => ({
      ...result,
      [route.name]: route
    }),
    {}
  ),
  {
    initialRouteName: 'First',
    initialRouteParams: {
      progress: route =>
        (routes.map(r => r.name).indexOf(route) + 1) / routes.length
    }
  }
);

这个应用程序有四个屏幕。渲染每个屏幕的组件存储在routes常量中,然后使用createStackNavigator()配置堆栈导航器。创建routes数组的原因是为了让它可以被传递给初始路由(First)作为路由参数的progress()函数使用。这个函数以当前路由名称作为参数,并查找它在 routes 中的索引位置。例如,Second在数字2的位置(索引为 1 + 1),数组的长度为4。这将把进度条设置为 50%。

让我们看看First组件如何使用progress函数:

import React from 'react';
import { View, Text } from 'react-native';

import styles from './styles';
import ProgressBar from './ProgressBar';

const First = () => (
  <View style={styles.container}>
    <Text style={styles.content}>First Content</Text>
  </View>
);

First.navigationOptions = ({ navigation }) => ({
  headerTitle: (
    <View style={styles.progress}>
      <Text style={styles.title}>First</Text>
      <ProgressBar
        label={false}
        progress={navigation.state.params.progress(
          navigation.state.routeName
        )}
      />
    </View>
  ),
  headerLeft: (
    <Text
      onPress={() =>
        navigation.navigate('Fourth', navigation.state.params)
      }
    >
      Fourth
    </Text>
  ),
  headerRight: (
    <Text
      onPress={() =>
        navigation.navigate('Second', navigation.state.params)
      }
    >
      Second
    </Text>
  )
});

export default First;

该函数可以通过navigation.state.params.progress()访问。它将navigation.state.routeName的值传递给当前页面的进度值。此外,对navigation.navigate()的调用必须传递navigation.state.params,以便progress()函数对屏幕可用。如果不这样做,那么progress()将只对第一个屏幕可用,因为它是在App组件中使用initialRouteParams选项设置的。

总结

在本章中,您学习了如何向用户显示一些在幕后发生的事情。首先,我们讨论了为什么显示进度对应用程序的可用性很重要。然后,您实现了一个基本的屏幕,指示进度正在进行。然后,您实现了一个ProgressBar组件,用于测量特定的进度量。

指示器适用于不确定的进度,并且您实现了导航,显示了在网络调用挂起时显示进度指示器。在最后一节中,您实现了一个进度条,向用户显示他们在预定义步骤中的位置。

在下一章中,您将看到 React Native 地图和地理位置数据的实际应用。

测试你的知识

  1. 进度条和活动指示器有什么区别?

  2. 进度条是确定的,而进度指示器用于指示不确定的时间量。

  3. 没有区别。进度条和进度指示器实际上是相同的东西。

  4. 进度条渲染一个水平条,其他所有的都被视为进度指示器。

  5. React Native 的ActivityIndicator组件在 iOS 和 Android 上是否工作相同?

  6. 不,这个组件不是平台无关的。

  7. 是的,这个组件是平台无关的。

  8. 如何以平台不可知的方式使用ProgressViewIOSProgressBarAndroid组件?

  9. 您可以定义自己的ProgressBar组件,导入具有特定于平台的文件扩展名的其他组件。

  10. 你不能;你必须在想要使用进度条的每个地方实现平台检查逻辑。

进一步阅读

查看以下链接获取更多信息:

第十八章:地理位置和地图

在本章中,您将学习 React Native 的地理位置和地图功能。您将开始学习如何使用地理位置 API;然后您将继续使用MapView组件来标记兴趣点和区域。

您将使用react-native-maps包来实现地图。本章的目标是介绍 React Native 中用于地理位置和 React Native Maps 中地图的功能。

我在哪里?

Web 应用程序用于确定用户位置的地理位置 API 也可以被 React Native 应用程序使用,因为相同的 API 已经进行了 polyfill。除了地图之外,此 API 对于从移动设备的 GPS 获取精确坐标非常有用。然后,您可以使用这些信息向用户显示有意义的位置数据。

不幸的是,地理位置 API 返回的数据本身用处不大;您的代码必须进行一些工作,将其转换为有用的东西。例如,纬度和经度对用户来说毫无意义,但您可以使用这些数据查找对用户有用的信息。这可能只是简单地显示用户当前所在位置。

让我们实现一个示例,使用 React Native 的地理位置 API 查找坐标,然后使用这些坐标从 Google Maps API 查找可读的位置信息:

import React, { Component } from 'react';
import { Text, View } from 'react-native';
import { fromJS } from 'immutable';

import styles from './styles';

// For fetching human-readable address info.
const URL = 'https://maps.google.com/maps/api/geocode/json?latlng=';

export default class WhereAmI extends Component {
  // The "address" state is "loading..." initially because
  // it takes the longest to fetch.
  state = {
    data: fromJS({
      address: 'loading...'
    })
  };

  // Getter for "Immutable.js" state data...
  get data() {
    return this.state.data;
  }

  // Setter for "Immutable.js" state data...
  set data(data) {
    this.setState({ data });
  }

  // We don't setup any geo data till the component
  // mounts.
  componentDidMount() {
    const setPosition = pos => {
      // This component renders the "coords" data from
      // a geolocation response. This can simply be merged
      // into the state map.
      this.data = this.data.merge(pos.coords);

      // We need the "latitude" and the "longitude"
      // in order to lookup the "address" from the
      // Google maps API.
      const {
        coords: { latitude, longitude }
      } = pos;

      // Fetches data from the Google Maps API then sets
      // the "address" state based on the response.
      fetch(`${URL}${latitude},${longitude}`)
        .then(resp => resp.json(), e => console.error(e))
        .then(({ results: [{ formatted_address }] }) => {
          this.data = this.data.set('address', formatted_address);
        });
    };

    // First, we try to lookup the current position
    // data and update the component state.
    navigator.geolocation.getCurrentPosition(setPosition);

    // Then, we setup a high accuracy watcher, that
    // issues a callback whenever the position changes.
    this.watcher = navigator.geolocation.watchPosition(
      setPosition,
      err => console.error(err),
      { enableHighAccuracy: true }
    );
  }

  // It's always a good idea to make sure that this
  // "watcher" is cleared when the component is removed.
  componentWillUnmount() {
    navigator.geolocation.clearWatch(this.watcher);
  }

  render() {
    // Since we want to iterate over the properties
    // in the state map, we need to convert the map
    // to pairs using "entries()". Then we need to
    // use the spread operator to make the map iterator
    // into a plain array. The "sort()" method simply
    // sorts the map based on it's keys.
    const state = [...this.data.sortBy((v, k) => k).entries()];

    // Iterates over the state properties and renders them.
    return (
      <View style={styles.container}>
        {state.map(([k, v]) => (
          <Text key={k} style={styles.label}>
            {`${k[0].toUpperCase()}${k.slice(1)}`}: {v}
          </Text>
        ))}
      </View>
    );
  }
} 

此组件的目标是在屏幕上呈现地理位置 API 返回的属性,并查找用户的特定位置并显示它。如果您查看componentDidMount()方法,您会发现这里有大部分有趣的代码。setPosition()函数在几个地方用作回调。它的工作是设置组件的状态。

首先,它设置了coords属性。通常,您不会直接显示这些数据,但这是一个示例,展示了地理位置 API 的可用数据。其次,它使用latitudelongitude值来查找用户当前所在位置的名称,使用 Google Maps API。

setPosition()回调函数与getCurrentPosition()一起使用,当组件挂载时只调用一次。您还在watchPosition()中使用setPosition(),它会在用户位置发生变化时调用回调函数。

iOS 模拟器和 Android Studio 允许您通过菜单选项更改位置。您不必每次想要测试更改位置时都在物理设备上安装您的应用程序。

让我们看看一旦位置数据加载后,这个屏幕是什么样子的:

获取的地址信息在应用程序中可能比纬度和经度数据更有用。比物理地址文本更好的是在地图上可视化用户的物理位置;您将在下一节中学习如何做到这一点。

周围有什么?

react-native-maps中的MapView组件是您在 React Native 应用程序中渲染地图时将使用的主要工具。

让我们实现一个基本的MapView组件,看看您可以从中得到什么。

import React from 'react';
import { View } from 'react-native';
import MapView from 'react-native-maps';

import styles from './styles';

export default () => (
  <View style={styles.container}>
    <MapView
      style={styles.mapView}
      showsUserLocation
      followUserLocation
    />
  </View>
); 

您传递给MapView的两个布尔属性为您做了很多工作。showsUserLocation属性将激活地图上的标记,表示运行此应用程序的设备的物理位置。followUserLocation属性告诉地图在设备移动时更新位置标记。让我们看看结果地图:

设备的当前位置在地图上清晰标记。默认情况下,地图上也会显示兴趣点。这些是用户附近的事物,让他们可以看到周围的环境。

通常情况下,当使用showsUserLocation时最好使用followUserLocation属性。这样地图就会缩放到用户所在的区域。

注释兴趣点

到目前为止,您已经看到MapView组件如何渲染用户当前位置和用户周围的兴趣点。这里的挑战是,您可能希望显示与您的应用程序相关的兴趣点,而不是默认渲染的兴趣点。

在这一部分,您将学习如何在地图上为特定位置渲染标记,以及渲染地图上的区域。

绘制点

让我们标记一些当地的啤酒厂!以下是如何将注释传递给MapView组件:

import React from 'react';
import { View } from 'react-native';
import MapView from 'react-native-maps';

import styles from './styles';

export default () => (
  <View style={styles.container}>
    <MapView
      style={styles.mapView}
      showsPointsOfInterest={false}
      showsUserLocation
      followUserLocation
    >
      <MapView.Marker
        title="Duff Brewery"
        description="Duff beer for me, Duff beer for you"
        coordinate={{
          latitude: 43.8418728,
          longitude: -79.086082
        }}
      />
      <MapView.Marker
        title="Pawtucket Brewery"
        description="New! Patriot Light!"
        coordinate={{
          latitude: 43.8401328,
          longitude: -79.085407
        }}
      />
    </MapView>
  </View>
); 

注释就像它们听起来的那样;在基本地图地理信息的顶部呈现的额外信息。实际上,当您呈现MapView组件时,默认情况下会显示注释,因为它们会显示感兴趣的点。在这个例子中,您通过将showsPointsOfInterest属性设置为false来选择退出此功能。让我们看看这些啤酒厂的位置:

当您按下显示地图上啤酒厂位置的标记时,会显示标注。您给<MapView.Marker>titledescription属性值用于呈现此文本。

绘制叠加层

在本章的最后一节中,您将学习如何渲染区域叠加层。一个点是一个单一的纬度/经度坐标。将区域视为几个坐标的连线图。区域可以有很多用途,比如显示我们更可能找到 IPA 饮酒者与 stout 饮酒者的地方。代码如下所示:

import React, { Component } from 'react';
import { View, Text } from 'react-native';
import MapView from 'react-native-maps';
import { fromJS } from 'immutable';

import styles from './styles';

// The "IPA" region coordinates and color...
const ipaRegion = {
  coordinates: [
    { latitude: 43.8486744, longitude: -79.0695283 },
    { latitude: 43.8537168, longitude: -79.0700046 },
    { latitude: 43.8518394, longitude: -79.0725697 },
    { latitude: 43.8481651, longitude: -79.0716377 },
    { latitude: 43.8486744, longitude: -79.0695283 }
  ],
  strokeColor: 'coral',
  strokeWidth: 4
};

// The "stout" region coordinates and color...
const stoutRegion = {
  coordinates: [
    { latitude: 43.8486744, longitude: -79.0693283 },
    { latitude: 43.8517168, longitude: -79.0710046 },
    { latitude: 43.8518394, longitude: -79.0715697 },
    { latitude: 43.8491651, longitude: -79.0716377 },
    { latitude: 43.8486744, longitude: -79.0693283 }
  ],
  strokeColor: 'firebrick',
  strokeWidth: 4
};

export default class PlottingOverlays extends Component {
  // The "IPA" region is rendered first. So the "ipaStyles"
  // list has "boldText" in it, to show it as selected. The
  // "overlays" list has the "ipaRegion" in it.
  state = {
    data: fromJS({
      ipaStyles: [styles.ipaText, styles.boldText],
      stoutStyles: [styles.stoutText],
      overlays: [ipaRegion]
    })
  };

  // Getter for "Immutable.js" state data...
  get data() {
    return this.state.data;
  }

  // Setter for "Immutable.js" state data...
  set data(data) {
    this.setState({ data });
  }

  // The "IPA" text was clicked...
  onClickIpa = () => {
    this.data = this.data
      // Makes the IPA text bold...
      .update('ipaStyles', i => i.push(styles.boldText))
      // Removes the bold from the stout text...
      .update('stoutStyles', i => i.pop())
      // Replaces the stout overlay with the IPA overlay...
      .update('overlays', i => i.set(0, ipaRegion));
  };

  // The "stout" text was clicked...
  onClickStout = () => {
    this.data = this.data
      // Makes the stout text bold...
      .update('stoutStyles', i => i.push(styles.boldText))
      // Removes the bold from the IPA text...
      .update('ipaStyles', i => i.pop())
      // Replaces the IPA overlay with the stout overlay...
      .update('overlays', i => i.set(0, stoutRegion));
  };

  render() {
    const { ipaStyles, stoutStyles, overlays } = this.data.toJS();

    return (
      <View style={styles.container}>
        <View>
          {/* Text that when clicked, renders the IPA
               map overlay. */}
          <Text style={ipaStyles} onPress={this.onClickIpa}>
            IPA Fans
          </Text>

          {/* Text that when clicked, renders the stout
               map overlay. */}
          <Text style={stoutStyles} onPress={this.onClickStout}>
            Stout Fans
          </Text>
        </View>

        {/* Renders the map with the "overlays" array. There
             will only ever be a single overlay in this
             array. */}
        <MapView
          style={styles.mapView}
          showsPointsOfInterest={false}
          showsUserLocation
          followUserLocation
        >
          {overlays.map((v, i) => (
            <MapView.Polygon
              key={i}
              coordinates={v.coordinates}
              strokeColor={v.strokeColor}
              strokeWidth={v.strokeWidth}
            />
          ))}
        </MapView>
      </View>
    );
  }
} 

区域数据由几个纬度/经度坐标组成,定义了区域的形状和位置。其余的代码大部分是关于在按下两个文本链接时处理状态。默认情况下,IPA 区域被渲染:

当按下stout文本时,地图上将删除 IPA 叠加层,并添加 stout 区域:

总结

在本章中,您了解了 React Native 中的地理位置和地图。地理位置 API 的工作方式与其 Web 对应物相同。在 React Native 应用程序中使用地图的唯一可靠方式是安装第三方react-native-maps包。

您看到了基本配置MapView组件,以及它如何跟踪用户的位置,并显示相关的兴趣点。然后,您看到了如何绘制自己的兴趣点和兴趣区域。

在下一章中,您将学习如何使用类似 HTML 表单控件的 React Native 组件收集用户输入。

测试你的知识

  1. 在 React Native 中找到的地理位置 API 与 Web 浏览器中找到的地理位置 API 的工作方式相同。

  2. 是的,它是相同的 API。

  3. 不,React Native API 具有其自己独特的特性。

  4. React Native 应用程序中地理位置 API 的主要目的是什么?

  5. 计算从一个位置到另一个位置的距离。

  6. 查找设备的纬度和经度坐标,并将这些值与其他 API 一起使用,以查找有用信息,如地址。

  7. 查找地址和有关这些地址的其他信息。

  8. MapView组件能够显示用户附近的兴趣点吗?

  9. 是的,默认情况下启用了这个功能。

  10. 不,您必须手动绘制和标记所有内容。

  11. 是的,但您必须使用showsPointsOfInterest属性。

  12. 如何在地图上标记点?

  13. 通过将纬度/经度数组数据作为属性传递给MapView组件。

  14. 通过将坐标传递给MapView.Marker组件。

进一步阅读

请查看以下网址以获取更多信息:

第十九章:收集用户输入

在 Web 应用程序中,您可以从标准 HTML 表单元素中收集用户输入,这些元素在所有浏览器上看起来和行为类似。对于原生 UI 平台,收集用户输入更加微妙。

在本章中,您将学习如何使用各种 React Native 组件来收集用户输入。这些包括文本输入、从选项列表中选择、复选框和日期/时间选择器。您将看到 iOS 和 Android 之间的区别,以及如何为您的应用程序实现适当的抽象。

收集文本输入

实施文本输入时,原来有很多要考虑的事情。例如,它是否应该有占位文本?这是不应该在屏幕上显示的敏感数据吗?在用户移动到另一个字段时,您应该如何处理文本?

与传统的 Web 文本输入相比,移动文本输入的显着区别在于前者有自己内置的虚拟键盘,您可以对其进行配置和响应。让我们构建一个示例,渲染几个<TextInput>组件的实例:

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Text, TextInput, View } from 'react-native';
import { fromJS } from 'immutable';

import styles from './styles';

// A Generic "<Input>" component that we can use in our app.
// It's job is to wrap the "<TextInput>" component in a "<View>"
// so that we can render a label, and to apply styles to the
// appropriate components.
const Input = props => (
  <View style={styles.textInputContainer}>
    <Text style={styles.textInputLabel}>{props.label}</Text>
    <TextInput style={styles.textInput} {...props} />
  </View>
);

Input.propTypes = {
  label: PropTypes.string
};

export default class CollectingTextInput extends Component {
  // This state is only relevant for the "input events"
  // component. The "changedText" state is updated as
  // the user types while the "submittedText" state is
  // updated when they're done.
  state = {
    data: fromJS({
      changedText: '',
      submittedText: ''
    })
  };

  // Getter for "Immutable.js" state data...
  get data() {
    return this.state.data;
  }

  // Setter for "Immutable.js" state data...
  set data(data) {
    this.setState({ data });
  }

  render() {
    const { changedText, submittedText } = this.data.toJS();

    return (
      <View style={styles.container}>
        {/* The simplest possible text input. */}
        <Input label="Basic Text Input:" />

        {/* The "secureTextEntry" property turns
             the text entry into a password input
             field. */}
        <Input label="Password Input:" secureTextEntry />

        {/* The "returnKeyType" property changes
             the return key that's displayed on the
             virtual keyboard. In this case, we want
             a "search" button. */}
        <Input label="Return Key:" returnKeyType="search" />

        {/* The "placeholder" property works just
             like it does with web text inputs. */}
        <Input label="Placeholder Text:" placeholder="Search" />

        {/* The "onChangeText" event is triggered as
             the user enters text. The "onSubmitEditing"
             event is triggered when they click "search". */}
        <Input
          label="Input Events:"
          onChangeText={e => {
            this.data = this.data.set('changedText', e);
          }}
          onSubmitEditing={e => {
            this.data = this.data.set(
              'submittedText',
              e.nativeEvent.text
            );
          }}
          onFocus={() => {
            this.data = this.data
              .set('changedText', '')
              .set('submittedText', '');
          }}
        />

        {/* Displays the captured state from the
             "input events" text input component. */}
        <Text>Changed: {changedText}</Text>
        <Text>Submitted: {submittedText}</Text>
      </View>
    );
  }
} 

我不会深入讨论每个<TextInput>组件正在做什么 - 代码中有注释。让我们看看这些组件在屏幕上是什么样子的:

纯文本输入显示已输入的文本。密码字段不会显示任何字符。当输入为空时,占位文本会显示。还显示了更改的文本状态。您没有看到提交的文本状态,因为在我截屏之前我没有按下虚拟键盘上的提交按钮。

让我们来看看输入元素的虚拟键盘,您可以通过returnKeyType属性更改返回键文本:

当键盘返回键反映用户按下它时会发生什么时,用户会更加与应用程序保持一致。

从选项列表中进行选择

在 Web 应用程序中,通常使用<select>元素让用户从选项列表中进行选择。React Native 带有一个<Picker>组件,可以在 iOS 和 Android 上使用。根据用户所在的平台对此组件进行样式处理有一些技巧,因此让我们将所有这些隐藏在一个通用的Select组件中。这是Select.ios.js模块:

import React from 'react';
import PropTypes from 'prop-types';
import { View, Picker, Text } from 'react-native';
import styles from './styles';

// The "<Select>" component provides an
// abstraction around the "<Picker>" component.
// It actually has two outer views that are
// needed to get the styling right.
const Select = props => (
  <View style={styles.pickerHeight}>
    <View style={styles.pickerContainer}>
      {/* The label for the picker... */}
      <Text style={styles.pickerLabel}>{props.label}</Text>
      <Picker style={styles.picker} {...props}>
        {/* Maps each "items" value to a
             "<Picker.Item>" component. */}
        {props.items.map(i => <Picker.Item key={i.label} {...i} />)}
      </Picker>
    </View>
  </View>
);

Select.propTypes = {
  items: PropTypes.array,
  label: PropTypes.string
};

export default Select; 

这对于一个简单的Select组件来说有很多额外的开销。事实证明,样式化 React Native 的<Picker>组件实际上是相当困难的。以下是Select.android.js模块:

import React from 'react';
import PropTypes from 'prop-types';
import { View, Picker, Text } from 'react-native';
import styles from './styles';

// The "<Select>" component provides an
// abstraction around the "<Picker>" component.
// It actually has two outer views that are
// needed to get the styling right.
const Select = props => (
  <View>
    {/* The label for the picker... */}
    <Text style={styles.pickerLabel}>{props.label}</Text>
    <Picker {...props}>
      {/* Maps each "items" value to a
           "<Picker.Item>" component. */}
      {props.items.map(i => <Picker.Item key={i.label} {...i} />)}
    </Picker>
  </View>
);

Select.propTypes = {
  items: PropTypes.array,
  label: PropTypes.string
};

export default Select;

以下是样式的样子:

import { StyleSheet } from 'react-native'; 

export default StyleSheet.create({ 
  container: { 
    flex: 1, 
    flexDirection: 'row', 
    flexWrap: 'wrap', 
    justifyContent: 'space-around', 
    alignItems: 'center', 
    backgroundColor: 'ghostwhite', 
  }, 

  // The outtermost container, needs a height. 
  pickerHeight: { 
    height: 175, 
  }, 

  // The inner container lays out the picker 
  // components and sets the background color. 
  pickerContainer: { 
    flex: 1, 
    flexDirection: 'column', 
    alignItems: 'center', 
    marginTop: 40, 
    backgroundColor: 'white', 
    padding: 6, 
    height: 240, 
  }, 

  pickerLabel: { 
    fontSize: 14, 
    fontWeight: 'bold', 
  }, 

  picker: { 
  width: 100, 
    backgroundColor: 'white', 
  }, 

  selection: { 
    width: 200, 
    marginTop: 230, 
    textAlign: 'center', 
  }, 
}); 

现在你可以渲染你的<Select>组件:

import React, { Component } from 'react';
import { View, Text } from 'react-native';
import { fromJS } from 'immutable';

import styles from './styles';
import Select from './Select';

export default class SelectingOptions extends Component {
  // The state is a collection of "sizes" and
  // "garments". At any given time there can be
  // selected size and garment.
  state = {
    data: fromJS({
      sizes: [
        { label: '', value: null },
        { label: 'S', value: 'S' },
        { label: 'M', value: 'M' },
        { label: 'L', value: 'L' },
        { label: 'XL', value: 'XL' }
      ],
      selectedSize: null,
      garments: [
        { label: '', value: null, sizes: ['S', 'M', 'L', 'XL'] },
        { label: 'Socks', value: 1, sizes: ['S', 'L'] },
        { label: 'Shirt', value: 2, sizes: ['M', 'XL'] },
        { label: 'Pants', value: 3, sizes: ['S', 'L'] },
        { label: 'Hat', value: 4, sizes: ['M', 'XL'] }
      ],
      availableGarments: [],
      selectedGarment: null,
      selection: ''
    })
  };

  // Getter for "Immutable.js" state data...
  get data() {
    return this.state.data;
  }

  // Setter for "Immutable.js" state data...
  set data(data) {
    this.setState({ data });
  }

  render() {
    const {
      sizes,
      selectedSize,
      availableGarments,
      selectedGarment,
      selection
    } = this.data.toJS();

    // Renders two "<Select>" components. The first
    // one is a "size" selector, and this changes
    // the available garments to select from.
    // The second selector changes the "selection"
    // state to include the selected size
    // and garment.
    return (
      <View style={styles.container}>
        <Select
          label="Size"
          items={sizes}
          selectedValue={selectedSize}
          onValueChange={size => {
            this.data = this.data
              .set('selectedSize', size)
              .set('selectedGarment', null)
              .set(
                'availableGarments',
                this.data
                  .get('garments')
                  .filter(i => i.get('sizes').includes(size))
              );
          }}
        />
        <Select
          label="Garment"
          items={availableGarments}
          selectedValue={selectedGarment}
          onValueChange={garment => {
            this.data = this.data.set('selectedGarment', garment).set(
              'selection',
              this.data.get('selectedSize') +
                ' ' +
                this.data
                  .get('garments')
                  .find(i => i.get('value') === garment)
                  .get('label')
            );
          }}
        />
        <Text style={styles.selection}>{selection}</Text>
      </View>
    );
  }
} 

这个例子的基本思想是,第一个选择器中选择的选项会改变第二个选择器中的可用选项。当第二个选择器改变时,标签会显示所选的尺寸和服装。以下是屏幕的样子:

在关闭和打开之间切换

在 Web 表单中,你会看到另一个常见的元素是复选框。React Native 有一个Switch组件,可以在 iOS 和 Android 上使用。幸运的是,这个组件比Picker组件更容易样式化。以下是一个简单的抽象,你可以实现为你的开关提供标签:

import React from 'react';
import PropTypes from 'prop-types';
import { View, Text, Switch } from 'react-native';

import styles from './styles';

// A fairly straightforward wrapper component
// that adds a label to the React Native
// "<Switch>" component.
const CustomSwitch = props => (
  <View style={styles.customSwitch}>
    <Text>{props.label}</Text>
    <Switch {...props} />
  </View>
);

CustomSwitch.propTypes = {
  label: PropTypes.string
};

export default CustomSwitch; 

现在,让我们看看如何使用一对开关来控制应用程序状态:

import React, { Component } from 'react';
import { View } from 'react-native';
import { fromJS } from 'immutable';

import styles from './styles';
import Switch from './Switch';

export default class TogglingOnAndOff extends Component {
  state = {
    data: fromJS({
      first: false,
      second: false
    })
  };

  // Getter for "Immutable.js" state data...
  get data() {
    return this.state.data;
  }

  // Setter for "Immutable.js" state data...
  set data(data) {
    this.setState({ data });
  }

  render() {
    const { first, second } = this.state.data.toJS();

    return (
      <View style={styles.container}>
        {/* When this switch is turned on, the
             second switch is disabled. */}
        <Switch
          label="Disable Next Switch"
          value={first}
          disabled={second}
          onValueChange={v => {
            this.data = this.data.set('first', v);
          }}
        />

        {/* When this switch is turned on, the
             first switch is disabled. */}
        <Switch
          label="Disable Previous Switch"
          value={second}
          disabled={first}
          onValueChange={v => {
            this.data = this.data.set('second', v);
          }}
        />
      </View>
    );
  }
} 

这两个开关简单地切换彼此的disabled属性。以下是 iOS 上屏幕的样子:

以下是 Android 上相同屏幕的样子:

收集日期/时间输入

在本章的最后一节中,你将学习如何实现日期/时间选择器。React Native 为 iOS 和 Android 分别提供了独立的日期/时间选择器组件,这意味着你需要处理组件之间的跨平台差异。

所以,让我们从 iOS 的日期选择器组件开始:

import React from 'react';
import PropTypes from 'prop-types';
import { Text, View, DatePickerIOS } from 'react-native';

import styles from './styles';

// A simple abstraction that adds a label to
// the "<DatePickerIOS>" component.
const DatePicker = props => (
  <View style={styles.datePickerContainer}>
    <Text style={styles.datePickerLabel}>{props.label}</Text>
    <DatePickerIOS mode="date" {...props} />
  </View>
);

DatePicker.propTypes = {
  label: PropTypes.string
};

export default DatePicker; 

这个组件并不复杂;它只是向DatePickerIOS组件添加了一个标签。日期选择器的 Android 版本需要更多的工作。让我们看一下实现:

import React from 'react';
import PropTypes from 'prop-types';
import { Text, View, DatePickerAndroid } from 'react-native';

import styles from './styles';

// Opens the "DatePickerAndroid" dialog and handles
// the response. The "onDateChange" function is
// a callback that's passed in from the container
// component and expects a "Date" instance.
const pickDate = (options, onDateChange) => {
  DatePickerAndroid.open(options).then(date =>
    onDateChange(new Date(date.year, date.month, date.day))
  );
};

// Renders a "label" and the "date" properties.
// When the date text is clicked, the "pickDate()"
// function is used to render the Android
// date picker dialog.
const DatePicker = ({ label, date, onDateChange }) => (
  <View style={styles.datePickerContainer}>
    <Text style={styles.datePickerLabel}>{label}</Text>
    <Text onPress={() => pickDate({ date }, onDateChange)}>
      {date.toLocaleDateString()}
    </Text>
  </View>
);

DatePicker.propTypes = {
  label: PropTypes.string,
  date: PropTypes.instanceOf(Date),
  onDateChange: PropTypes.func.isRequired
};

export default DatePicker; 

两个日期选择器之间的关键区别是,Android 版本不使用 React Native 组件,比如DatePickerIOS。相反,我们必须使用命令式的DatePickerAndroid.open() API。当用户按下我们组件渲染的日期文本时,这将被触发,并打开一个日期选择器对话框。好消息是,我们的这个组件将这个 API 隐藏在一个声明性组件后面。

我还实现了一个遵循这个确切模式的时间选择器组件。因此,我建议您从github.com/PacktPublishing/React-and-React-Native-Second-Edition下载本书的代码,这样您就可以看到微妙的差异并运行示例。

现在,让我们看看如何使用我们的日期和时间选择器组件:

import React, { Component } from 'react';
import { View } from 'react-native';

import styles from './styles';

// Imports our own platform-independent "DatePicker"
// and "TimePicker" components.
import DatePicker from './DatePicker';
import TimePicker from './TimePicker';

export default class CollectingDateTimeInput extends Component {
  state = {
    date: new Date(),
    time: new Date()
  };

  render() {
    return (
      <View style={styles.container}>
        <DatePicker
          label="Pick a date, any date:"
          date={this.state.date}
          onDateChange={date => this.setState({ date })}
        />
        <TimePicker
          label="Pick a time, any time:"
          date={this.state.time}
          onTimeChange={time => this.setState({ time })}
        />
      </View>
    );
  }
} 

太棒了!现在我们有两个简单的组件,可以在 iOS 和 Android 上使用。让我们看看在 iOS 上选择器的外观:

正如您所看到的,iOS 的日期和时间选择器使用了您在本章中学到的Picker组件。Android 选择器看起来大不相同-让我们现在看看它:

总结

在本章中,您了解了各种类似于您习惯的 Web 表单元素的 React Native 组件。您首先学习了文本输入,以及每个文本输入都有自己的虚拟键盘需要考虑。接下来,您了解了Picker组件,允许用户从选项列表中选择项目。然后,您了解了Switch组件,类似于复选框。

在最后一节中,您学会了如何实现通用的日期/时间选择器,可以在 iOS 和 Android 上使用。在下一章中,您将学习有关 React Native 中模态对话框的内容。

测试您的知识

  1. 为什么要更改文本输入的虚拟键盘上的返回键?

  2. 您永远不应该更改返回键

  3. 因为在某些情况下,有意义的是有一个搜索按钮或其他更符合输入上下文的内容

  4. 您只应该更改搜索输入或密码输入的返回键

  5. 应该使用哪个TextInput属性来标记输入为密码字段?

  6. secureTextEntry

  7. password

  8. securePassword

  9. secureText

  10. 为什么要为选择元素创建抽象?

  11. 因为 iOS 和 Android 的组件完全不同

  12. 因为两个平台之间的样式挑战

  13. 您不需要创建一个抽象。

  14. 为什么要为日期和时间选择器创建抽象?

  15. 因为 iOS 和 Android 的组件完全不同

  16. 因为两个平台之间的样式挑战

  17. 您不需要创建一个抽象

进一步阅读

访问以下链接获取更多信息:

第二十章:警报、通知和确认

本章的目标是向你展示如何以不干扰当前页面的方式向用户呈现信息。页面使用View组件,并直接在屏幕上呈现。然而,有时候有重要信息需要用户看到,但你不一定希望将他们从当前页面中踢出去。

你将首先学习如何显示重要信息。了解重要信息是什么以及何时使用它,你将看到如何获得用户的确认,无论是错误还是成功的情况。然后,你将实现被动通知,向用户显示发生了某事。最后,你将实现模态视图,向用户显示后台正在发生某事。

重要信息

在你开始实施警报、通知和确认之前,让我们花几分钟时间思考一下这些项目各自的含义。我认为这很重要,因为如果你只是被动地通知用户发生了错误,很容易被忽视。以下是我对你需要显示的信息类型的定义:

  • 警报:发生了重要的事情,你需要确保用户看到发生了什么。可能用户需要确认警报。

  • 通知:发生了某事,但不重要到完全阻止用户正在做的事情。这些通常会自行消失。

确认实际上是警报的一部分。例如,如果用户刚刚执行了一个操作,然后想要确保操作成功后才继续进行,他们必须确认已经看到了信息才能关闭模态框。确认也可以存在于警报中,警告用户即将执行的操作。

关键是要尝试在信息是好知道但不是关键的情况下使用通知。只有在没有用户确认发生的情况下功能的工作流程无法继续进行时才使用确认。在接下来的章节中,你将看到警报和通知用于不同目的的示例。

获得用户确认

在本节中,您将学习如何显示模态视图以从用户那里获得确认。首先,您将学习如何实现成功的情景,其中一个操作生成了您希望用户知晓的成功结果。然后,您将学习如何实现错误情景,其中出现了问题,您不希望用户在未确认问题的情况下继续前进。

成功确认

让我们首先实现一个模态视图,作为用户成功执行操作的结果显示出来。以下是用于显示用户成功确认的Modal组件:

import React from 'react';
import PropTypes from 'prop-types';
import { View, Text, Modal } from 'react-native';

import styles from './styles';

// Uses "<Modal>" to display the underlying view
// on top of the current view. Properties passed to
// this component are also passed to the modal.
const ConfirmationModal = props => (
  <Modal {...props}>
    {/* Slightly confusing, but we need an inner and
         an outer "<View>" to style the height of the
         modal correctly. */}
    <View style={styles.modalContainer}>
      <View style={styles.modalInner}>
        {/* The confirmation message... */}
        <Text style={styles.modalText}>Dude, srsly?</Text>

        {/* The confirmation and the cancel buttons. Each
             button triggers a different callback function
             that's passed in from the container
             component. */}
        <Text
          style={styles.modalButton}
          onPress={props.onPressConfirm}
        >
          Yep
        </Text>
        <Text
          style={styles.modalButton}
          onPress={props.onPressCancel}
        >
          Nope
        </Text>
      </View>
    </View>
  </Modal>
);

ConfirmationModal.propTypes = {
  visible: PropTypes.bool.isRequired,
  onPressConfirm: PropTypes.func.isRequired,
  onPressCancel: PropTypes.func.isRequired
};

ConfirmationModal.defaultProps = {
  transparent: true,
  onRequestClose: () => {}
};

export default ConfirmationModal;

传递给ConfirmationModal的属性被转发到 React Native 的Modal组件。一会儿您就会明白为什么。首先,让我们看看这个确认模态框是什么样子的:

用户完成操作后显示的模态框具有我们自己的样式和确认消息。它还有两个操作,但根据确认是在操作前还是操作后,可能只需要一个。以下是用于此模态框的样式:

modalContainer: { 
  flex: 1, 
  justifyContent: 'center', 
  alignItems: 'center', 
}, 

modalInner: { 
  backgroundColor: 'azure', 
  padding: 20, 
  borderWidth: 1, 
  borderColor: 'lightsteelblue', 
  borderRadius: 2, 
  alignItems: 'center', 
}, 

modalText: { 
  fontSize: 16, 
  margin: 5, 
  color: 'slategrey', 
}, 

modalButton: { 
  fontWeight: 'bold', 
  margin: 5, 
  color: 'slategrey', 
}, 

使用 React Native 的Modal组件,您基本上可以自行决定您希望确认模态视图的外观。将它们视为常规视图,唯一的区别是它们是在其他视图之上渲染的。

很多时候,您可能不在意样式化自己的模态视图。例如,在 Web 浏览器中,您可以简单地调用alert()函数,它会在浏览器样式的窗口中显示文本。React Native 有类似的功能:Alert.alert()。这里的棘手之处在于这是一个命令式 API,并且您不一定希望直接在应用程序中公开它。

相反,让我们实现一个警报确认组件,隐藏这个特定的 React Native API 的细节,以便您的应用程序可以将其视为任何其他组件:

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Alert } from 'react-native';

// The "actions" Map will map the "visible"
// property to the "Alert.alert()" function,
// or to a noop function.
const actions = new Map([[true, Alert.alert], [false, () => {}]]);

class ConfirmationAlert extends Component {
  state = { visible: false, title: '', message: '', buttons: [] };

  static getDerivedStateFromProps(props) {
    return props;
  }

  render() {
    actions.get(this.state.visible)(
      this.state.title,
      this.state.message,
      this.state.buttons
    );

    return null;
  }
}

ConfirmationAlert.propTypes = {
  visible: PropTypes.bool.isRequired,
  title: PropTypes.string,
  message: PropTypes.string,
  buttons: PropTypes.array
};

export default ConfirmationAlert;

这个组件有两个重要方面。首先,看一下actions映射。它的键——truefalse——对应于visible状态值。值对应于命令式的alert()API 和一个noop函数。这是将我们所熟悉和喜爱的声明式 React 组件接口转换为隐藏视图的关键。

其次,注意render()方法不需要渲染任何东西,因为这个组件专门处理命令式的 React Native 调用。但是,对于使用ConfirmationAlert的人来说,感觉就像有东西被渲染出来了。

这是 iOS 上警报的外观:

在功能上,这里并没有真正的不同。有一个标题和下面的文本,但如果你想的话,这很容易添加到模态视图中。真正的区别在于这个模态看起来像一个 iOS 模态,而不是应用程序样式的东西。让我们看看这个警报在 Android 上是什么样子的:

这个模态看起来像一个 Android 模态,而你不需要对它进行样式设置。我认为大多数情况下,使用警报而不是模态是一个更好的选择。让它看起来像 iOS 的一部分或 Android 的一部分是有意义的。然而,有时候你需要更多地控制模态的外观,比如显示错误确认。以下是用于显示模态和警报确认对话框的代码:

import React, { Component } from 'react';
import { View, Text } from 'react-native';
import { fromJS } from 'immutable';

import styles from './styles';
import ConfirmationModal from './ConfirmationModal';
import ConfirmationAlert from './ConfirmationAlert';

export default class SuccessConfirmation extends Component {
  // The two pieces of state used to control
  // the display of the modal and the alert
  // views.
  state = {
    data: fromJS({
      modalVisible: false,
      alertVisible: false
    })
  };

  // Getter for "Immutable.js" state data...
  get data() {
    return this.state.data;
  }

  // Setter for "Immutable.js" state data...
  set data(data) {
    this.setState({ data });
  }

  // A "modal" button was pressed. So show
  // or hide the modal based on its current state.
  toggleModal = () => {
    this.data = this.data.update('modalVisible', v => !v);
  };

  // A "alert" button was pressed. So show
  // or hide the alert based on its current state.
  toggleAlert = () => {
    this.data = this.data.update('alertVisible', v => !v);
  };

  render() {
    const { modalVisible, alertVisible } = this.data.toJS();

    const { toggleModal, toggleAlert } = this;

    return (
      <View style={styles.container}>
        {/* Renders the "<ConfirmationModal>" component,
             which is hidden by default and controlled
             by the "modalVisible" state. */}
        <ConfirmationModal
          animationType="fade"
          visible={modalVisible}
          onPressConfirm={toggleModal}
          onPressCancel={toggleModal}
        />

        {/* Renders the "<ConfirmationAlert>" component,
             which doesn't actually render anything since
             it controls an imperative API under the hood.
             The "alertVisible" state controls this API. */}
        <ConfirmationAlert
          title="Are you sure?"
          message="For realz?"
          visible={alertVisible}
          buttons={[
            {
              text: 'Nope',
              onPress: toggleAlert
            },
            {
              text: 'Yep',
              onPress: toggleAlert
            }
          ]}
        />

        {/* Shows the "<ConfirmationModal>" component
             by changing the "modalVisible" state. */}
        <Text style={styles.text} onPress={toggleModal}>
          Show Confirmation Modal
        </Text>

        {/* Shows the "<ConfirmationAlert>" component
             by changing the "alertVisible" state. */}
        <Text style={styles.text} onPress={toggleAlert}>
          Show Confimation Alert
        </Text>
      </View>
    );
  }
} 

渲染模态的方法与渲染警报的方法不同。然而,它们都是根据属性值的变化而改变的声明式组件。

错误确认

在前面部分学到的所有原则在需要用户确认错误时都是适用的。如果你需要更多地控制显示,使用模态。例如,你可能希望模态是红色和令人恐惧的外观:

以下是用于创建这种外观的样式。也许你想要更加低调的东西,但重点是你可以根据自己的喜好来定制这种外观:

import { StyleSheet } from 'react-native'; 

export default StyleSheet.create({ 
  container: { 
    flex: 1, 
    justifyContent: 'center', 
    alignItems: 'center', 
    backgroundColor: 'ghostwhite', 
  }, 

  text: { 
    color: 'slategrey', 
  }, 

  modalContainer: { 
    flex: 1, 
    justifyContent: 'center', 
    alignItems: 'center', 
  }, 

  modalInner: { 
    backgroundColor: 'azure', 
    padding: 20, 
    borderWidth: 1, 
    borderColor: 'lightsteelblue', 
    borderRadius: 2, 
    alignItems: 'center', 
  }, 

  modalInnerError: { 
    backgroundColor: 'lightcoral', 
    borderColor: 'darkred', 
  }, 

  modalText: { 
    fontSize: 16, 
    margin: 5, 
    color: 'slategrey', 
  }, 

  modalTextError: { 
    fontSize: 18, 
    color: 'darkred', 
  }, 

  modalButton: { 
    fontWeight: 'bold', 
    margin: 5, 
    color: 'slategrey', 
  }, 

  modalButtonError: { 
    color: 'black', 
  }, 
}); 

你用于成功确认的相同模态样式仍然在这里。这是因为错误确认模态需要许多相同的样式。以下是如何将它们都应用到Modal组件中的方法:

import React from 'react';
import PropTypes from 'prop-types';
import { View, Text, Modal } from 'react-native';

import styles from './styles';

// Declares styles for the error modal by
// combining regular modal styles with
// error styles.
const innerViewStyle = [styles.modalInner, styles.modalInnerError];

const textStyle = [styles.modalText, styles.modalTextError];

const buttonStyle = [styles.modalButton, styles.modalButtonError];

// Just like a success modal, accept for the addition of
// error styles.
const ErrorModal = props => (
  <Modal {...props}>
    <View style={styles.modalContainer}>
      <View style={innerViewStyle}>
        <Text style={textStyle}>Epic fail!</Text>
        <Text style={buttonStyle} onPress={props.onPressConfirm}>
          Fix it
        </Text>
        <Text style={buttonStyle} onPress={props.onPressCancel}>
          Ignore it
        </Text>
      </View>
    </View>
  </Modal>
);

ErrorModal.propTypes = {
  visible: PropTypes.bool.isRequired,
  onPressConfirm: PropTypes.func.isRequired,
  onPressCancel: PropTypes.func.isRequired
};

ErrorModal.defaultProps = {
  transparent: true,
  onRequestClose: () => {}
};

export default ErrorModal; 

样式在传递给style属性之前会被组合成数组。错误样式总是最后出现的,因为冲突的样式属性,比如backgroundColor,会被数组中后面出现的样式覆盖。

除了错误确认中的样式,您可以包含任何您想要的高级控件。这取决于您的应用程序如何让用户处理错误;例如,可能有几种可以采取的行动。

然而,更常见的情况是出了问题,你无能为力,除了确保用户意识到情况。在这些情况下,您可能只需显示一个警报:

被动通知

到目前为止,在本章中您所检查的通知都需要用户输入。这是有意设计的,因为这是您强制用户查看的重要信息。然而,您不希望过度使用这一点。对于重要但如果被忽略不会改变生活的通知,您可以使用被动通知。这些通知以比模态框更不显眼的方式显示,并且不需要任何用户操作来解除。

在本节中,您将创建一个Notification组件,该组件使用 Android 的 Toast API,并为 iOS 创建一个自定义模态框。它被称为 Toast API,因为显示的信息看起来像是弹出的一片吐司。以下是 Android 组件的样子:

import React from 'react';
import PropTypes from 'prop-types';
import { ToastAndroid } from 'react-native';
import { Map } from 'immutable';

// Toast helper. Always returns "null" so that the
// output can be rendered as a React element.
const show = (message, duration) => {
  ToastAndroid.show(message, duration);
  return null;
};

// This component will always return null,
// since it's using an imperative React Native
// interface to display popup text. If the
// "message" property was provided, then
// we display a message.
const Notification = ({ message, duration }) =>
  Map([[null, null], [undefined, null]]).get(
    message,
    show(message, duration)
  );

Notification.propTypes = {
  message: PropTypes.string,
  duration: PropTypes.number.isRequired
};

Notification.defaultProps = {
  duration: ToastAndroid.LONG
};

export default Notification;

再次,您正在处理一个命令式的 React Native API,您不希望将其暴露给应用程序的其他部分。相反,这个组件将命令式的ToastAndroid.show()函数隐藏在一个声明性的 React 组件后面。无论如何,这个组件都会返回null,因为它实际上不会渲染任何内容。以下是ToastAndroid通知的样子:

发生了某事的通知显示在屏幕底部,并在短暂延迟后移除。关键是通知不会打扰。

iOS 通知组件涉及更多,因为它需要状态和生命周期事件,使模态视图的行为类似于瞬态通知。以下是代码:

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { View, Modal, Text } from 'react-native';
import { Map } from 'immutable';

import styles from './styles';

class Notification extends Component {
  static propTypes = {
    message: PropTypes.string,
    duration: PropTypes.number.isRequired
  };

  static defaultProps = {
    duration: 1500
  };

  static getDerivedStateFromProps(props) {
    // Update the "visible" state, based on whether
    // or not there's a "message" value.
    return {
      ...this.state,
      visible: Map([[null, false], [undefined, false]]).get(
        props.message,
        true
      )
    };
  }

  // The modal component is either "visible", or not.
  // The "timer" is used to hide the notification
  // after some predetermined amount of time.
  state = { visible: false };
  timer = null;

  componentWillUnmount() {
    clearTimeout(this.timer);
  }

  render() {
    const modalProps = {
      animationType: 'fade',
      transparent: true,
      visible: this.state.visible
    };

    this.timer = Map([
      [null, () => null],
      [undefined, () => null]
    ]).get(this.props.message, () =>
      setTimeout(
        () => this.setState({ visible: false }),
        this.props.duration
      )
    )();

    return (
      <Modal {...modalProps}>
        <View style={styles.notificationContainer}>
          <View style={styles.notificationInner}>
            <Text>{this.props.message}</Text>
          </View>
        </View>
      </Modal>
    );
  }
}

Notification.propTypes = {
  message: PropTypes.string,
  duration: PropTypes.number.isRequired
};

Notification.defaultProps = {
  duration: 1500
};

export default Notification; 

您必须设计模态框以显示通知文本,以及用于在延迟后隐藏通知的状态。以下是 iOS 的最终结果:

ToastAndroid API 相同的原则适用于这里。您可能已经注意到,除了显示通知按钮之外,还有另一个按钮。这是一个简单的计数器,重新渲染视图。实际上,演示这个看似晦涩的功能是有原因的,您马上就会看到。这是主应用视图的代码:

import React, { Component } from 'react';
import { Text, View } from 'react-native';
import { fromJS } from 'immutable';

import styles from './styles';
import Notification from './Notification';

export default class PassiveNotifications extends Component {
  // The initial state is the number of times
  // the counter button has been clicked, and
  // the notification message.
  state = {
    data: fromJS({
      count: 0,
      message: null
    })
  };

  // Getter for "Immutable.js" state data...
  get data() {
    return this.state.data;
  }

  // Setter for "Immutable.js" state data...
  set data(data) {
    this.setState({ data });
  }

  render() {
    const { count, message } = this.data.toJS();

    return (
      <View style={styles.container}>
        {/* The "Notification" component is
             only displayed if the "message" state
             has something in it. */}
        <Notification message={message} />

        {/* Updates the count. Also needs to make
             sure that the "message" state is null,
             even if the message has been hidden
             already. */}
        <Text
          onPress={() => {
            this.data = this.data
              .update('count', c => c + 1)
              .set('message', null);
          }}
        >
          Pressed {count}
        </Text>

        {/* Displays the notification by
             setting the "message" state. */}
        <Text
          onPress={() => {
            this.data = this.data.set(
              'message',
              'Something happened!'
            );
          }}
        >
          Show Notification
        </Text>
      </View>
    );
  }
}

按下计数器的整个目的是要证明,即使Notification组件是声明性的,并在状态改变时接受新的属性值,当改变其他状态值时,仍然必须将消息状态设置为 null。原因是,如果重新渲染组件并且消息状态仍然包含字符串,它将一遍又一遍地显示相同的通知。

活动模态

在本章的最后一节中,您将实现一个显示进度指示器的模态。想法是显示模态,然后在 promise 解析时隐藏它。以下是显示带有活动指示器的模态的通用Activity组件的代码:

import React from 'react';
import PropTypes from 'prop-types';
import { View, Modal, ActivityIndicator } from 'react-native';

import styles from './styles';

// The "Activity" component will only display
// if the "visible" property is try. The modal
// content is an "<ActivityIndicator>" component.
const Activity = props => (
  <Modal visible={props.visible} transparent>
    <View style={styles.modalContainer}>
      <ActivityIndicator size={props.size} />
    </View>
  </Modal>
);

Activity.propTypes = {
  visible: PropTypes.bool.isRequired,
  size: PropTypes.string.isRequired
};

Activity.defaultProps = {
  visible: false,
  size: 'large'
};

export default Activity; 

您可能会想要将 promise 传递给组件,以便在 promise 解析时自动隐藏自己。我认为这不是一个好主意,因为这样你就必须将状态引入到这个组件中。此外,它将依赖于 promise 才能正常工作。通过您实现这个组件的方式,您可以仅基于visible属性来显示或隐藏模态。这是 iOS 上活动模态的样子:

模态上有一个半透明的背景,覆盖在带有获取内容...链接的主视图上。以下是在styles.js中创建此效果的方法:

modalContainer: { 
  flex: 1, 
  justifyContent: 'center', 
  alignItems: 'center', 
  backgroundColor: 'rgba(0, 0, 0, 0.2)', 
}, 

与其将实际的Modal组件设置为透明,不如在backgroundColor中设置透明度,这样看起来就像是一个覆盖层。现在,让我们来看看控制这个组件的代码:

import React, { Component } from 'react';
import { Text, View } from 'react-native';
import { fromJS } from 'immutable';

import styles from './styles';
import Activity from './Activity';

export default class ActivityModals extends Component {
  // The state is a "fetching" boolean value,
  // and a "promise" that is used to determine
  // when the fetching is done.
  state = {
    data: fromJS({
      fetching: false,
      promise: Promise.resolve()
    })
  };

  // Getter for "Immutable.js" state data...
  get data() {
    return this.state.data;
  }

  // Setter for "Immutable.js" state data...
  set data(data) {
    this.setState({ data });
  }

  // When the fetch button is pressed, the
  // promise that simulates async activity
  // is set, along with the "fetching" state.
  // When the promise resolves, the "fetching"
  // state goes back to false, hiding the modal.
  onPress = () => {
    this.data = this.data.merge({
      promise: new Promise(resolve => setTimeout(resolve, 3000)).then(
        () => {
          this.data = this.data.set('fetching', false);
        }
      ),
      fetching: true
    });
  };

  render() {
    return (
      <View style={styles.container}>
        {/* The "<Activity>" modal is only visible
             when the "fetching" state is true. */}
        <Activity visible={this.data.get('fetching')} />
        <Text onPress={this.onPress}>Fetch Stuff...</Text>
      </View>
    );
  }
} 

当按下获取链接时,将创建一个模拟异步网络活动的新 promise。然后,当 promise 解析时,将fetching状态更改回 false,以便隐藏活动对话框。

摘要

在本章中,您了解到向移动用户显示重要信息的必要性。有时,这需要用户的明确反馈,即使只是对消息的确认。在其他情况下,被动通知效果更好,因为它们比确认模态更不显眼。

有两种工具可用于向用户显示消息:模态和警报。模态更灵活,因为它们就像常规视图一样。警报适用于显示纯文本,并且它们会为您处理样式问题。在 Android 上,您还有额外的 ToastAndroid 接口。您看到这在 iOS 上也是可能的,但这需要更多的工作。

在下一章中,我们将深入研究 React Native 中的手势响应系统,这比浏览器能提供更好的移动体验。

测试你的知识

  1. 警报和模态之间有什么区别?

  2. 警报用于不重要的信息,而模态用于不太重要的信息。

  3. 它们用途相同,使用哪一个都无所谓。

  4. 警报很擅长继承移动环境的外观和感觉,而模态框是常规的 React Native 视图,您可以完全控制其样式。

  5. 哪个 React Native 组件可用于创建覆盖屏幕上其他组件的模态视图?

  6. 没有办法做到这一点。

  7. Modal 组件。

  8. Modal.open() 函数用于此目的。

  9. 在 Android 系统上显示被动通知的最佳方法是什么?

  10. React Native 有一个通知 API 用于此目的。

  11. 您可以使用 ToastAndroid React Native API。在 iOS 上,没有不涉及自己编写代码的好的替代方法。

  12. React Native 仅支持 iOS 上的被动通知。

  13. React Native 警报 API 仅在 iOS 上可用。

进一步阅读

查看以下链接以获取更多信息:

第二十一章:响应用户手势

到目前为止,您在本书中实现的所有示例都依赖于用户手势。在传统的 Web 应用程序中,您主要处理鼠标事件。然而,触摸屏依赖用户用手指操作元素,这与鼠标完全不同。

本章的目标是向您展示 React Native 内部手势响应系统的工作原理,以及通过组件公开该系统的一些方式。

首先,您将学习有关滚动的内容。除了触摸之外,这可能是最常见的手势。然后,您将学习在用户与您的组件交互时提供适当级别的反馈。最后,您将实现可以被滑动的组件。

用手指滚动

在 Web 应用程序中,通过使用鼠标指针来拖动滚动条来进行滚动,或者通过旋转鼠标滚轮来进行滚动。这在移动设备上不起作用,因为没有鼠标。一切都由屏幕上的手势控制。例如,如果您想向下滚动,您可以使用拇指或食指在屏幕上移动手指来将内容向上拉。

像这样滚动是很难实现的,但它变得更加复杂。当您在移动屏幕上滚动时,会考虑拖动动作的速度。您快速拖动屏幕,然后松开,屏幕将根据您移动的速度继续滚动。在此过程中,您也可以触摸屏幕以阻止其滚动。

幸运的是,您不必处理大部分这些内容。ScrollView组件为您处理了大部分滚动复杂性。实际上,在第十六章渲染项目列表中,您已经使用了ScrollView组件。ListView组件内置了ScrollView

您可以通过实现手势生命周期方法来突破用户交互的低级部分。您可能永远不需要这样做,但如果您感兴趣,可以在facebook.github.io/react-native/releases/next/docs/gesture-responder-system.html上阅读相关内容。

您可以在ListView之外使用ScrollView。例如,如果您只是渲染文本和其他小部件等任意内容,而不是列表,您可以将其包装在<ScrollView>中。以下是一个示例:

import React from 'react';
import {
  Text,
  ScrollView,
  ActivityIndicator,
  Switch,
  View
} from 'react-native';

import styles from './styles';

export default () => (
  <View style={styles.container}>
    {/* The "<ScrollView>" can wrap any
         other component to make it scrollable.
         Here, we're repeating an arbitrary group
         of components to create some scrollable
         content */}
    <ScrollView style={styles.scroll}>
      {new Array(6).fill(null).map((v, i) => (
        <View key={i}>
          {/* Abitrary "<Text>" component... */}
          <Text style={[styles.scrollItem, styles.text]}>
            Some text
          </Text>

          {/* Arbitrary "<ActivityIndicator>"... */}
          <ActivityIndicator style={styles.scrollItem} size="large" />

          {/* Arbitrary "<Switch>" component... */}
          <Switch style={styles.scrollItem} />
        </View>
      ))}
    </ScrollView>
  </View>
); 

ScrollView组件本身并没有太多用处——它用于包装其他组件。它需要一个高度才能正确地发挥作用。以下是滚动样式的外观:

scroll: { 
  height: 1, 
  alignSelf: 'stretch', 
}, 

height设置为1,但alignSelfstretch值允许项目正确显示。以下是最终结果的外观:

当您拖动内容时,屏幕右侧会出现垂直滚动条。如果运行此示例,您可以尝试进行各种手势,例如使内容自动滚动,然后停止。

提供触摸反馈

到目前为止,在本书中您已经使用了纯文本来充当按钮或链接的 React Native 示例。在 Web 应用程序中,要使文本看起来像可以点击的东西,只需用适当的链接包装它。移动设备上没有类似的东西,因此您可以将文本样式化为按钮。

尝试在移动设备上将文本样式化为链接的问题在于它们太难按。按钮为手指提供了更大的目标,并且更容易应用触摸反馈。

让我们将一些文本样式化为按钮。这是一个很好的第一步,使文本看起来可以点击。但是当用户开始与按钮交互时,您还希望给予视觉反馈。React Native 提供了两个组件来帮助实现这一点:TouchableOpacityTouchableHighlight。但在深入代码之前,让我们先看一下这些组件在用户与它们交互时的外观,首先是TouchableOpacity

这里渲染了两个按钮,顶部的按钮标有“Opacity”当前正在被用户按下。当按下时,按钮的不透明度会变暗,这为用户提供了重要的视觉反馈。让我们看看当按下时TouchableHighlight按钮的外观,如下所示:

当按下时,TouchableHighlight组件不会改变不透明度,而是在按钮上添加一个高亮层。在这种情况下,它使用了 slate gray 的更透明的版本来进行高亮显示,slate gray 是字体和边框颜色中使用的颜色。

您使用哪种方法并不重要。重要的是,您为用户提供适当的触摸反馈,以便他们与按钮进行交互。实际上,您可能希望在同一个应用程序中使用两种方法,但用于不同的事物。让我们创建一个Button组件,这样可以轻松使用任一方法:

import React from 'react';
import PropTypes from 'prop-types';
import {
  Text,
  TouchableOpacity,
  TouchableHighlight
} from 'react-native';

import styles from './styles';

// The "touchables" map is used to get the right
// component to wrap around the button. The
// "undefined" key represents the default.
const touchables = new Map([
  ['opacity', TouchableOpacity],
  ['highlight', TouchableHighlight],
  [undefined, TouchableOpacity]
]);

const Button = ({ label, onPress, touchable }) => {
  // Get's the "Touchable" component to use,
  // based on the "touchable" property value.
  const Touchable = touchables.get(touchable);

  // Properties to pass to the "Touchable"
  // component.
  const touchableProps = {
    style: styles.button,
    underlayColor: 'rgba(112,128,144,0.3)',
    onPress
  };

  // Renders the "<Text>" component that's
  // styled to look like a button, and is
  // wrapped in a "<Touchable>" component
  // to properly handle user interactions.
  return (
    <Touchable {...touchableProps}>
      <Text style={styles.buttonText}> {label} </Text>
    </Touchable>
  );
};

Button.propTypes = {
  onPress: PropTypes.func.isRequired,
  label: PropTypes.string.isRequired,
  touchable: PropTypes.oneOf(['opacity', 'highlight'])
};

export default Button; 

touchables映射用于确定基于touchable属性值的哪个 React Native 可触摸组件包装文本。以下是用于创建此按钮的样式:

button: { 
  padding: 10, 
  margin: 5, 
  backgroundColor: 'azure', 
  borderWidth: 1, 
  borderRadius: 4, 
  borderColor: 'slategrey', 
}, 

buttonText: { 
  color: 'slategrey', 
} 

以下是如何在主应用程序模块中使用这些按钮:

import React from 'react';
import { View } from 'react-native';

import styles from './styles';
import Button from './Button';

export default () => (
  <View style={styles.container}>
    {/* Renders a "<Button>" that uses
         "TouchableOpacity" to handle user
         gestures, since that is the default */}
    <Button onPress={() => {}} label="Opacity" />

    {/* Renders a "<Button>" that uses
         "TouchableHighlight" to handle
         user gestures. */}
    <Button
      onPress={() => {}}
      label="Highlight"
      touchable="highlight"
    />
  </View>
); 

请注意,onPress回调实际上并不执行任何操作,我们传递它们是因为它们是必需的属性。

可滑动和可取消

使原生移动应用程序比移动 Web 应用程序更易于使用的部分原因是它们感觉更直观。使用手势,您可以快速掌握事物的工作原理。例如,用手指在屏幕上滑动元素是一种常见的手势,但手势必须是可发现的。

假设您正在使用一个应用程序,并且不确定屏幕上的某些内容是做什么的。因此,您用手指按下并尝试拖动元素。它开始移动。不确定会发生什么,您松开手指,元素又回到原位。您刚刚发现了这个应用程序的一部分是如何工作的。

您将使用Scrollable组件来实现可滑动和可取消的行为。您可以创建一个相对通用的组件,允许用户将文本从屏幕上滑走,并在发生这种情况时调用回调函数。让我们先看看呈现滑动组件的代码,然后再看通用组件本身:

import React, { Component } from 'react';
import { View } from 'react-native';
import { fromJS } from 'immutable';

import styles from './styles';
import Swipeable from './Swipeable';

export default class SwipableAndCancellable extends Component {
  // The initial state is an immutable list of
  // 8 swipable items.
  state = {
    data: fromJS(
      new Array(8)
        .fill(null)
        .map((v, id) => ({ id, name: 'Swipe Me' }))
    )
  };

  // Getter for "Immutable.js" state data...
  get data() {
    return this.state.data;
  }

  // Setter for "Immutable.js" state data...
  set data(data) {
    this.setState({ data });
  }

  // The swipe handler passed to "<Swipeable>".
  // The swiped item is removed from the state.
  // This is a higher-order function that returns
  // the real handler so that the "id" context
  // can be set.
  onSwipe = id => () => {
    this.data = this.data.filterNot(v => v.get('id') === id);
  };

  render() {
    return (
      <View style={styles.container}>
        {this.data
          .toJS()
          .map(i => (
            <Swipeable
              key={i.id}
              onSwipe={this.onSwipe(i.id)}
              name={i.name}
            />
          ))}
      </View>
    );
  }
} 

这将在屏幕上呈现八个<Swipeable>组件。让我们看看这是什么样子:

现在,如果您开始向左滑动其中一个项目,它将移动。这是它的样子:

如果您没有滑动足够远,手势将被取消,并且项目将按预期移回原位。如果您将其完全滑动,项目将从列表中完全移除,并且屏幕上的项目将填充空白空间,就像这样:

现在让我们来看看Swipeable组件本身:

import React from 'react';
import PropTypes from 'prop-types';
import {
  View,
  ScrollView,
  Text,
  TouchableOpacity
} from 'react-native';

import styles from './styles';

// The "onScroll" handler. This is actually
// a higher-order function that returns the
// actual handler. When the x offset is 200,
// when know that the component has been
// swiped and can call "onSwipe()".
const onScroll = onSwipe => e =>
  e.nativeEvent.contentOffset.x === 200 && onSwipe();

// The static properties used by the "<ScrollView>"
// component.
const scrollProps = {
  horizontal: true,
  pagingEnabled: true,
  showsHorizontalScrollIndicator: false,
  scrollEventThrottle: 10
};

const Swipeable = ({ onSwipe, name }) => (
  <View style={styles.swipeContainer}>
    {/* The "<View>" that wraps this "<ScrollView>"
         is necessary to make scrolling work properly. */}
    <ScrollView {...scrollProps} onScroll={onScroll(onSwipe)}>
      {/* Not strictly necessary, but "<TouchableOpacity>"
           does provide the user with meaningful feedback
           when they initially press down on the text. */}
      <TouchableOpacity>
        <View style={styles.swipeItem}>
          <Text style={styles.swipeItemText}>{name}</Text>
        </View>
      </TouchableOpacity>
      <View style={styles.swipeBlank} />
    </ScrollView>
  </View>
);

Swipeable.propTypes = {
  onSwipe: PropTypes.func.isRequired,
  name: PropTypes.string.isRequired
};

export default Swipeable; 

请注意,<ScrollView>组件被设置为水平,并且pagingEnabled为 true。分页行为会将组件捕捉到位,并提供可取消的行为。这就是为什么在文本组件旁边有一个空白组件的原因。以下是用于此组件的样式:

swipeContainer: { 
  flex: 1, 
  flexDirection: 'row', 
  width: 200, 
  height: 30, 
  marginTop: 50, 
}, 

swipeItem: { 
  width: 200, 
  height: 30, 
  backgroundColor: 'azure', 
  justifyContent: 'center', 
  borderWidth: 1, 
  borderRadius: 4, 
  borderColor: 'slategrey', 
}, 

swipeItemText: { 
  textAlign: 'center', 
  color: 'slategrey', 
}, 

swipeBlank: { 
  width: 200, 
  height: 30, 
}, 

swipeBlank样式与swipeItem具有相同的尺寸,但没有其他内容。它是不可见的。

总结

在本章中,我们介绍了在原生平台上的手势与移动 web 平台相比的差异。我们首先看了ScrollView组件,以及它通过为包装组件提供原生滚动行为而使生活变得更加简单。

接下来,我们花了一些时间实现带有触摸反馈的按钮。这是另一个在移动 web 上很难做到的领域。你学会了如何使用TouchableOpacityTouchableHighlight组件。

最后,你实现了一个通用的Swipeable组件。滑动是一种常见的移动模式,它允许用户在不感到害怕的情况下发现事物是如何工作的。在下一章中,你将学习如何使用 React Native 来控制图像显示。

测试你的知识

  1. web 应用程序和本地移动应用程序之间的用户交互的主要区别是什么?

  2. 在 web 和移动应用中,用户交互没有明显的区别。

  3. 移动应用程序本质上比其 web 等效版本更快,因此您的代码需要考虑到这一点。

  4. 没有鼠标。相反,用户使用手指与您的 UI 进行交互。这是一种与使用鼠标完全不同的体验,需要进行适应。

  5. 你如何在 React Native 中为用户提供触摸反馈?

  6. 通过将View组件传递给feedback属性。

  7. 通过用TouchableOpacityTouchableHighlight组件包装可触摸组件。

  8. 你必须在onPress处理程序中手动调整视图的样式。

  9. 移动应用中的滚动为什么比 web 应用中的滚动复杂得多?

  10. 在移动 web 应用中滚动需要考虑诸如速度之类的因素,因为用户是用手指进行交互。否则,交互会感到不自然。

  11. 在复杂性上没有真正的区别。

  12. 只有当你把它复杂化时,它才会变得复杂。触摸交互可以被实现成与鼠标交互完全相同的行为。

  13. 为什么要使用 ScrollView 组件来实现可滑动的行为?

  14. 因为这是 Web 应用程序中用户习惯的方式。

  15. 因为这是移动 Web 应用程序中用户习惯的方式,以及他们学习 UI 控件的方式。

  16. 你不应该实现可滑动的行为。

进一步阅读

查看以下链接以获取更多信息:

第二十二章:控制图像显示

到目前为止,本书中的示例在移动屏幕上还没有渲染任何图像。这并不反映移动应用程序的现实情况。Web 应用程序显示大量图像。如果说什么,原生移动应用程序比 Web 应用程序更依赖图像,因为图像是在有限空间下的强大工具。

在本章中,您将学习如何使用 React Native 的Image组件,从不同来源加载图像。然后,您将看到如何使用Image组件调整图像大小,以及如何为懒加载的图像设置占位符。最后,您将学习如何使用react-native-vector-icons包实现图标。

加载图像

让我们开始解决如何加载图像的问题。您可以渲染<Image>组件并像任何其他 React 组件一样传递属性。但是这个特定的组件需要图像 blob 数据才能发挥作用。让我们看一些代码:

import React from 'react';
import PropTypes from 'prop-types';
import { View, Image } from 'react-native';

import styles from './styles';

// Renders two "<Image>" components, passing the
// properties of this component to the "source"
// property of each image.
const LoadingImages = ({ reactSource, relaySource }) => (
  <View style={styles.container}>
    <Image style={styles.image} source={reactSource} />
    <Image style={styles.image} source={relaySource} />
  </View>
);

// The "source" property can be either
// an object with a "uri" string, or a number
// represending a local "require()" resource.
const sourceProp = PropTypes.oneOfType([
  PropTypes.shape({
    uri: PropTypes.string.isRequired
  }),
  PropTypes.number
]).isRequired;

LoadingImages.propTypes = {
  reactSource: sourceProp,
  relaySource: sourceProp
};

LoadingImages.defaultProps = {
  // The "reactSource" image comes from a remote
  // location.
  reactSource: {
    uri:
      'https://facebook.github.io/react-native/docs/assets/favicon.png'
  },

  // The "relaySource" image comes from a local
  // source.
  relaySource: require('./images/relay.png')
};

export default LoadingImages;

有两种方法可以将 blob 数据加载到<Image>组件中。第一种方法是从网络加载图像数据。通过将带有uri属性的对象传递给source来实现。在这个例子中的第二个<Image>组件是使用本地图像文件,通过调用require()并将结果传递给source

看一下sourceProp属性类型验证器。这让您了解可以传递给source属性的内容。它要么是一个带有uri字符串属性的对象,要么是一个数字。它期望一个数字,因为require()返回一个数字。

现在,让我们看看渲染结果如下:

这是与这些图像一起使用的样式:

image: { 
  width: 100, 
  height: 100, 
  margin: 20, 
}, 

请注意,如果没有widthheight样式属性,图像将不会渲染。在下一节中,您将学习在设置widthheight值时图像调整大小的工作原理。

调整图像大小

Image组件的widthheight样式属性决定了在屏幕上渲染的大小。例如,您可能会在某个时候需要处理分辨率比您在 React Native 应用程序中想要显示的更大的图像。只需在Image上设置widthheight样式属性就足以正确缩放图像。

让我们看一些代码,让您可以使用控件动态调整图像的尺寸,如下所示:

import React, { Component } from 'react';
import { View, Text, Image, Slider } from 'react-native';
import { fromJS } from 'immutable';

import styles from './styles';

export default class ResizingImages extends Component {
  // The initial state of this component includes
  // a local image source, and the width/height
  // image dimensions.
  state = {
    data: fromJS({
      source: require('./images/flux.png'),
      width: 100,
      height: 100
    })
  };

  // Getter for "Immutable.js" state data...
  get data() {
    return this.state.data;
  }

  // Setter for "Immutable.js" state data...
  set data(data) {
    this.setState({ data });
  }

  render() {
    // The state values we need...
    const { source, width, height } = this.data.toJS();

    return (
      <View style={styles.container}>
        {/* The image is rendered using the
             "source", "width", and "height"
             state values. */}
        <Image source={source} style={{ width, height }} />
        {/* The current "width" and "height"
             values are displayed. */}
        <Text>Width: {width}</Text>
        <Text>Height: {height}</Text>
        {/* This slider scales the image size
             up or down by changing the "width"
             and "height" states. */}
        <Slider
          style={styles.slider}
          minimumValue={50}
          maximumValue={150}
          value={width}
          onValueChange={v => {
            this.data = this.data.merge({
              width: v,
              height: v
            });
          }}
        />
      </View>
    );
  }
} 

如果您使用默认的 100 x 100 尺寸,图像的外观如下:

这是图像的缩小版本:

最后,这是图像的放大版本:

Image组件可以传递resizeMode属性。这确定了缩放图像如何适应实际组件的尺寸。您将在本章的最后一节中看到此属性的作用。

延迟加载图像

有时,您不一定希望图像在渲染时立即加载。例如,您可能正在渲染尚未在屏幕上可见的内容。大多数情况下,从网络获取图像源在实际可见之前是完全可以的。但是,如果您正在微调应用程序并发现通过网络加载大量图像会导致性能问题,您可以懒惰地加载源。

我认为在移动环境中更常见的用例是处理渲染一个或多个图像的情况,其中它们是可见的,但网络响应速度很慢。在这种情况下,您可能希望渲染一个占位图像,以便用户立即看到一些东西,而不是空白空间。

要做到这一点,您可以实现一个包装实际图像的抽象,一旦加载完成,您就可以显示它。以下是代码:

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { View, Image } from 'react-native';

// The local placeholder image source.
const placeholder = require('./images/placeholder.png');

// The mapping to the "loaded" state that gets us
// the appropriate image component.
const Placeholder = props =>
  new Map([
    [true, null],
    [false, <Image {...props} source={placeholder} />]
  ]).get(props.loaded);

class LazyImage extends Component {
  // The "width" and "height" properties
  // are required. All other properties are
  // forwarded to the actual "<Image>"
  // component.
  static propTypes = {
    style: PropTypes.shape({
      width: PropTypes.number.isRequired,
      height: PropTypes.number.isRequired
    })
  };

  constructor() {
    super();

    // We assume that the source hasn't finished
    // loading yet.
    this.state = {
      loaded: false
    };
  }

  render() {
    // The props and state this component
    // needs in order to render...
    const {
      props: {
        style: { width, height }
      },
      state: { loaded }
    } = this;

    return (
      <View style={{ width, height }}>
        {/* The placeholder image is just a standard
             "<Image>" component with a predefined
             source. It isn't rendered if "loaded" is
             true. */}
        <Placeholder loaded={loaded} {...this.props} />
        {/* The actual image is forwarded props that
             are passed to "<LazyImage>". The "onLoad"
             handler ensures the "loaded" state is true,
             removing the placeholder image. */}
        <Image
          {...this.props}
          onLoad={() =>
            this.setState({
              loaded: true
            })
          }
        />
      </View>
    );
  }
}

export default LazyImage; 

此组件呈现一个带有两个Image组件的View。它还具有一个loaded状态,最初为 false。当loaded为 false 时,将呈现占位图像。当调用“onLoad()”处理程序时,loaded状态设置为 true。这意味着占位图像被移除,主图像被显示。

现在让我们使用您刚刚实现的LazyImage组件。您将渲染没有源的图像,并且应该显示占位图像。让我们添加一个按钮,为懒惰图像提供源,当它加载时,占位图像应该被替换。主应用程序模块的外观如下:

import React, { Component } from 'react';
import { View } from 'react-native';

import styles from './styles';
import LazyImage from './LazyImage';
import Button from './Button';

// The remote image to load...
const remote =
  'https://facebook.github.io/react-native/docs/assets/favicon.png';

export default class LazyLoading extends Component {
  state = {
    source: null
  };

  render() {
    return (
      <View style={styles.container}>
        {/* Renders the lazy image. Since there's
             no "source" value initially, the placeholder
             image will be rendered. */}
        <LazyImage
          style={{ width: 200, height: 100 }}
          resizeMode="contain"
          source={this.state.source}
        />
        {/* When pressed, this button changes the
             "source" of the lazy image. When the new
             source loads, the placeholder image is
             replaced. */}
        <Button
          label="Load Remote"
          onPress={() =>
            this.setState({
              source: { uri: remote }
            })
          }
        />
      </View>
    );
  }
} 

这是屏幕最初的样子:

然后,如果单击“加载远程”按钮,最终将看到我们实际想要的图像:

你可能会注意到,根据你的网络速度,占位图片在你点击加载远程按钮后仍然可见。这是有意设计的,因为你不希望在确保实际图片准备好显示之前移除占位图片。

渲染图标

在本章的最后一节中,你将学习如何在 React Native 组件中渲染图标。使用图标来表示含义使 web 应用更易用。那么,原生移动应用为什么要有所不同呢?

你会想要使用react-native-vector-icons包将各种矢量字体包引入到你的 React Native 项目中:

npm install --save @expo/vector-icons

现在你可以导入Icon组件并渲染它们。让我们实现一个示例,根据选择的图标类别渲染几个FontAwesome图标:

import React, { Component } from 'react';
import { View, Picker, FlatList, Text } from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
import { fromJS } from 'immutable';

import styles from './styles';
import iconNames from './icon-names.json';

export default class RenderingIcons extends Component {
  // The initial state consists of the "selected"
  // category, the "icons" JSON object, and the
  // "listSource" used to render the list view.
  state = {
    data: fromJS({
      selected: 'Web Application Icons',
      icons: iconNames,
      listSource: []
    })
  };

  // Getter for "Immutable.js" state data...
  get data() {
    return this.state.data;
  }

  // Setter for "Immutable.js" state data...
  set data(data) {
    this.setState({ data });
  }

  // Sets the "listSource" state based on the
  // "selected" icon state. Also sets the "selected"
  // state.
  updateListSource = selected => {
    this.data = this.data
      .update('listSource', listSource =>
        this.data.getIn(['icons', selected])
      )
      .set('selected', selected);
  };

  // Make sure the "listSource" is populated
  // before the first render.
  componentDidMount() {
    this.updateListSource(this.data.get('selected'));
  }

  render() {
    const { updateListSource } = this;

    // Get the state that we need to render the icon
    // category picker and the list view with icons.
    const selected = this.data.get('selected');
    const categories = this.data
      .get('icons')
      .keySeq()
      .toJS();
    const listSource = this.data.get('listSource');

    return (
      <View style={styles.container}>
        <View style={styles.picker}>
          {/* Lets the user select a FontAwesome icon
               category. When the selection is changed,
               the list view is changed. */}
          <Picker
            selectedValue={selected}
            onValueChange={updateListSource}
          >
            {categories.map(c => (
              <Picker.Item key={c} label={c} value={c} />
            ))}
          </Picker>
        </View>
        <FlatList
          style={styles.icons}
          data={listSource
            .map((value, key) => ({ key: key.toString(), value }))
            .toJS()}
          renderItem={({ item }) => (
            <View style={styles.item}>
              {/* The "<Icon>" component is used
                   to render the FontAwesome icon */}
              <Icon name={item.value} style={styles.itemIcon} />
              {/* Shows the icon class used */}
              <Text style={styles.itemText}>{item.value}</Text>
            </View>
          )}
        />
      </View>
    );
  }
} 

当你运行示例时,你应该看到类似以下的东西:

每个图标的颜色都是以与文本颜色相同的方式指定的,通过样式。

总结

在本章中,你学会了如何在 React Native 应用中处理图片。在原生移动应用中,图片和在 web 上下文中一样重要——它们提高了用户体验。

你学会了加载图片的不同方法,然后如何调整它们的大小。你还学会了如何实现一个懒加载图片,使用占位图片来显示,直到实际图片加载完成。最后,你学会了如何在 React Native 应用中使用图标。

在下一章中,你将学习关于 React Native 中的本地存储,这在你的应用离线时非常方便。

检验你的知识

  1. Image组件的source属性接受什么类型的值?

  2. Image组件接受本地文件的路径。

  3. Image组件接受远程图片 URL 的路径。

  4. Image组件接受本地文件和远程图片 URL 的路径。

  5. 在图片加载时,你应该使用什么作为占位符?

  6. 你应该使用一个在图片使用的上下文中有意义的占位图片。

  7. 你应该为屏幕上尚未加载的任何图片使用ActivityIndicator组件。

  8. Image组件会自动为你处理占位符。

  9. 你如何使用Image组件来缩放图片?

  10. 你必须确保Image组件中只使用缩放后的图片。

  11. 通过设置widthheight属性,Image组件将自动处理图像的缩放。

  12. 在移动应用程序中缩放图像会消耗大量 CPU,并且应该避免。

  13. 值得为您的应用程序安装react-native-vector-icons包吗?

  14. 是的,这个包可以为您的应用程序提供数千个图标,并且图标是向用户传达意图的重要工具。

  15. 不,这会增加很多额外开销,并且图标在移动应用程序中并不有用。

进一步阅读

查看以下链接以获取更多信息:

第二十三章:离线操作

用户期望应用程序在网络连接不稳定的情况下能够无缝运行。如果您的移动应用程序无法应对瞬时网络问题,那么用户将使用其他应用程序。当没有网络时,您必须在设备上将数据持久保存在本地。或者,也许您的应用程序甚至不需要网络访问,即使是这种情况,您仍然需要在本地存储数据。

在本章中,您将学习如何使用 React Native 执行以下三件事。首先,您将学习如何检测网络连接状态。其次,您将学习如何在本地存储数据。最后,您将学习如何在网络问题导致数据存储后,一旦网络恢复,同步本地数据。

检测网络状态

如果您的代码在断开连接时尝试通过fetch()进行网络请求,将会发生错误。您可能已经为这些情况设置了错误处理代码,因为服务器可能返回其他类型的错误。然而,在连接问题的情况下,您可能希望在用户尝试进行网络请求之前检测到此问题。

主动检测网络状态有两个潜在原因。您可能会向用户显示友好的消息,指出网络已断开,他们无法做任何事情。然后,您将阻止用户执行任何网络请求,直到检测到网络已恢复。早期检测网络状态的另一个可能好处是,您可以准备在离线状态下执行操作,并在网络重新连接时同步应用程序状态。

让我们看一些使用NetInfo实用程序来处理网络状态变化的代码:

import React, { Component } from 'react';
import { Text, View, NetInfo } from 'react-native';
import { fromJS } from 'immutable';

import styles from './styles';

// Maps the state returned from "NetInfo" to
// a string that we want to display in the UI.
const connectedMap = {
  none: 'Disconnected',
  unknown: 'Disconnected',
  wifi: 'Connected',
  cell: 'Connected',
  mobile: 'Connected'
};

export default class NetworkState extends Component {
  // The "connected" state is a simple
  // string that stores the state of the
  // network.
  state = {
    data: fromJS({
      connected: ''
    })
  };

  // Getter for "Immutable.js" state data...
  get data() {
    return this.state.data;
  }

  // Setter for "Immutable.js" state data...
  set data(data) {
    this.setState({ data });
  }

  // When the network state changes, use the
  // "connectedMap" to find the string to display.
  onNetworkChange = connection => {
    this.data = this.data.set(
      'connected',
      connectedMap[connection.type]
    );
  };

  // When the component is mounted, we add a listener
  // that changes the "connected" state when the
  // network state changes.
  componentDidMount() {
    NetInfo.addEventListener(
      'connectionChange',
      this.onNetworkChange
    );
  }

  // Make sure the listener is removed...
  componentWillUnmount() {
    NetInfo.removeEventListener(
      'connectionChange',
      this.onNetworkChange
    );
  }

  // Simply renders the "connected" state as
  // it changes.
  render() {
    return (
      <View style={styles.container}>
        <Text>{this.data.get('connected')}</Text>
      </View>
    );
  }
} 

该组件将根据connectedMap中的字符串值呈现网络状态。NetInfo对象的connectionChange事件将导致connected状态发生变化。例如,当您首次运行此应用程序时,屏幕可能如下所示:

然后,如果您在主机机器上关闭网络,模拟设备上的网络状态也会发生变化,导致我们应用程序的状态如下所示:

存储应用程序数据

AsyncStorage API 在 iOS 和 Android 平台上的工作方式相同。您可以在不需要任何网络连接的应用程序中使用此 API,或者存储数据,一旦网络可用,就会使用 API 端点最终进行同步。

让我们看一些代码,允许用户输入键和值,然后存储它们:

import React, { Component } from 'react';
import {
  Text,
  TextInput,
  View,
  FlatList,
  AsyncStorage
} from 'react-native';
import { fromJS } from 'immutable';

import styles from './styles';
import Button from './Button';

export default class StoringData extends Component {
  // The initial state of this component
  // consists of the current "key" and "value"
  // that the user is entering. It also has
  // a "source" for the list view to display
  // everything that's been stored.
  state = {
    data: fromJS({
      key: null,
      value: null,
      source: []
    })
  };

  // Getter for "Immutable.js" state data...
  get data() {
    return this.state.data;
  }

  // Setter for "Immutable.js" state data...
  set data(data) {
    this.setState({ data });
  }

  // Uses "AsyncStorage.setItem()" to store
  // the current "key" and "value" states.
  // When this completes, we can delete
  // "key" and "value" and reload the item list.
  setItem = () =>
    AsyncStorage.setItem(this.data.get('key'), this.data.get('value'))
      .then(() => {
        this.data = this.data.delete('key').delete('value');
      })
      .then(() => this.loadItems());

  // Uses "AsyncStorage.clear()" to empty any stored
  // values. Then, it loads the empty list of
  // items to clear the item list on the screen.
  clearItems = () =>
    AsyncStorage.clear().then(() => this.loadItems());

  // This method is async because awaits on the
  // data store keys and values, which are two
  // dependent async calls.
  async loadItems() {
    const keys = await AsyncStorage.getAllKeys();
    const values = await AsyncStorage.multiGet(keys);

    this.data = this.data.set('source', fromJS(values));
  }

  // Load any existing items that have
  // already been stored when the app starts.
  componentDidMount() {
    this.loadItems();
  }

  render() {
    // The state that we need...
    const { source, key, value } = this.data.toJS();

    return (
      <View style={styles.container}>
        <Text>Key:</Text>
        <TextInput
          style={styles.input}
          value={key}
          onChangeText={v => {
            this.data = this.data.set('key', v);
          }}
        />
        <Text>Value:</Text>
        <TextInput
          style={styles.input}
          value={value}
          onChangeText={v => {
            this.data = this.data.set('value', v);
          }}
        />
        <View style={styles.controls}>
          <Button label="Add" onPress={this.setItem} />
          <Button label="Clear" onPress={this.clearItems} />
        </View>
        <View style={styles.list}>
          <FlatList
            data={source.map(([key, value]) => ({
              key: key.toString(),
              value
            }))}
            renderItem={({ item: { value, key } }) => (
              <Text>
                {value} ({key})
              </Text>
            )}
          />
        </View>
      </View>
    );
  }
} 

在我解释这段代码在做什么之前,让我们先看一下以下屏幕,因为它将提供您所需的大部分解释:

如您所见,有两个输入字段和两个按钮。字段允许用户输入新的键和值。添加按钮允许用户在其设备上本地存储此键值对,而清除按钮则清除先前存储的任何现有项目。

AsyncStorage API 在 iOS 和 Android 上的工作方式相同。在底层,AsyncStorage的工作方式取决于它正在运行的平台。React Native 能够在两个平台上公开相同的存储 API 的原因是因为它的简单性——它只是键值对。比这更复杂的任何操作都留给应用程序开发人员。

在这个示例中,您围绕AsyncStorage创建的抽象很少。想法是设置和获取项目。然而,即使是这样简单的操作也值得一个抽象层。例如,您在这里实现的setItem()方法将进行异步调用到AsyncStorage并在完成后更新items状态。加载项目更加复杂,因为您需要将键和值作为两个单独的异步操作获取。

原因是保持 UI 的响应性。如果在将数据写入磁盘时需要进行待处理的屏幕重绘,通过阻止它们发生来阻止会导致用户体验不佳。

同步应用程序数据

到目前为止,在本章中,您已经学会了如何检测网络连接的状态,以及如何在 React Native 应用程序中本地存储数据。现在是时候结合这两个概念,并实现一个可以检测网络中断并继续运行的应用程序。

基本思想是只有在确定设备在线时才发出网络请求。如果知道设备不在线,可以在本地存储任何状态更改。然后,当您再次在线时,可以将这些存储的更改与远程 API 同步。

让我们实现一个简化的 React Native 应用程序来实现这一点。第一步是实现一个抽象层,位于 React 组件和存储数据的网络调用之间。我们将称这个模块为store.js

import { NetInfo, AsyncStorage } from 'react-native';
import { Map as ImmutableMap } from 'immutable';

// Mock data that would otherwise come from a real
// networked API endpoint.
const fakeNetworkData = {
  first: false,
  second: false,
  third: false
};

// We'll assume that the device isn't "connected"
// by default.
let connected = false;

// There's nothing to sync yet...
const unsynced = [];

// Sets the given "key" and "value". The idea
// is that application that uses this function
// shouldn't care if the network is connected
// or not.
export const set = (key, value) =>
  // The returned promise resolves to true
  // if the network is connected, false otherwise.
  new Promise((resolve, reject) => {
    if (connected) {
      // We're online - make the proper request (or fake
      // it in this case) and resolve the promise.
      fakeNetworkData[key] = value;
      resolve(true);
    } else {
      // We're offline - save the item using "AsyncStorage"
      // and add the key to "unsynced" so that we remember
      // to sync it when we're back online.
      AsyncStorage.setItem(key, value.toString()).then(
        () => {
          unsynced.push(key);
          resolve(false);
        },
        err => reject(err)
      );
    }
  });

// Gets the given key/value. The idea is that the application
// shouldn't care whether or not there is a network connection.
// If we're offline and the item hasn't been synced, read it
// from local storage.
export const get = key =>
  new Promise((resolve, reject) => {
    if (connected) {
      // We're online. Resolve the requested data.
      resolve(key ? fakeNetworkData[key] : fakeNetworkData);
    } else if (key) {
      // We've offline and they're asking for a specific key.
      // We need to look it up using "AsyncStorage".
      AsyncStorage.getItem(key).then(
        item => resolve(item),
        err => reject(err)
      );
    } else {
      // We're offline and they're asking for all values.
      // So we grab all keys, then all values, then we
      // resolve a plain JS object.
      AsyncStorage.getAllKeys().then(
        keys =>
          AsyncStorage.multiGet(keys).then(
            items => resolve(ImmutableMap(items).toJS()),
            err => reject(err)
          ),
        err => reject(err)
      );
    }
  });

// Check the network state when the module first
// loads so that we have an accurate value for "connected".
NetInfo.getConnectionInfo().then(
  connection => {
    connected = ['wifi', 'unknown'].includes(connection.type);
  },
  () => {
    connected = false;
  }
);

// Register a handler for when the state of the network changes.
NetInfo.addEventListener('connectionChange', connection => {
  // Update the "connected" state...
  connected = ['wifi', 'unknown'].includes(connection.type);

  // If we're online and there's unsynced values,
  // load them from the store, and call "set()"
  // on each of them.
  if (connected && unsynced.length) {
    AsyncStorage.multiGet(unsynced).then(items => {
      items.forEach(([key, val]) => set(key, val));
      unsynced.length = 0;
    });
  }
}); 

该模块导出了两个函数——set()get()。它们的工作分别是设置和获取数据。由于这只是演示如何在本地存储和网络端点之间同步的示例,因此该模块只是用fakeNetworkData对象模拟了实际网络。

让我们先看看set()函数。这是一个异步函数,它总是返回一个解析为布尔值的 promise。如果为 true,则表示您在线,并且网络调用成功。如果为 false,则表示您离线,并且使用AsyncStorage保存了数据。

get()函数也采用了相同的方法。它返回一个解析布尔值的 promise,指示网络的状态。如果提供了一个键参数,那么将查找该键的值。否则,将返回所有值,无论是从网络还是从AsyncStorage中。

除了这两个函数之外,该模块还做了另外两件事。它使用NetInfo.getConnectionInfo()来设置connected状态。然后,它添加了一个监听器以侦听网络状态的变化。这就是当您离线时本地保存的项目在再次连接时与网络同步的方式。

现在让我们看一下使用这些函数的主要应用程序:

import React, { Component } from 'react';
import { Text, View, Switch, NetInfo } from 'react-native';
import { fromJS } from 'immutable';

import styles from './styles';
import { set, get } from './store';

// Used to provide consistent boolean values
// for actual booleans and their string representations.
const boolMap = {
  true: true,
  false: false
};

export default class SynchronizingData extends Component {
  // The message state is used to indicate that
  // the user has gone offline. The other state
  // items are things that the user wants to change
  // and sync.
  state = {
    data: fromJS({
      message: null,
      first: false,
      second: false,
      third: false
    })
  };

  // Getter for "Immutable.js" state data...
  get data() {
    return this.state.data;
  }

  // Setter for "Immutable.js" state data...
  set data(data) {
    this.setState({ data });
  }

  // Generates a handler function bound to a given key.
  save = key => value => {
    // Calls "set()" and depending on the resolved value,
    // sets the user message.
    set(key, value).then(
      connected => {
        this.data = this.data
          .set('message', connected ? null : 'Saved Offline')
          .set(key, value);
      },
      err => {
        this.data = this.data.set('message', err);
      }
    );
  };

  componentDidMount() {
    // We have to call "NetInfo.fetch()" before
    // calling "get()" to ensure that the
    // connection state is accurate. This will
    // get the initial state of each item.
    NetInfo.getConnectionInfo().then(() =>
      get().then(
        items => {
          this.data = this.data.merge(items);
        },
        err => {
          this.data = this.data.set('message', err);
        }
      )
    );
  }

  render() {
    // Bound methods...
    const { save } = this;

    // State...
    const { message, first, second, third } = this.data.toJS();

    return (
      <View style={styles.container}>
        <Text>{message}</Text>
        <View>
          <Text>First</Text>
          <Switch
            value={boolMap[first.toString()]}
            onValueChange={save('first')}
          />
        </View>
        <View>
          <Text>Second</Text>
          <Switch
            value={boolMap[second.toString()]}
            onValueChange={save('second')}
          />
        </View>
        <View>
          <Text>Third</Text>
          <Switch
            value={boolMap[third.toString()]}
            onValueChange={save('third')}
          />
        </View>
      </View>
    );
  }
} 

App组件的工作是保存三个复选框的状态,当您为用户提供无缝的在线和离线模式切换时,这是困难的。幸运的是,您在另一个模块中实现的set()get()抽象层隐藏了大部分细节,使应用功能更加简单。

然而,您会注意到,在尝试加载任何项目之前,您需要在此模块中检查网络状态。如果您不这样做,那么get()函数将假定您处于离线状态,即使连接正常。应用程序的外观如下:

请注意,直到您在 UI 中更改了某些内容,您才会实际看到“已保存离线”消息。

总结

本章介绍了在 React Native 应用程序中离线存储数据。您希望在设备离线并且您的应用无法与远程 API 通信时,才需要将数据存储在本地。然而,并非所有应用程序都需要 API 调用,AsyncStorage可以用作通用存储机制。您只需要围绕它实现适当的抽象。

您还学会了如何在 React Native 应用程序中检测网络状态的变化。了解设备何时离线很重要,这样您的存储层就不会进行无谓的网络调用尝试。相反,您可以让用户知道设备处于离线状态,然后在连接可用时同步应用程序状态。

这就结束了本书的第二部分。您已经了解了如何为 Web 构建 React 组件,以及为移动平台构建 React 组件。在本书的开头,我提出了 React 之美在于渲染目标的概念。React 的声明式编程接口永远不需要更改。将 JSX 元素转换的底层机制是完全可替换的 - 理论上,您可以将 React 渲染到任何地方。

在本书的最后部分,我将讨论 React 应用程序中的状态。状态和管理其在应用程序中流动的策略可以决定 React 架构的成败。

测试您的知识

  1. 为什么AsyncStorage API 中的操作是异步的?

  2. 这样您可以同时执行大量存储操作。

  3. 为了不干扰 UI 的响应性。

  4. 它们不是异步操作,它们只是返回承诺,以保持与其他存储 API 的一致性。

  5. 您将使用哪个AsyncStorage API 来一次查找多个项目?

  6. AsyncStorage.getAll()

  7. AsyncStorage.filter()

  8. AsyncStorage.getAllKeys()AsyncStorage.multiGet()的组合。

  9. 在 React Native 应用程序中如何获取设备的连接状态?

  10. 您调用NetInfo.getConnectionInfo()并读取结果连接类型。

  11. 您调用NetInfo.getConnectionInfo(),如果返回 true,则表示已连接。否则,您处于离线状态。

  12. 有一个全局的reactNativeConnectionInfo对象,您可以随时从中读取以确定连接的状态。

  13. 在 React Native 应用程序中如何响应连接状态的变化?

  14. 无法响应连接状态的更改。

  15. 您可以通过调用NetInfo.addEventListener('connectionChange', ...)来监听connectionChange事件。

  16. 您可以为NetInfo.onChange() API 提供回调函数。

进一步阅读

访问以下链接以获取更多信息:

第二十四章:处理应用程序状态

在本书的早期,你一直在使用状态来控制你的 React 组件。状态是任何 React 应用程序中的重要概念,因为它控制用户可以看到和交互的内容。没有状态,你只有一堆空的 React 组件。

在本章中,你将学习 Flux 以及它如何作为信息架构的基础。然后,你将学习如何构建最适合 Web 和移动架构的架构。你还将介绍 Redux 库,然后讨论 React 架构的局限性以及如何克服它们。

信息架构和 Flux

将用户界面视为信息架构可能很难。更常见的是,你对 UI 应该如何看起来和行为有一个大致的想法,然后你实现它。我一直这样做,这是一个很好的方法,可以让事情开始进行,及早发现你的方法存在的问题等等。但是然后我喜欢退一步,想象没有任何小部件时会发生什么。不可避免的是,我构建的东西在状态通过各种组件流动方面存在缺陷。这没关系;至少现在我有东西可以使用。我只需要确保在构建太多之前解决信息架构的问题。

Flux 是 Facebook 创建的一组模式,它帮助开发人员以与其应用程序自然契合的方式思考他们的信息架构。接下来我将介绍 Flux 的关键概念,这样你就可以将这些想法应用到统一的 React 架构中。

单向性

在本书的前面,我介绍了 React 组件的容器模式。容器组件具有状态,但实际上不会呈现任何 UI 元素。相反,它呈现其他 React 组件并将其状态作为属性传递。每当容器状态更改时,子组件都会使用新的属性值重新呈现。这是单向数据流。

Flux 采纳了这个想法,并将其应用于称为存储的东西。存储是一个抽象概念,它保存应用程序状态。就我而言,React 容器是一个完全有效的 Flux 存储。我一会儿会详细介绍存储。首先,我希望你理解为什么单向数据流是有利的。

您很可能已经实现了一个改变状态的 UI 组件,但并不总是确定它是如何发生的。它是另一个组件中的某个事件的结果吗?是某个网络调用完成的副作用吗?当发生这种情况时,您会花费大量时间追踪更新的来源。结果往往是一个连续的麻烦游戏。当改变只能来自一个方向时,您可以排除许多其他可能性,从而使整体架构更可预测。

同步更新轮次

当您改变 React 容器的状态时,它将重新渲染其子组件,子组件将重新渲染它们的子组件,依此类推。在 Flux 术语中,这称为更新轮次。从状态改变到 UI 元素反映这一变化的时间,这就是轮次的边界。能够将应用程序行为的动态部分分组成更大的块是很好的,因为这样更容易理解因果关系。

React 容器组件的一个潜在问题是它们可以交织在一起并以非确定性的顺序进行渲染。例如,如果某个 API 调用完成并导致在另一个更新轮次中的渲染完成之前发生状态更新,会发生什么?如果不认真对待,异步性的副作用会累积并演变成不可持续的架构。

Flux 架构中的解决方案是强制同步更新轮次,并将试图规避更新轮次顺序的尝试视为错误。JavaScript 是一个单线程的、运行至完成的环境,应该通过与之合作而不是对抗来接受它。先更新整个 UI,然后再次更新整个 UI。事实证明,React 是这项工作的一个非常好的工具。

可预测的状态转换

在 Flux 架构中,您有一个用于保存应用程序状态的存储。您知道,当状态发生变化时,它是同步和单向的,使整个系统更可预测且更易于理解。然而,还有一件事可以做,以确保不会引入副作用。

你将所有应用程序状态都保存在一个存储中,这很好,但你仍然可以通过在其他地方改变数据来破坏一切。这些变化乍看起来可能无害,但对你的架构来说是有害的。例如,处理fetch()调用的回调函数可能在将数据传递给存储之前对数据进行操作。事件处理程序可能生成一些结构并将其传递给存储。可能性是无限的。

在存储之外执行这些状态转换的问题在于你并不一定知道它们正在发生。将数据变异看作蝴蝶效应:一个小的改变会产生不明显的深远影响。解决方案是只在存储中变异状态,没有例外。这样做是可预测的,可以轻松追踪你的 React 架构的因果关系。

我一直在本书的大部分示例中使用Immutable.js来管理状态。当你考虑 Flux 架构中的状态转换时,这将会很有用。控制状态转换发生的位置很重要,但状态的不可变性也很重要。它有助于强化 Flux 架构的理念,当我们深入了解 Redux 时,你将更深入地了解这些理念。

统一的信息架构

让我们回顾一下到目前为止我们应用程序架构的要素:

  • React Web:在 Web 浏览器中运行的应用程序

  • React Native:在移动平台上本地运行的应用程序

  • Flux:可扩展数据在 React 应用程序中的模式

记住,React 只是一个位于渲染目标之上的抽象。两个主要的渲染目标是浏览器和移动原生应用。这个列表可能会不断增长,所以你需要设计你的架构,以便不排除未来的可能性。挑战在于你不是将一个 Web 应用程序移植到原生移动应用程序;它们是不同的应用程序,但它们有相同的目的。

话虽如此,是否有一种方式可以基于 Flux 的思想仍然拥有某种统一的信息架构,可以被这些不同的应用使用?我能想到的最好答案,不幸的是,是:有点。你不希望让不同的网页和移动用户体验导致在处理状态上采取截然不同的方法。如果应用的目标是相同的,那么必须有一些共同的信息可以使用,使用相同的 Flux 概念。

困难的部分在于网页和原生移动应用是不同的体验,这意味着你的应用状态的形式会有所不同。它必须是不同的;否则,你只是在不同平台之间移植,这违背了使用 React Native 来利用浏览器中不存在的功能的初衷。

实现 Redux

你将使用一个叫做 Redux 的库来实现一个演示 Flux 架构的基本应用。Redux 并不严格遵循 Flux 所设定的模式。相反,它借鉴了 Flux 的关键思想,并实现了一个小的 API,使得实现 Flux 变得容易。

应用本身将是一个新闻阅读器,一个你可能从未听说过的时髦读者。这是一个简单的应用,但我想要在实现过程中突出架构上的挑战。即使是简单的应用,在关注数据时也会变得复杂。

你将实现这个应用的两个版本。你将从网页版本开始,然后实现移动——iOS 和 Android 的原生应用。你将看到如何在应用之间共享架构概念。当你需要在多个平台上实现相同的应用时,这降低了概念上的负担。你现在正在实现两个应用,但随着 React 扩展其渲染能力,将来可能会有更多。

我再次敦促你从github.com/PacktPublishing/React-and-React-Native-Second-Edition下载本书的代码示例。这本书中有很多我无法在书中覆盖的细节,尤其是对于我们即将看到的这些示例应用。

初始应用状态

让我们首先看一下 Flux 存储的初始状态。在 Redux 中,应用的整个状态由一个单一的存储表示。它看起来是这样的:

import { fromJS } from 'immutable';

// The state of the application is contained
// within an Immutable.js Map. Each key represents
// a "slice" of state.
export default fromJS({
  // The "App" state is the generic state that's
  // always visible. This state is not specific to
  // one particular feature, in other words. It has
  // the app title, and links to various article
  // sections.
  App: {
    title: 'Neckbeard News',
    links: [
      { name: 'All', url: '/' },
      { name: 'Local', url: '/local' },
      { name: 'Global', url: '/global' },
      { name: 'Tech', url: '/tech' },
      { name: 'Sports', url: '/sports' }
    ]
  },

  // The "Home" state is where lists of articles are
  // rendered. Initially, there are no articles, so
  // the "articles" list is empty until they're fetched
  // from the API.
  Home: {
    articles: []
  },

  // The "Article" state represents the full article. The
  // assumption is that the user has navigated to a full
  // article page and we need the entire article text here.
  Article: {
    full: ''
  }
}); 

该模块导出一个Immutable.js Map实例。稍后您会明白原因。但现在,让我们看看这个状态的组织。在 Redux 中,您通过切片来划分应用程序状态。在这种情况下,这是一个简单的应用程序,因此存储只有三个状态切片。每个状态切片都映射到一个主要的应用程序功能。

例如,Home键表示应用程序的Home组件使用的状态。初始化任何状态都很重要,即使它是一个空对象或数组,这样您的组件就有了初始属性。现在让我们使用一些 Redux 函数来创建一个用于向您的 React 组件获取数据的存储。

创建存储

初始状态在应用程序首次启动时很有用。这足以呈现组件,但仅此而已。一旦用户开始与 UI 交互,您需要一种改变存储状态的方法。在 Redux 中,您为存储中的每个状态切片分配一个减速器函数。因此,例如,您的应用程序将有一个Home减速器,一个App减速器和一个Article减速器。

Redux 中减速器的关键概念是它是纯净的,没有副作用。这就是在状态中使用Immutable.js结构有用的地方。让我们看看如何将初始状态与最终改变我们存储状态的减速器函数联系起来:

import { createStore } from 'redux';
import { combineReducers } from 'redux-immutable';

// So build a Redux store, we need the "initialState"
// and all of our reducer functions that return
// new state.
import initialState from './initialState';
import App from './App';
import Home from './Home';
import Article from './Article';

// The "createStore()" and "combineReducers()" functions
// perform all of the heavy-lifting.
export default createStore(
  combineReducers({
    App,
    Home,
    Article
  }),
  initialState
); 

AppHomeArticle函数的命名方式与它们操作的状态片段完全相同。随着应用程序的增长,这使得添加新的状态和减速器函数变得更容易。

现在您有一个准备就绪的 Redux 存储。但您仍然没有将其连接到实际呈现状态的 React 组件。现在让我们看看如何做到这一点。

存储提供程序和路由

Redux 有一个Provider组件(技术上,它是react-redux包提供的),用于包装应用程序的顶级组件。这将确保 Redux 存储数据对应用程序中的每个组件都可用。

在您正在开发的潮流新闻阅读器应用中,您将使用Provider组件将Router组件包装起来。然后,在构建组件时,您知道存储数据将可用。以下是Root组件的外观:

import React from 'react';
import { Provider } from 'react-redux';

import store from '../store';
import App from './App';

export default () => (
  <Provider store={store}>
    <App />
  </Provider>
);

通过将初始状态与减速器函数组合来创建的存储器被传递给<Provider>。这意味着,当你的减速器导致 Redux 存储器改变时,存储器数据会自动传递给每个应用程序组件。接下来我们将看一下App组件。

App 组件

App组件包括页面标题和各种文章分类的链接列表。当用户在用户界面中移动时,App组件总是被渲染,但每个<Route>元素根据当前路由渲染不同的内容。让我们来看一下这个组件,然后我们将分解它的工作原理:

import React from 'react';
import {
  BrowserRouter as Router,
  Route,
  NavLink
} from 'react-router-dom';
import { connect } from 'react-redux';

// Components that render application state.
import Home from './Home';
import Article from './Article';

// Higher order component for making the
// various article section components out of
// the "Home" component. The only difference
// is the "filter" property. Having unique JSX
// element names is easier to read than a bunch
// of different property values.
const articleList = filter => props => (
  <Home {...props} filter={filter} />
);

const categoryListStyle = {
  listStyle: 'none',
  margin: 0,
  padding: 0,
  display: 'flex'
};

const categoryItemStyle = {
  padding: '5px'
};

const Local = articleList('local');
const Global = articleList('global');
const Tech = articleList('tech');
const Sports = articleList('sports');

// Routes to the home page, the different
// article sections, and the article details page.
// The "<Provider>" element is how we pass Redux
// store data to each of our components.
export default connect(state => state.get('App').toJS())(
  ({ title, links }) => (
    <Router>
      <main>
        <h1>{title}</h1>
        <ul style={categoryListStyle}>
          {/* Renders a link for each article category.
             The key thing to note is that the "links"
             value comes from a Redux store. */}
          {links.map(l => (
            <li key={l.url} style={categoryItemStyle}>
              <NavLink
                exact
                to={l.url}
                activeStyle={{ fontWeight: 'bold' }}
              >
                {l.name}
              </NavLink>
            </li>
          ))}
        </ul>
        <section>
          <Route exact path="/" component={Home} />
          <Route exact path="/local" component={Local} />
          <Route exact path="/global" component={Global} />
          <Route exact path="/tech" component={Tech} />
          <Route exact path="/sports" component={Sports} />
          <Route exact path="/articles/:id" component={Article} />
        </section>
      </main>
    </Router>
  )
);

这个组件需要一个title属性和一个links属性。这两个值实际上都是来自 Redux 存储器的状态。请注意,它导出了一个使用connect()函数创建的高阶组件。这个函数接受一个回调函数,将存储器状态转换为组件需要的属性。

在这个例子中,你需要App状态。使用toJS()方法将这个映射转换为普通的 JavaScript 对象。这就是 Redux 状态传递给组件的方式。下面是App组件的渲染内容:

暂时忽略这些惊人的文章标题;我们稍后会回到这些。标题和分类链接是由App组件渲染的。文章标题是由<Route>元素之一渲染的。

注意所有分类都是粗体吗?这是因为它是当前选定的分类。如果选择了本地分类,所有文本将恢复为常规字体,而本地文本将加粗。这一切都是通过 Redux 状态控制的。现在让我们来看一下App减速器函数:

import { fromJS } from 'immutable';
import initialState from './initialState';

// The initial page heading.
const title = initialState.getIn(['App', 'title']);

// Links to display when an article is displayed.
const articleLinks = fromJS([
  {
    name: 'Home',
    url: '/'
  }
]);

// Links to display when we're on the home page.
const homeLinks = initialState.getIn(['App', 'links']);

// Maps the action type to a function
// that returns new state.
const typeMap = fromJS({
  // The article is being fetched, adjust
  // the "title" and "links" state.
  FETCHING_ARTICLE: state =>
    state.set('title', '...').set('links', articleLinks),

  // The article has been fetched. Set the title
  // of the article.
  FETCH_ARTICLE: (state, payload) =>
    state.set('title', payload.title),

  // The list of articles are being fetched. Set
  // the "title" and the "links".
  FETCHING_ARTICLES: state =>
    state.set('title', title).set('links', homeLinks),

  // The articles have been fetched, update the
  // "title" state.
  FETCH_ARTICLES: state => state.set('title', title)
});

// This reducer relies on the "typeMap" and the
// "type" of action that was dispatched. If it's
// not found, then the state is simply returned.
export default (state, { type, payload }) =>
  typeMap.get(type, () => state)(state, payload); 

关于这个减速器逻辑,我想提出两点。首先,你现在可以看到,使用不可变数据结构使得这段代码简洁易懂。其次,对于简单的操作,这里发生了很多状态处理。例如,以FETCHING_ARTICLEFETCHING_ARTICLES操作为例。在实际发出网络请求之前,你希望改变 UI。我认为这种明确性是 Flux 和 Redux 的真正价值。你知道为什么某些东西会改变。它是明确的,但不啰嗦。

主页组件

Redux 架构中缺少的最后一个重要部分是动作创建函数。这些函数由组件调用,以便向 Redux 存储发送有效负载。调度任何操作的最终结果是状态的改变。然而,有些操作需要去获取状态,然后才能作为有效负载调度到存储中。

让我们来看看Neckbeard News应用程序的Home组件。它将向您展示如何在将组件连接到 Redux 存储时传递动作创建函数。以下是代码:

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { Map } from 'immutable';

// Various styles...
const listStyle = {
  listStyle: 'none',
  margin: 0,
  padding: 0
};

const listItemStyle = {
  margin: '0 5px'
};

const titleStyle = {
  background: 'transparent',
  border: 'none',
  font: 'inherit',
  cursor: 'pointer',
  padding: '5px 0'
};

// What to render when the article list is empty
// (true/false). When it's empty, a single elipses
// is displayed.
const emptyMap = Map()
  .set(true, <li style={listItemStyle}>...</li>)
  .set(false, null);

class Home extends Component {
  static propTypes = {
    articles: PropTypes.arrayOf(PropTypes.object).isRequired,
    fetchingArticles: PropTypes.func.isRequired,
    fetchArticles: PropTypes.func.isRequired,
    toggleArticle: PropTypes.func.isRequired,
    filter: PropTypes.string.isRequired
  };

  static defaultProps = {
    filter: ''
  };

  // When the component is mounted, there's two actions
  // to dispatch. First, we want to tell the world that
  // we're fetching articles before they're actually
  // fetched. Then, we call "fetchArticles()" to perform
  // the API call.
  componentWillMount() {
    this.props.fetchingArticles();
    this.props.fetchArticles(this.props.filter);
  }

  // When an article title is clicked, toggle the state of
  // the article by dispatching the toggle article action.
  onTitleClick = id => () => this.props.toggleArticle(id);

  render() {
    const { onTitleClick } = this;
    const { articles } = this.props;

    return (
      <ul style={listStyle}>
        {emptyMap.get(articles.length === 0)}
        {articles.map(a => (
          <li key={a.id} style={listItemStyle}>
            <button onClick={onTitleClick(a.id)} style={titleStyle}>
              {a.title}
            </button>
            {/* The summary of the article is displayed
                 based on the "display" property. This state
                 is toggled when the user clicks the title. */}
            <p style={{ display: a.display }}>
              <small>
                <span>{a.summary} </span>
                <Link to={`articles/${a.id}`}>More...</Link>
              </small>
            </p>
          </li>
        ))}
      </ul>
    );
  }
}

// The "connect()" function connects this component
// to the Redux store. It accepts two functions as
// arguments...
export default connect(
  // Maps the immutable "state" object to a JavaScript
  // object. The "ownProps" are plain JSX props that
  // are merged into Redux store data.
  (state, ownProps) =>
    Object.assign(state.get('Home').toJS(), ownProps),

  // Sets the action creator functions as props. The
  // "dispatch()" function is when actually invokes
  // store reducer functions that change the state
  // of the store, and cause new prop values to be passed
  // to this component.
  dispatch => ({
    fetchingArticles: () =>
      dispatch({
        type: 'FETCHING_ARTICLES'
      }),

    fetchArticles: filter => {
      const headers = new Headers();
      headers.append('Accept', 'application/json');

      fetch(`/api/articles/${filter}`, { headers })
        .then(resp => resp.json())
        .then(json =>
          dispatch({
            type: 'FETCH_ARTICLES',
            payload: json
          })
        );
    },

    toggleArticle: payload =>
      dispatch({
        type: 'TOGGLE_ARTICLE',
        payload
      })
  })
)(Home); 

让我们专注于connect()函数,它用于将Home组件连接到存储。第一个参数是一个函数,它从存储中获取相关状态,并将其作为此组件的props返回。它使用ownProps,这样您就可以直接将props传递给组件,并覆盖存储中的任何内容。filter属性是我们需要这种能力的原因。

第二个参数是一个函数,它将动作创建函数作为props返回。dispatch()函数是这些动作创建函数能够向存储传递有效负载的方式。例如,toggleArticle()函数直接调用了dispatch(),并且是响应用户点击文章标题时调用的。然而,fetchingArticles()调用涉及异步行为。这意味着直到fetch()承诺解决之前,dispatch()才会被调用。您需要确保在此期间不会发生意外情况。

让我们通过查看与Home组件一起使用的 reducer 函数来结束这些内容:

import { fromJS } from 'immutable';

const typeMap = fromJS({
  // Clear any old articles right before
  // we fetch new articles.
  FETCHING_ARTICLES: state =>
    state.update('articles', a => a.clear()),

  // Articles have been fetched. Update the
  // "articles" state, and make sure that the
  // summary display is "none".
  FETCH_ARTICLES: (state, payload) =>
    state.set(
      'articles',
      fromJS(payload)
        .map(a => a.set('display', 'none'))
    ),

  // Toggles the state of the selected article
  // "id". First we have to find the index of
  // the article so that we can update it's
  // "display" state. If it's already hidden,
  // we show it, and vice-versa.
  TOGGLE_ARTICLE: (state, id) =>
    state.updateIn([
      'articles',
      state
        .get('articles')
        .findIndex(a => a.get('id') === id),
      'display',
    ], display =>
      display === 'none' ?
        'block' : 'none'
    ),
});

export default (state, { type, payload }) =>
  typeMap.get(type, s => s)(state, payload); 

在这里也使用了使用类型映射根据操作类型改变状态的相同技术。再次强调,这段代码易于理解,但系统中可以发生变化的所有内容都是明确的。

移动应用中的状态

在 React Native 移动应用中使用 Redux 怎么样?当然应该,如果您正在为 Web 和原生平台开发相同的应用程序。事实上,我已经在 React Native 中为 iOS 和 Android 都实现了Neckbeard News。我鼓励您下载本书的代码,并让这个应用程序在 Web 和原生移动设备上运行。

在移动应用中,实际上使用 Redux 并没有什么不同。唯一的区别在于所使用的状态的形状。换句话说,不要认为你可以在网页和原生应用的版本中使用完全相同的 Redux 存储和减速器函数。想想 React Native 组件。许多事情并没有一种大小适合所有的组件。你有一些组件针对 iOS 平台进行了优化,而其他一些则针对 Android 平台进行了优化。Redux 状态也是同样的道理。以下是移动应用Neckbeard News的初始状态:

import { fromJS } from 'immutable';

export default fromJS({
  Main: {
    title: 'All',
    component: 'articles',
  },
  Categories: {
    items: [
      {
        title: 'All',
        filter: '',
        selected: true,
      },
      {
        title: 'Local',
        filter: 'local',
        selected: false,
      },
      {
        title: 'Global',
        filter: 'global',
        selected: false,
      },
      {
        title: 'Tech',
        filter: 'tech',
        selected: false,
      },
      {
        title: 'Sports',
        filter: 'sports',
        selected: false,
      },
    ],
  },
  Articles: {
    filter: '',
    items: [],
  },
  Article: {
    full: '',
  },
}); 

正如你所看到的,适用于 Web 环境的相同原则在移动环境中同样适用。只是状态本身不同,以支持我们使用的特定组件以及你使用它们实现应用程序的独特方式。

架构的扩展

到目前为止,你可能已经对 Flux 的概念、Redux 的机制以及它们如何用于实现 React 应用程序的健全信息架构有了很好的掌握。那么问题就变成了,这种方法有多可持续,它能否处理任意大型和复杂的应用程序?

我认为 Redux 是实现大规模 React 应用程序的好方法。你可以预测任何给定操作的结果,因为一切都是明确的。它是声明式的。它是单向的,没有副作用。但它并非没有挑战。

Redux 的限制因素也是它的核心;因为一切都是明确的,需要扩展功能数量和复杂性的应用程序最终会有更多的移动部分。这并没有什么错;这只是游戏的本质。扩展的不可避免后果是减速。你简单地无法把握足够的全局图景来快速实现事情。

在本书的最后两章中,我们将研究与 Flux 相关但不同的方法:Relay/GraphQL。我认为这种技术可以以 Redux 无法做到的方式扩展。

总结

在本章中,你了解了 Flux,一组有助于构建 React 应用程序信息架构的架构模式。Flux 的关键思想包括单向数据流、同步更新轮和可预测的状态转换。

接下来,我将详细介绍 Redux / React 应用程序的实现。Redux 提供了 Flux 思想的简化实现。好处是无论何时都能预测。

然后,您将了解 Redux 是否具备构建可扩展架构的 React 应用程序所需的条件。答案大多数情况下是肯定的。然而,在本书的其余部分,您将探索 Relay 和 GraphQL,以查看这些技术是否能将您的应用程序提升到下一个水平。

测试你的知识

  1. 以下哪种最能描述 Flux?

  2. Flux 是一种用于增强 DOM 元素属性的架构模式,使得更容易将 API 数据传入 HTML 中。

  3. Flux 是一种用于控制应用程序中数据单向流动的架构模式,使变化更加可预测。

  4. Flux 是一个处理应用程序状态的库。

  5. Flux 和 Redux 之间有什么区别?

  6. 没有区别,它们都代表相同的架构模式。

  7. Flux 是处理 React 组件状态的官方方式,而 Redux 是要避免的东西。

  8. Redux 是 Flux 概念的一种有主见的实现,可以帮助管理应用程序中的数据流。

  9. 如何将数据从 Redux 存储库传递到组件?

  10. 您可以使用connect()高阶函数将组件连接到存储库,使用将存储库数据转换为组件属性的函数。

  11. 您可以扩展Redux.Component以自动在组件上设置来自 Redux 存储库的状态。

  12. 您可以随时从全局store对象访问状态。

  13. Redux 在 Web 应用程序和原生移动应用程序之间有什么区别?

  14. 有一个特定的redux-react-native包,你应该使用它。

  15. 没有区别。

进一步阅读

欲了解更多信息,请查看以下链接:

第二十五章:为什么选择 Relay 和 GraphQL?

在前一章中,你了解了 Flux 的架构原则。特别是,你使用 Redux 库在 React 应用程序中实现了具体的 Flux 概念。有了像 Flux 这样的模式框架,可以帮助你思考状态如何改变并在应用程序中流动,这是一件好事。在本章的结尾,你了解了在扩展方面的潜在限制。

在本章中,我们将带你走进另一种处理 React 应用程序状态的方法。与 Redux 一样,Relay 用于 Web 和移动 React 应用程序。Relay 依赖一种叫做 GraphQL 的语言,用于获取资源和改变这些资源。

Relay 的前提是它可以以 Redux 和其他处理状态的方法所限制的方式进行扩展。它通过消除它们,将焦点放在组件的数据需求上来实现这一点。

在本书的最后一章,你将会在 React Native 中实现备受欢迎的 Todo MVC 应用程序。

又一种方法?

当我了解 Relay 和 GraphQL 时,我就有了这个确切的问题。然后我提醒自己,React 的美妙之处在于它只是 UI 的视图抽象;当然会有许多处理数据的方法。因此,真正的问题是,Relay 比 Redux 之类的东西更好还是更差?

在高层次上,你可以将 Relay 看作是 Flux 架构模式的一种实现,你可以将 GraphQL 看作是描述 Relay 内部 Flux 存储工作方式的接口。在更实际的层面上,Relay 的价值在于实现的便利性。例如,使用 Redux,你需要做很多实现工作,只是为了用数据填充存储。随着时间的推移,这变得冗长。正是这种冗长使得 Redux 难以在一定程度之上进行扩展。

难以扩展的不是单个数据点。而是有大量获取请求最终构建非常复杂的存储的总体效果。Relay 通过允许你声明给定组件需要的数据,并让 Relay 找出获取这些数据并将其与本地存储同步的最佳方法来改变这一点。

Relay 的方法是否比 Redux 和其他处理 React 应用程序中数据的方法更好?在某些方面,是的。它完美吗?远非如此。这涉及到一个学习曲线,并非每个人都能理解它。它是不可变的,其中的一些部分很难使用。然而,了解 Relay 的方法的前提并看到它的实际效果是值得的,即使你最终决定不采用它。

现在,让我们分解一些词汇。

冗长的俗语

在我开始更深入地讨论数据依赖和突变之前,我认为我应该先介绍一些一般的 Relay 和 GraphQL 术语定义:

  • Relay:一个管理应用程序数据获取和数据突变的库,并提供高阶组件,将数据传递给我们的应用程序组件

  • GraphQL:用于指定数据需求和数据突变的查询语言

  • 数据依赖:一个抽象概念,表示给定的 React 组件依赖于特定的数据

  • 查询:查询是数据依赖的一部分,用 GraphQL 语法表示,并由封装的 Relay 机制执行

  • 片段:较大的 GraphQL 查询的一部分

  • 容器:一个 Relay React 组件,将获取的数据传递给应用程序 React 组件

  • 突变:一种特殊类型的 GraphQL 查询,它改变了一些远程资源的状态,一旦完成,Relay 必须找出如何在前端反映这种变化

让我们快速谈谈数据依赖和突变,这样我们就可以看一些应用程序代码。

声明性数据依赖

Relay 使用 collocation 这个术语来描述声明性数据依赖,这些数据依赖与使用数据的组件并存。这意味着你不必四处寻找实际获取组件数据的动作创建函数,这些函数分散在几个模块中。通过 collocation,你可以清楚地看到组件需要什么。

让我们先尝试一下这是什么样子。如果你想显示用户的名字和姓氏,你需要告诉 Relay 你的组件需要这些数据。然后,你可以放心,数据将始终存在于你的组件中。这是一个例子:

const User = ({ first, last }) => ( 
  <section> 
    <p>{first}</p> 
    <p>{last}</p> 
  </section> 
); 

const UserContainer = Relay.createFragmentContainer(User, { 
   user: () => graphql` 
    fragment on User { 
      first, 
      last, 
   } 
  `
}); 

你有两个组件在这里。首先,有User组件。这是应用程序组件,实际上呈现了firstlast名称数据的 UI 元素。请注意,这只是一个普通的旧 React 组件,呈现传递给它的 props。使用您创建的UserContainer组件,Relay 遵循了您在本书中学到的容器模式。在createFragmentContainer()函数中,您通过传递 GraphQL 语法的片段来指定此组件需要的数据依赖关系。

再次强调,暂时不要过多关注 Relay/GraphQL 的具体细节。这里的想法只是简单说明这是您需要编写的所有代码,以获取组件所需的数据。其余的只是引导 Relay 查询机制,您将在下一章中看到。

改变应用程序状态

Relay mutations 是导致系统产生副作用的操作,因为它们改变了 UI 关心的某些资源的状态。关于 Relay mutations 有趣的是,它们关心的是由于某些状态变化而导致的数据的副作用。例如,如果您更改用户的名称,这肯定会影响显示用户详细信息的屏幕。但是,它也可能影响显示多个用户的列表屏幕。

让我们看看 mutation 是什么样子的:

const mutation = graphql`
  mutation ChangeAgeMutation($input: ChangeAgeInput!) {
    changeTodoStatus(input: $input) {
      viewer {
        users
      }
      user {
        age
      }
    }
  }
`; 

这就是 Relay 能够确定在执行此 mutation 的副作用可能受到影响的内容。例如,用户可能会改变,但viewer.users集合也可能会改变。您将在接下来的章节中看到更多 mutation 的操作。

GraphQL 后端和微服务

到目前为止,我所涵盖的关于 Relay 的一切都是在浏览器中的。Relay 需要将其 GraphQL 查询发送到某个地方。为此,您需要一个 GraphQL 后端。您可以使用 Node.js 和一些 GraphQL 库来实现这一点。您创建所谓的模式,描述将使用的所有数据类型、查询和 mutation。

在浏览器中,Relay 通过减少数据流复杂性来帮助您扩展应用程序。您有一种声明所需数据的方法,而不必担心如何获取它。实际上需要解析这些数据的是后端的模式。

这是 GraphQL 帮助解决的另一个扩展问题。现代 Web 应用程序由微服务组成。这些是较小的、自包含的 API 端点,提供一些比整个应用程序更小的特定目的(因此称为微服务)。我们的应用程序的工作是将这些微服务组合在一起,并为前端提供有意义的数据。

再次,你面临着一个可扩展性问题——如何在不引入不可逾越的复杂性的情况下维护由许多微服务组成的后端?这是 GraphQL 类型擅长的事情。在接下来的章节中,您将开始使用后端 GraphQL 服务实现您的 Todo 应用程序。

摘要

本章的目标是在本书的最后一章之前,快速向您介绍 Relay 和 GraphQL 的概念,您将在最后一章中实现一些 Relay/GraphQL 代码。

Relay 是 React 应用程序中状态管理问题的另一种方法。它不同之处在于,它减少了与数据获取代码相关的复杂性,我们必须使用其他 Flux 方法(如 Redux)编写。

Relay 的两个关键方面是声明式数据依赖和显式的突变副作用处理。所有这些都通过 GraphQL 语法表达。为了拥有一个 Relay 应用程序,你需要一个数据模式存在的 GraphQL 后端。现在,进入最后一章,你将更详细地研究 Relay/GraphQL 的概念。

测试你的知识

  1. Relay 和其他受 Flux 启发的库(如 Redux)之间有什么区别?

  2. 没有区别,Relay 只是另一个 Flux 选项。

  3. Relay 是为 React Native 应用程序设计的,你应该在 Web 应用程序中使用 Redux。

  4. Relay 通过允许数据依赖声明和隐藏所有服务器通信复杂性来帮助扩展您的 Flux 架构。

  5. Relay 如何简化 React 组件的数据需求?

  6. 通过合并数据依赖查询,您可以准确地看到您的组件使用的数据,而无需查看执行获取操作的代码。

  7. 通过预先获取所有应用程序数据,Relay 可以查询每个组件需要的数据。

  8. 通过抽象网络调用。GraphQL 是可选的,如果你愿意,你可以使用直接的 HTTP。

  9. 在基于 Relay 的应用程序中,您的 React 组件如何与服务器通信?

  10. 您必须实现自己的网络通信逻辑。Relay 只处理将数据传递给组件。

  11. Relay 编译在您的组件中找到的 GraphQL 查询,并为您处理所有的 GraphQL 服务器通信,包括缓存优化。

更多阅读

访问以下链接获取更多信息:

第二十六章:构建 Relay React 应用

在上一章中,你对 Relay/GraphQL 有了一个概览,并了解了为什么应该在 React 应用程序中使用这种方法。现在你可以使用 Relay 构建你的 Todo React Native 应用程序。在本章结束时,你应该对 GraphQL 中心架构中的数据传输感到自如。

TodoMVC 和 Relay

我最初计划扩展我们在本章前面工作过的 Neckbeard News 应用程序。但我决定使用 Relay 的 TodoMVC 示例(github.com/taion/relay-todomvc),这是一个强大而简洁的示例,我很难超越它。

我将带你走过一个示例 React Native 实现的 Todo 应用程序。关键是,它将使用与 Web UI 相同的 GraphQL 后端。我认为这对于想要构建其应用程序的 Web 和原生版本的 React 开发人员来说是一个胜利;他们可以共享相同的模式!

我已经在随本书一起提供的代码中包含了 TodoMVC 应用程序的 Web 版本,但我不会详细介绍它的工作原理。如果你在过去 5 年里从事过 Web 开发,你可能已经接触过一个样本 Todo 应用程序。这是 Web 版本的样子:

即使你以前没有使用过任何 TodoMVC 应用程序,我建议在尝试实现本章剩余部分的原生版本之前,先尝试玩一下这个。

你即将实现的原生版本的目标不是功能平等。事实上,你的目标是实现一个非常简化的 todo 功能子集。目标是向你展示,Relay 在原生平台上的工作方式与在 Web 平台上基本相同,并且 GraphQL 后端可以在 Web 和原生应用程序之间共享。

GraphQL 模式

模式是 GraphQL 后端服务器和前端 Relay 组件使用的词汇。GraphQL 类型系统使模式能够描述可用的数据,以及在查询请求到来时如何将所有数据组合在一起。这就是整个方法如此可扩展的原因,因为 GraphQL 运行时会找出如何组合数据。你只需要提供告诉 GraphQL 数据在哪里的函数;例如,在数据库中或在某个远程服务端点中。

让我们来看看在 TodoMVC 应用程序的 GraphQL 模式中使用的类型,如下所示:

import {
  GraphQLBoolean,
  GraphQLID,
  GraphQLInt,
  GraphQLList,
  GraphQLNonNull,
  GraphQLObjectType,
  GraphQLSchema,
  GraphQLString
} from 'graphql';
import {
  connectionArgs,
  connectionDefinitions,
  connectionFromArray,
  cursorForObjectInConnection,
  fromGlobalId,
  globalIdField,
  mutationWithClientMutationId,
  nodeDefinitions,
  toGlobalId
} from 'graphql-relay';

import {
  Todo,
  User,
  addTodo,
  changeTodoStatus,
  getTodo,
  getTodos,
  getUser,
  getViewer,
  markAllTodos,
  removeCompletedTodos,
  removeTodo,
  renameTodo
} from './database';

const { nodeInterface, nodeField } = nodeDefinitions(
  globalId => {
    const { type, id } = fromGlobalId(globalId);
    if (type === 'Todo') {
      return getTodo(id);
    }
    if (type === 'User') {
      return getUser(id);
    }
    return null;
  },
  obj => {
    if (obj instanceof Todo) {
      return GraphQLTodo;
    }
    if (obj instanceof User) {
      return GraphQLUser;
    }
    return null;
  }
);

const GraphQLTodo = new GraphQLObjectType({
  name: 'Todo',
  fields: {
    id: globalIdField(),
    complete: { type: GraphQLBoolean },
    text: { type: GraphQLString }
  },
  interfaces: [nodeInterface]
});

const {
  connectionType: TodosConnection,
  edgeType: GraphQLTodoEdge
} = connectionDefinitions({ nodeType: GraphQLTodo });

const GraphQLUser = new GraphQLObjectType({
  name: 'User',
  fields: {
    id: globalIdField(),
    todos: {
      type: TodosConnection,
      args: {
        status: {
          type: GraphQLString,
          defaultValue: 'any'
        },
        ...connectionArgs
      },
      resolve: (obj, { status, ...args }) =>
        connectionFromArray(getTodos(status), args)
    },
    numTodos: {
      type: GraphQLInt,
      resolve: () => getTodos().length
    },
    numCompletedTodos: {
      type: GraphQLInt,
      resolve: () => getTodos('completed').length
    }
  },
  interfaces: [nodeInterface]
});

const GraphQLRoot = new GraphQLObjectType({
  name: 'Root',
  fields: {
    viewer: {
      type: GraphQLUser,
      resolve: getViewer
    },
    node: nodeField
  }
});

const GraphQLAddTodoMutation = mutationWithClientMutationId({
  name: 'AddTodo',
  inputFields: {
    text: { type: new GraphQLNonNull(GraphQLString) }
  },
  outputFields: {
    viewer: {
      type: GraphQLUser,
      resolve: getViewer
    },
    todoEdge: {
      type: GraphQLTodoEdge,
      resolve: ({ todoId }) => {
        const todo = getTodo(todoId);
        return {
          cursor: cursorForObjectInConnection(getTodos(), todo),
          node: todo
        };
      }
    }
  },
  mutateAndGetPayload: ({ text }) => {
    const todoId = addTodo(text);
    return { todoId };
  }
});

const GraphQLChangeTodoStatusMutation = mutationWithClientMutationId({
  name: 'ChangeTodoStatus',
  inputFields: {
    id: { type: new GraphQLNonNull(GraphQLID) },
    complete: { type: new GraphQLNonNull(GraphQLBoolean) }
  },
  outputFields: {
    viewer: {
      type: GraphQLUser,
      resolve: getViewer
    },
    todo: {
      type: GraphQLTodo,
      resolve: ({ todoId }) => getTodo(todoId)
    }
  },
  mutateAndGetPayload: ({ id, complete }) => {
    const { id: todoId } = fromGlobalId(id);
    changeTodoStatus(todoId, complete);
    return { todoId };
  }
});

const GraphQLMarkAllTodosMutation = mutationWithClientMutationId({
  name: 'MarkAllTodos',
  inputFields: {
    complete: { type: new GraphQLNonNull(GraphQLBoolean) }
  },
  outputFields: {
    viewer: {
      type: GraphQLUser,
      resolve: getViewer
    },
    changedTodos: {
      type: new GraphQLList(GraphQLTodo),
      resolve: ({ changedTodoIds }) => changedTodoIds.map(getTodo)
    }
  },
  mutateAndGetPayload: ({ complete }) => {
    const changedTodoIds = markAllTodos(complete);
    return { changedTodoIds };
  }
});

const GraphQLRemoveCompletedTodosMutation = mutationWithClientMutationId(
  {
    name: 'RemoveCompletedTodos',
    outputFields: {
      viewer: {
        type: GraphQLUser,
        resolve: getViewer
      },
      deletedIds: {
        type: new GraphQLList(GraphQLString),
        resolve: ({ deletedIds }) => deletedIds
      }
    },
    mutateAndGetPayload: () => {
      const deletedTodoIds = removeCompletedTodos();
      const deletedIds = deletedTodoIds.map(
        toGlobalId.bind(null, 'Todo')
      );
      return { deletedIds };
    }
  }
);

const GraphQLRemoveTodoMutation = mutationWithClientMutationId({
  name: 'RemoveTodo',
  inputFields: {
    id: { type: new GraphQLNonNull(GraphQLID) }
  },
  outputFields: {
    viewer: {
      type: GraphQLUser,
      resolve: getViewer
    },
    deletedId: {
      type: GraphQLID,
      resolve: ({ id }) => id
    }
  },
  mutateAndGetPayload: ({ id }) => {
    const { id: todoId } = fromGlobalId(id);
    removeTodo(todoId);
    return { id };
  }
});

const GraphQLRenameTodoMutation = mutationWithClientMutationId({
  name: 'RenameTodo',
  inputFields: {
    id: { type: new GraphQLNonNull(GraphQLID) },
    text: { type: new GraphQLNonNull(GraphQLString) }
  },
  outputFields: {
    todo: {
      type: GraphQLTodo,
      resolve: ({ todoId }) => getTodo(todoId)
    }
  },
  mutateAndGetPayload: ({ id, text }) => {
    const { id: todoId } = fromGlobalId(id);
    renameTodo(todoId, text);
    return { todoId };
  }
});

const GraphQLMutation = new GraphQLObjectType({
  name: 'Mutation',
  fields: {
    addTodo: GraphQLAddTodoMutation,
    changeTodoStatus: GraphQLChangeTodoStatusMutation,
    markAllTodos: GraphQLMarkAllTodosMutation,
    removeCompletedTodos: GraphQLRemoveCompletedTodosMutation,
    removeTodo: GraphQLRemoveTodoMutation,
    renameTodo: GraphQLRenameTodoMutation
  }
});

export default new GraphQLSchema({
  query: GraphQLRoot,
  mutation: GraphQLMutation
});

这里导入了很多东西,所以我将从导入开始。我想包括所有这些导入,因为我认为它们在这次讨论中是相关的。首先,有来自graphql库的基本 GraphQL 类型。接下来,您有来自graphql-relay库的辅助程序,简化了定义 GraphQL 模式。最后,有来自您自己的database模块的导入。这不一定是一个数据库,实际上,在这种情况下,它只是模拟数据。例如,如果您需要与远程 API 端点通信,您可以将database替换为api,或者我们可以将两者结合起来;就您的 React 组件而言,这都是 GraphQL。

然后,您定义了一些自己的 GraphQL 类型。例如,GraphQLTodo类型有两个字段——textcomplete。一个是布尔值,一个是字符串。关于 GraphQL 字段的重要事情是resolve()函数。这是告诉 GraphQL 运行时如何在需要时填充这些字段的方法。这两个字段只是返回属性值。

然后,有GraphQLUser类型。这个字段代表了用户在 UI 中的整个宇宙,因此得名。例如,todos字段是您如何从 Relay 组件查询待办事项的方式。它使用connectionFromArray()函数进行解析,这是一种快捷方式,可以省去更冗长的字段定义。然后,有GraphQLRoot类型。这有一个单一的viewer字段,用作所有查询的根。

现在让我们更仔细地看一下添加待办事项的突变,如下所示。出于篇幅考虑,我不会介绍此应用程序的 Web 版本中使用的每个突变:

const GraphQLAddTodoMutation = mutationWithClientMutationId({
  name: 'AddTodo',
  inputFields: {
    text: { type: new GraphQLNonNull(GraphQLString) }
  },
  outputFields: {
    viewer: {
      type: GraphQLUser,
      resolve: getViewer
    },
    todoEdge: {
      type: GraphQLTodoEdge,
      resolve: ({ todoId }) => {
        const todo = getTodo(todoId);
        return {
          cursor: cursorForObjectInConnection(getTodos(), todo),
          node: todo
        };
      }
    }
  },
  mutateAndGetPayload: ({ text }) => {
    const todoId = addTodo(text);
    return { todoId };
  }
}); 

所有的突变都有一个mutateAndGetPayload()方法,这是突变实际上调用某个外部服务来改变数据的方法。返回的有效负载可以是已更改的实体,但也可以包括作为副作用而更改的数据。这就是outputFields发挥作用的地方。这是传递给 Relay 在浏览器中的信息,以便它有足够的信息来根据突变的副作用正确更新组件。别担心,您很快就会从 Relay 的角度看到这是什么样子。

您在这里创建的突变类型用于保存所有应用程序突变。最后,这是整个模式如何组合并从模块中导出的方式:

export default new GraphQLSchema({
  query: GraphQLRoot,
  mutation: GraphQLMutation
}); 

现在不要担心将此模式馈送到 GraphQL 服务器中。

引导 Relay

此时,您的 GraphQL 后端已经启动运行。现在,您可以专注于前端的 React 组件。特别是,您将在 React Native 环境中查看 Relay,这实际上只有一些细微的差异。例如,在 Web 应用程序中,通常是react-router引导 Relay。在 React Native 中,情况有些不同。让我们看看作为本机应用程序入口点的App.js文件:

import React from 'react';
import { View, Text } from 'react-native';
import { Network } from 'relay-local-schema';
import { Environment, RecordSource, Store } from 'relay-runtime';
import { QueryRenderer, graphql } from 'react-relay';

import schema from './data/schema';
import styles from './styles';
import TodoInput from './TodoInput';
import TodoList from './TodoList';

if (typeof Buffer === 'undefined')
  global.Buffer = require('buffer').Buffer;

const environment = new Environment({
  network: Network.create({ schema }),
  store: new Store(new RecordSource())
});

export default () => (
  <QueryRenderer
    environment={environment}
    query={graphql`
      query App_Query($status: String!) {
        viewer {
          ...TodoList_viewer
        }
      }
    `}
    variables={{ status: 'any' }}
    render={({ error, props }) => {
      if (error) {
        return <Text>Error!</Text>;
      }
      if (!props) {
        return <Text>Loading...</Text>;
      }
      return (
        <View style={styles.container}>
          <TodoInput environment={environment} {...props} />
          <TodoList {...props} />
        </View>
      );
    }}
  />
); 

让我们从这里开始分解发生的事情,从环境常量开始:

const environment = new Environment({
  network: Network.create({ schema }),
  store: new Store(new RecordSource())
});

这是您与 GraphQL 后端通信的方式,通过配置网络。在这个例子中,您从relay-local-schema中导入Network,这意味着没有进行网络请求。这对于刚开始使用特别方便,尤其是构建 React Native 应用程序。

接下来是QueryRenderer组件。这个 Relay 组件用于渲染依赖于 GraphQL 查询的其他组件。它期望一个查询属性:

query={graphql`
  query App_Query($status: String!) {
    viewer {
      ...TodoList_viewer
    }
  }
`}

请注意,查询是由它们所在的模块前缀的。在这种情况下,是App。这个查询使用了另一个模块TodoList中的 GraphQL 片段,并命名为TodoList_viewer。您可以向查询传递变量:

variables={{ status: 'any' }}

然后,render属性是一个在 GraphQL 数据准备就绪时渲染组件的函数:

render={({ error, props }) => {
  if (error) {
    return <Text>Error!</Text>;
  }
  if (!props) {
    return <Text>Loading...</Text>;
  }
  return (
    <View style={styles.container}>
      <TodoInput environment={environment} {...props} />
      <TodoList {...props} />
    </View>
  );
}}

如果出现问题,错误将包含有关错误的信息。如果没有错误和没有属性,那么可以安全地假定 GraphQL 数据仍在加载中。

添加待办事项

TodoInput组件中,有一个文本输入框,允许用户输入新的待办事项。当他们输入完待办事项后,Relay 将需要向后端 GraphQL 服务器发送一个 mutation。以下是组件代码的样子:

import React, { Component } from 'react';
import { TextInput } from 'react-native';

import styles from './styles';
import AddTodoMutation from './mutations/AddTodoMutation';

export default class App extends Component {
  onSubmitEditing = ({ nativeEvent: { text } }) => {
    const { environment, viewer } = this.props;
    AddTodoMutation.commit(environment, viewer, text);
  };

  render() {
    return (
      <TextInput
        style={styles.textInput}
        placeholder="What needs to be done?"
        onSubmitEditing={this.onSubmitEditing}
      />
    );
  }
} 

它看起来并不比您典型的 React Native 组件有多大的不同。突出的部分是 mutation——AddTodoMutation。这是告诉 GraphQL 后端您想要创建一个新的todo节点的方式。

让我们看看目前为止应用程序的样子:

用于添加新待办事项的文本框就在待办事项列表的上方。现在,让我们看看TodoList组件,它负责渲染待办事项列表。

渲染待办事项

TodoList组件的工作是渲染待办事项列表项。当AddTodoMutation发生时,TodoList组件需要能够渲染这个新项目。Relay 负责更新内部数据存储,其中包含我们所有的 GraphQL 数据。再次查看项目列表,添加了几个更多的待办事项:

这是TodoList组件本身:

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { View } from 'react-native';
import { createFragmentContainer, graphql } from 'react-relay';

import Todo from './Todo';

class TodoList extends Component {
  static propTypes = {
    viewer: PropTypes.object.isRequired,
    relay: PropTypes.object.isRequired
  };

  static contextTypes = {
    relay: PropTypes.shape({
      variables: PropTypes.shape({
        status: PropTypes.string.isRequired
      }).isRequired
    }).isRequired
  };

  render() {
    const { viewer } = this.props;
    return (
      <View>
        {viewer.todos.edges.map(edge => (
          <Todo key={edge.node.id} viewer={viewer} todo={edge.node} />
        ))}
      </View>
    );
  }
}

export default createFragmentContainer(
  TodoList,
  graphql`
    fragment TodoList_viewer on User {
      todos(status: $status, first: 2147483647)
        @connection(key: "TodoList_todos") {
        edges {
          node {
            id
            complete
            ...Todo_todo
          }
        }
      }
      id
      numTodos
      numCompletedTodos
      ...Todo_viewer
    }
  `
); 

获取所需数据的相关 GraphQL 作为第二个参数传递给createFragmentContainer()。这是组件的声明性数据依赖关系。当您渲染<Todo>组件时,您会将edge.todo数据传递给它。现在,让我们看看Todo组件本身是什么样子。

完成待办事项

这个应用程序的最后一部分是渲染每个待办事项并提供更改待办事项状态的能力。让我们看看这段代码:

import React, { Component } from 'react';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import { createFragmentContainer, graphql } from 'react-relay';
import { Text, View, Switch } from 'react-native';

import ChangeTodoStatusMutation from './mutations/ChangeTodoStatusMutation';
import styles from './styles';

const completeStyleMap = new Map([
  [true, { textDecorationLine: 'line-through' }],
  [false, {}]
]);

class Todo extends Component {
  static propTypes = {
    viewer: PropTypes.object.isRequired,
    todo: PropTypes.object.isRequired,
    relay: PropTypes.object.isRequired
  };

  onValueChange = value => {
    const { relay, viewer, todo } = this.props;

    ChangeTodoStatusMutation.commit(
      relay.environment,
      viewer,
      todo,
      value
    );
  };

  render() {
    const {
      todo: { text, complete }
    } = this.props;

    return (
      <View style={styles.todoItem}>
        <Switch value={complete} onValueChange={this.onValueChange} />
        <Text style={completeStyleMap.get(complete)}>{text}</Text>
      </View>
    );
  }
}

export default createFragmentContainer(Todo, {
  viewer: graphql`
    fragment Todo_viewer on User {
      id
    }
  `,
  todo: graphql`
    fragment Todo_todo on Todo {
      id
      complete
      text
    }
  `
}); 

实际渲染的组件是一个开关控件和项目文本。当用户标记待办事项为完成时,项目文本会被划掉。用户也可以取消选中项目。ChangeTodoStatusMutation变异发送请求到 GraphQL 后端以更改todo状态。GraphQL 后端然后与任何需要使此操作发生的微服务进行通信。然后,它会响应此组件所依赖的字段。

我想指出的这段代码的重要部分是 Relay 容器中使用的片段。这个容器实际上并不直接使用它们。相反,它们被TodoList组件中的todos查询使用(Todo.getFrament())。这很有用,因为这意味着您可以在另一个上下文中使用Todo组件,使用另一个查询,并且它的数据依赖关系总是会被满足。

摘要

在本章中,您实现了一些特定的 Relay 和 GraphQL 想法。从 GraphQL 模式开始,您学习了如何声明应用程序使用的数据以及这些数据类型如何解析为特定的数据源,例如微服务端点。然后,您学习了如何在 React Native 应用程序中从 Relay 引导 GraphQL 查询。接下来,您将详细了解如何添加、更改和列出待办事项。应用程序本身使用与 Todo 应用程序的 Web 版本相同的模式,这在开发 Web 和原生 React 应用程序时会更加容易。

好了,这本书就到这里了。我们一起学习了很多材料,我希望你从阅读中学到的东西和我从写作中学到的一样多。如果有一个主题是你应该记住的,那就是 React 只是一个渲染抽象。随着新的渲染目标出现,新的 React 库也会出现。随着开发人员想出处理大规模状态的新方法,你会看到新的技术和库发布。我希望你现在已经准备好在这个快速发展的 React 生态系统中工作了。

第二十七章:测试你的知识答案

第一章

  1. 声明式 UI 结构是什么,React 如何支持这个想法?

  2. 声明式 UI 结构定义了 UI 组件是什么,而不是担心它是如何定义的。React 通过允许使用 JSX 语法声明组件来支持这个想法。

  3. React 如何提高渲染性能?

  4. React 具有虚拟 DOM,它比较内存中组件数据的更改,尽量避免浏览器 DOM。React 16 具有新的内部架构,允许将渲染分成更小的工作块并优先处理。

  5. 何时会渲染片段?

  6. 片段用于避免渲染不必要的 DOM 元素

第二章

  1. 您可以将所有标准 HTML 标签用作 JSX 元素吗?

  2. 是的,React 默认支持这一点

  3. 如何从组件中访问子元素?

  4. 通过 children 属性始终可以访问子 JSX 元素

  5. React 中的 Fragment 组件是做什么的?

  6. 它作为一个容器组件,通过 否定 渲染无意义的元素, 如 容器 divs

第三章

  1. 为什么总是初始化组件的状态是一个好主意?

  2. 因为如果 render() 方法期望状态值,您需要确保它们始终存在,以避免意外的渲染行为。

  3. 何时应该使用属性而不是状态?

  4. 状态应该只用于可以改变的值。对于其他所有情况,应该使用属性。

  5. 在 React 中什么是上下文?

  6. 上下文用于避免瞬态属性。上下文用于与少数组件共享常见数据。

第四章

  1. 在 React 中,事件处理程序是什么使得它声明式的?

  2. React 事件处理程序被声明为组件 JSX 的一部分

  3. 高阶事件处理程序函数的常见用途是什么?

  4. 当您有多个处理相同事件的组件时,可以使用高阶函数将被点击的项目的 ID 绑定到处理程序函数

  5. 您可以将内联函数传递给事件属性吗?

  6. 是的。当事件处理程序很简单时,这是更可取的。

  7. 为什么 React 使用事件实例池而不是在每个事件中创建新实例?

  8. 为了避免在 短时间内 触发大量事件时调用垃圾收集器来删除未使用的事件实例

第五章

  1. 为什么应该避免庞大的 React 组件?

  2. 因为它们难以理解,并且难以重构为以后可重用的较小组件。

  3. 为什么应该使组件功能化?

  4. 功能组件只依赖于传递给它的属性值。它们不依赖于状态或生命周期方法,这两者都是潜在的问题来源。

  5. 渲染道具如何简化 React 应用程序?

  6. 它们减少了组件的直接依赖数量,使您能够组合新的行为。

第六章

  1. render()是一个生命周期方法吗?

  2. 是的,render()与任何其他生命周期方法没有区别。

  3. 以下哪项是componentWillUnmount()方法的有效用途?

  4. 取消异步操作,如果组件未挂载则会失败。

  5. 错误边界组件使用哪个生命周期方法?

  6. **componentDidCatch()**

第七章

  1. 以下哪项最能描述prop-types包?

  2. 用于在开发过程中验证传递给组件的属性值。

  3. 如何验证属性值是否可以被渲染?

  4. 使用PropTypes.node验证器。

  5. PropTypes.shape验证器的目的是什么?

  6. 确保对象具有特定类型的特定属性,忽略任何额外的属性。

第八章

  1. 何时应该继承组件状态?

  2. 只有当你有许多不同的组件都共享相同的状态结构,但渲染不同的输出时

  3. 什么是高阶组件?

  4. 返回另一个组件的组件

  5. 如果你从一个组件继承 JSX,你应该覆盖什么?

  6. 你可以在componentDidMount()中向继承的组件传递新的状态值。

第九章

  1. react-router包是 React 应用程序中路由的官方包,因此是唯一的选择。

  2. 不,react-router是 React 的事实上的路由解决方案,除非你有充分的理由不使用它。

  3. RouteRouter组件之间有什么区别?

  4. Route用于根据 URL 匹配渲染组件,Router用于声明路由-组件映射。

  5. 如何在路由更改时仅更改 UI 的某些部分?

  6. 您可以使用Route组件根据提供的path属性渲染特定于任何给定部分的内容。您可以有多个具有相同path值的Route

  7. 何时应该使用NavLink组件?

  8. 当您想要使用activeStyleactiveClassName属性来为活动链接设置样式时

  9. 如何从 URL 路径中获取值?

  10. 您可以使用: 语法来指定这是一个变量,react-router将将此值作为属性传递给您的组件

第十章

  1. react-dom中的render()函数和react-dom/server中的renderToString()函数之间有什么区别?

  2. render()函数仅用于在浏览器中将 React 组件内容与 DOM 同步。renderToString()函数不需要 DOM,因为它将标记呈现为字符串。

  3. 服务器端的路由是必要的,因为:

  4. 服务器上的路由将根据请求的 URL 确定渲染的内容。然后将此内容发送到浏览器,以便用户感知更快的加载时间。

  5. 在协调服务器端渲染的 React 标记与浏览器中的 React 组件时应该使用哪个函数?

  6. 当服务器发送渲染的 React 组件时,始终使用hydrate()。与render()不同,hydrate()期望渲染的组件标记并且可以有效地处理它。

第十一章

  1. 为什么 React 开发人员应该考虑移动优先的方法来设计他们的应用程序?

  2. 因为将移动设备作为应用程序的主要显示目标可以确保您可以处理移动设备,并且向更大的设备进行扩展比反之容易。

  3. react-routerreact-bootstrap集成良好吗?

  4. 是的。尽管您可能希望使用react-router-bootstrap包,以确保您可以将链接添加到NavItemMenuItem组件中。

  5. 如何使用react-bootstrap渲染项目列表?

  6. 使用react-bootstrap中的ListGroupListGroupItem组件。

  7. 为什么应该为react-bootstrap表单组件创建一个抽象?

  8. 因为有许多相关的组件需要用于基本输入,创建这种抽象会让生活更容易。

第十二章

  1. React Native 的主要目标是什么?

  2. 让 React 开发人员能够将他们已经了解的构建 UI 组件的知识应用到构建原生移动应用程序中。

  3. React Native 在 iOS 和 Android 上提供完全相同的体验吗?

  4. 不,iOS 和 Android 有根本不同的用户体验。

  5. React Native 是否消除了移动 Web 应用的需求?

  6. 不,移动 Web 应用程序始终需要。当您需要原生移动应用程序时,React Native 就在那里为您。

第十三章

  1. create-react-native-app工具是由 Facebook 创建的

  2. 不,这是一个社区支持的工具,跟随 create-react-app 的脚步

  3. 为什么应该全局安装create-react-native-app

  4. 因为这是一个用于生成项目样板的工具,实际上并不是项目的一部分

  5. Expo 应用在移动设备上的作用是什么?

  6. 这是一个帮助开发人员在开发过程中在移动设备上运行其应用程序的工具,开销非常小

  7. React Native 打包程序能够模拟 iOS 和 Android 设备

  8. 它不会这样做,但它会与 iOS 和 Android 模拟器通信以运行应用程序

第十四章

  1. CSS 样式和 React Native 组件使用的样式有什么区别?

  2. React Native 与 CSS 共享许多样式属性。样式属性在 React Native 中表示为普通对象属性

  3. 为什么在设计布局时需要考虑状态栏?

  4. 因为状态栏可能会干扰 iOS 上的组件

  5. 什么是 flexbox 模型?

  6. flexbox 布局模型用于以一种抽象许多细节并自动对布局更改做出灵活响应的方式来布局组件

  7. 在考虑布局选项时,屏幕方向是否是一个因素?

  8. 是的,在开发过程中,始终需要确保在纵向或横向方向上没有意外情况

第十五章

  1. 在 React web 应用和 React Native 应用中导航的主要区别是什么?

  2. Web 应用程序依赖于 URL 作为移动的中心概念。原生应用程序没有这样的概念,因此由开发人员和他们使用的导航库来管理他们的屏幕。

  3. 应该使用什么函数来导航到新屏幕?

  4. 屏幕组件会传递一个导航属性。您应该 使用 navigation.navigate() 来移动到另一个屏幕。

  5. react-navigation 是否为您处理返回按钮功能?

  6. 是的。包括 Android 系统上的内置返回按钮。

  7. 如何将数据传递给屏幕?

  8. 您可以将普通对象作为第二个参数传递给 navigation.navigate()然后,通过 navigation.getParam() 可以访问这些属性。

第十六章

  1. FlatList组件可以呈现什么类型的数据?

  2. FlatList期望一个对象数组。renderItem属性接受一个负责渲染每个项目的函数。

  3. 为什么key属性是传递给FlatList的每个数据项的要求?

  4. 这样列表可以进行有效的相等性检查,有助于在列表数据更新期间提高渲染性能。

  5. 如何渲染在滚动期间保持固定位置的列表控件?

  6. 您可以使用FlatListListHeaderComponent属性。

  7. 当用户滚动列表时,如何懒加载更多数据?

  8. 您可以为FlatListonEndReached属性提供一个函数。当用户接近列表的末尾时,将调用此函数,并且该函数可以使用更多数据填充列表数据。

第十七章

  1. 进度条和活动指示器有什么区别?

  2. 进度条是确定的,而进度指示器用于指示不确定的时间量。

  3. React Native 的ActivityIndicator组件在 iOS 和 Android 上是否工作相同?

  4. 是的,这个组件是平台无关的。

  5. 如何以平台无关的方式使用ProgressViewIOSProgressBarAndroid组件?

  6. 您可以定义自己的ProgressBar组件,导入具有特定于平台的文件扩展名的其他组件。

第十八章

  1. 在 React Native 中找到的地理位置 API 的工作方式与 Web 浏览器中找到的地理位置 API 相同。

  2. 是的,它是相同的 API。

  3. React Native 应用程序中地理位置 API 的主要目的是什么?

  4. 查找设备的纬度和经度坐标,并将这些值与其他 API 一起使用,以查找有用信息,比如地址。

  5. MapView组件能够显示用户附近的兴趣点吗?

  6. 是的,默认情况下已启用。

  7. 如何在地图上标记点?

  8. 通过将纬度/经度数组数据作为属性传递给MapView组件。

第十九章

  1. 为什么要更改文本输入的虚拟键盘上的返回键?

  2. 因为在某些情况下,有一个搜索按钮或其他更符合输入上下文的东西是有意义的

  3. 应该使用哪个TextInput属性将输入标记为密码字段?

  4. **secureTextEntry**

  5. 为什么要为选择元素创建抽象?

  6. 由于两个平台之间的样式挑战

  7. 为什么要为日期和时间选择器创建抽象?

  8. 因为 iOS 和 Android 的组件完全不同

第二十章

  1. 警报和模态之间有什么区别?

  2. 警报在继承移动环境的外观和感觉方面做得很好,而模态是常规的 React Native 视图,您可以完全控制其样式。

  3. 哪个 React Native 组件可用于创建覆盖屏幕上其他组件的模态视图?

  4. Modal组件。

  5. 在 Android 系统上显示被动通知的最佳方法是什么?

  6. 您可以使用ToastAndroid React Native API。在 iOS 上没有不涉及自己编写代码的好的替代方法。

  7. React Native Alert API 仅在 iOS 上可用。

  8. 错误

第二十一章

  1. Web 应用程序和本机移动应用程序之间用户交互的主要区别是什么?

  2. 没有鼠标。相反,用户使用手指与您的 UI 进行交互。这是一种根本不同于使用鼠标的体验,需要进行调整。

  3. 如何在 React Native 中为用户提供触摸反馈?

  4. 通过使用TouchableOpacityTouchableHighlight组件包装可触摸组件。

  5. 移动应用程序中的滚动比 Web 应用程序中的滚动复杂得多的原因是什么?

  6. 在移动 Web 应用程序中滚动需要考虑诸如速度之类的因素,因为用户是用手指进行交互的。否则,交互会感觉不自然。

  7. 为什么要使用ScrollView组件来实现可滑动行为?

  8. 因为这是用户在移动 Web 应用程序中习惯的,以及他们学习 UI 控件的方式。

第二十二章

  1. Image组件的source属性接受哪些类型的值?

  2. 图像组件接受本地文件和远程图像 URL 的路径。

  3. 在图像加载时应该使用什么作为占位符?

  4. 您应该使用在图像使用的上下文中有意义的占位图像。

  5. 如何使用Image组件缩放图像?

  6. 通过设置widthheight属性,Image组件将自动处理图像的缩放。

  7. 安装react-native-vector-icons包值得吗?

  8. 是的,这个包为您的应用程序提供了数千个图标,并且图标是向用户传达意图的重要工具。

第二十三章

  1. 为什么AsyncStorage API 中的操作是异步的?

  2. 为了避免干扰 UI 的响应性。

  3. 您会使用哪个AsyncStorage API 来一次查找多个项目?

  4. AsyncStorage.getAllKeys()AsyncStorage.multiGet()的组合。

  5. 在 React Native 应用程序中,如何获取设备的连接状态?

  6. 您调用NetInfo.getConnectionInfo()并读取生成的连接类型。

  7. 在 React Native 应用程序中如何响应连接状态的变化?

  8. 您可以通过调用NetInfo.addEventListener('connectionChange', ...)来监听connectionChange事件。

第二十四章

  1. 以下哪项最能描述 Flux?

  2. Flux 是一种用于控制应用程序中数据单向流动的架构模式,使变化更加可预测。

  3. Flux 和 Redux 之间有什么区别?

  4. Redux 是 Flux 概念的一种有偏见的实现,您可以使用它来帮助管理应用程序中的数据流。

  5. 如何将 Redux 存储中的数据传递到您的组件中?

  6. 您使用connect()高阶函数将您的组件连接到存储,使用一个将存储数据转换为组件属性的函数。

  7. Redux 在 Web 应用程序和原生移动应用程序中有什么区别?

  8. 没有区别。

第二十五章

  1. Relay 和其他受 Flux 启发的库(如 Redux)之间有什么区别?

  2. Relay 通过允许数据依赖声明并隐藏所有服务器通信复杂性来帮助扩展您的 Flux 架构。

  3. Relay 如何简化 React 组件的数据需求?

  4. 通过合并数据依赖查询,您可以准确地看到您的组件使用的所有数据,而无需查看执行获取操作的代码。

  5. 在基于 Relay 的应用程序中,您的 React 组件如何与服务器通信?

  6. Relay 编译在您的组件中找到的 GraphQL 查询,并为您处理所有的 GraphQL 服务器通信,包括缓存优化。

posted @ 2024-05-16 14:50  绝不原创的飞龙  阅读(17)  评论(0编辑  收藏  举报