Skip to content

Latest commit

 

History

History
790 lines (546 loc) · 35.4 KB

File metadata and controls

790 lines (546 loc) · 35.4 KB

三、React 组件

在 Hello World 示例中,我们使用纯 JSX 创建了一个非常基本的 React 本地组件。然而,在现实世界中,您想要做的远不止一个简单的单线 JSX 所能做的。这就是 React 组件的用武之地。React 组件可以使用其他组件和基本 HTML 元素组成;它们可以响应用户输入、改变状态、与其他组件交互等等。

但是,在进入所有细节之前,让我首先描述一下我们将作为本书的一部分构建的应用。在本章以及后续章节中的每一步,我们将逐一介绍需要执行的应用或任务的特性,并解决它们。我喜欢这种方法,因为当我将它们立即投入使用时,我学到了最好的东西。这种方法不仅让你欣赏和内化概念,因为你把它们用上了,而且把更有用和实用的概念带到了最前面。

我想出的这个应用是大多数开发人员都能理解的。

问题跟踪器

我相信你们大多数人都熟悉 GitHub 问题或吉拉。这些应用帮助您创建一堆问题或错误,将它们分配给人们,并跟踪它们的状态。这些本质上是管理一系列对象或实体的 CRUD 应用(创建、读取、更新和删除数据库中的记录)。CRUD 模式非常有用,因为几乎所有的企业应用都是在不同的实体或对象上围绕 CRUD 模式构建的。

在问题跟踪器的情况下,我们将只处理单个对象或记录,因为这足以描述模式。一旦您掌握了如何在 MERN 实现 CRUD 模式的基本原理,您就能够复制该模式并创建一个真实的应用。

以下是问题跟踪应用的需求列表,这是 GitHub 问题或吉拉的简化或低调版本:

  • 用户应该能够查看问题列表,并能够通过各种参数过滤列表。

  • 用户应该能够通过提供问题字段的初始值来添加新问题。

  • 用户应该能够通过更改问题的字段值来编辑和更新问题。

  • 用户应该能够删除问题。

问题应具有以下属性:

  • 总结问题的标题(自由格式的长文本)

  • 向其分配问题的所有者(自由格式短文本)

  • 状态指示器(可能的状态值列表)

  • 创建日期(自动分配的日期)

  • 解决问题所需的努力(天数,一个数字)

  • 预计完成日期或到期日期(日期,可选)

请注意,我包含了不同类型的字段(列表、日期、数字、文本),以确保您了解如何处理不同的数据类型。我们将从简单开始,一次构建一个特性,并在过程中了解 MERN 堆栈。

在本章中,我们将创建 React 类并实例化组件。我们还将通过组合较小的组件来创建较大的组件。最后,我们将在这些组件之间传递数据,并根据数据动态创建组件。就功能而言,本章的目标是展示问题跟踪器的主页:问题列表。我们将对用于显示页面的数据进行硬编码,并将从服务器检索数据的工作留到下一章。

React 类

在这一节中,我们的目标是将单行 JSX 转换成一个从 React 类实例化的简单 React 组件,以便我们稍后可以使用第一个类 React 组件的全部功能。

React 类用于创建真正的组件(与模板化的 HTML 相反,在模板化的 HTML 中,我们基于变量创建 Hello World 消息,这是我们在上一章中创建的)。这些类可以在其他组件中重用,处理事件等等。首先,让我们用一个简单的类代替 Hello World 示例,该类构成了问题跟踪器应用的起点。

React 类是通过扩展React.Component创建的,所有定制类都必须从这个基类派生。在类定义中,至少需要一个render()方法。当 React 需要在 UI 中显示组件时,它调用这个方法。

还有其他一些对 React 有特殊意义的方法可以实现,称为生命周期方法。这些提供了组件形成和其他事件的不同阶段的挂钩。我们将在后面的章节中讨论其他的生命周期函数。但是render()必须存在的一个,否则组件将没有屏幕存在。render()函数应该返回一个元素(可以是一个本地 HTML 元素,比如<div>,也可以是另一个 React 组件的实例)。

让我们将 Hello World 示例从一个简单的元素改为使用一个名为HelloWorld的 React 类,它是从React.Component扩展而来的:

...
class HelloWorld extends React.Component {
  ...
}
...

注意

我们使用 ES2015 class关键字和extends关键字来定义一个 JavaScript 类。React 建议使用 ES2015 类。如果您不熟悉 JavaScript 类,请阅读并了解从 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes 开始的类。

现在,在这个类中,需要一个render()方法,它应该返回一个元素。我们将使用与消息相同的 JSX <div>作为返回的元素。

...
  render() {
    return (
      <div title="Outer div">
        <h1>{message}</h1>
      </div>
    );
...

让我们也将所有用于消息构造的代码移到render()函数中,这样它仍然封装在需要的范围内,而不是污染全局名称空间。

...
  render() {
    const continents = ['Africa','America','Asia','Australia','Europe'];
    const helloContinents = Array.from(continents, c => `Hello ${c}!`);
    const message = helloContinents.join(' ');

    return (
      ...
    );
...

本质上,JSX 元素现在是从名为 Hello World 的组件类的render()方法返回的。Hello World 元素的 JSX 表示形式周围的括号不是必需的,但这是一种惯例,通常用于使代码更具可读性,尤其是当 JSX 跨越多行时。

正如使用形式为<div></div>的 JSX 创建一个div元素的实例一样,HelloWorld类的实例也可以这样创建:

...
const element = <HelloWorld />;
...

现在,这个元素可以用来代替<div>元素,在名为contents的 Node 中进行渲染,就像以前一样。这里值得注意的是,divh1是内置的 React 组件或元素,可以直接实例化。而HelloWorld是我们定义并随后实例化的东西。并且在HelloWorld内,我们使用了 React 内置的div组件。清单 3-1 中显示了新的变更后的App.jsx

将来,我可能会互换使用组件和组件类,就像有时我们倾向于使用类和对象一样。但是现在应该很明显,HelloWorlddiv实际上是 React 组件类,而<HelloWorld /><div />是组件类的有形组件或实例。不用说,只有一个HelloWorld类,但是基于这个类可以实例化许多HelloWorld组件。

class HelloWorld extends React.Component {
  render() {
    const continents = ['Africa','America','Asia','Australia','Europe'];
    const helloContinents = Array.from(continents, c => `Hello ${c}!`);
    const message = helloContinents.join(' ');

    return (
      <div title="Outer div">
        <h1>{message}</h1>
      </div>
    );
  }
}

const element = <HelloWorld />;

ReactDOM.render(element, document.getElementById('contents'));

Listing 3-1App.jsx: A Simple React Class and Instance

到现在为止,您应该在一个控制台中运行npm run watch,并且在一个单独的控制台中使用npm start启动服务器。因此,对App.jsx的任何修改都应该被自动编译。因此,如果你刷新你的浏览器,你应该看到所有大洲的问候,就像以前一样。

练习:React 类

  1. render函数中,不是返回一个<div>,而是尝试返回两个前后放置的<div>元素。会发生什么?为什么,解决方案是什么?确保你看着控制台运行npm run watch

  2. 通过将字符串'contents'更改为'main'或其他不能识别 HTML 中元素的字符串,为 React 库创建一个运行时错误。从哪里可以看出错误?像未定义的变量引用这样的 JavaScript 运行时错误怎么办?

本章末尾有答案。

构成组件

在上一节中,您看到了如何通过将内置的 React 组件(相当于 HTML 元素)放在一起来构建组件。也可以构建一个使用其他用户定义组件的组件,这就是我们将在本节中探讨的内容。

组件组合是 React 最强大的特性之一。这样,UI 可以被分割成更小的独立部分,这样每个部分都可以独立地编码和推理,从而更容易构建和理解复杂的 UI。使用组件而不是以整体的方式构建 UI 也鼓励重用。我们将在后面的章节中看到我们构建的组件是如何被轻松重用的,即使我们在构建组件的时候没有想到重用。

组件接受输入(称为属性),其输出是组件的呈现 UI。在本节中,我们将不使用输入,而是将细粒度的组件放在一起构建一个更大的 UI。在编写组件时,需要记住以下几点:

  • 当细粒度组件之间可能存在逻辑分离时,应该将较大的组件拆分为细粒度组件。在本节中,我们将创建逻辑上分离的组件。

  • 当有重用的机会时,可以构建从不同调用者接受不同输入的组件。当我们在第 10 章中为用户输入构建专门的部件时,我们将创建可重用的组件。

  • React 的哲学更喜欢组件组合,而不是继承。例如,现有组件的专门化可以通过将属性传递给通用组件而不是从它继承来完成。你可以在 https://reactjs.org/docs/composition-vs-inheritance.html 了解更多信息。

  • 一般来说,记住将组件之间的耦合保持在最低限度(耦合是指一个组件需要知道另一个组件的细节,包括它们之间传递的参数或属性)。

让我们设计应用的主页来显示问题列表,并能够过滤问题和创建新问题。因此,它将包含三个部分:一个用于选择显示哪些问题的过滤器、问题列表,以及最后一个用于添加问题的条目表单。我们现在关注的是组成组件,所以我们将只为这三个部分使用占位符。用户界面的结构和层次如图 3-1 所示。

img/426054_2_En_3_Chapter/426054_2_En_3_Fig1_HTML.jpg

图 3-1

问题列表页面的结构

让我们定义三个占位符类——IssueFilterIssueTableIssueAdd——每个类中的<div>内只有一个占位符文本。IssueFilter组件将如下所示:

...
class IssueFilter extends React.Component {
  render() {
    return (
      <div>This is a placeholder for the issue filter.</div>
    );
  }
}
...

另外两个类——IssueTableIssueAdd——将是相似的,各有不同的占位符消息:

...
class IssueTable extends React.Component {
    ...
      <div>This is a placeholder for a table of issues.</div>
...
class IssueAdd extends React.Component {
    ...
      <div>This is a placeholder for a form to add an issue.</div>
...

为了将这些放在一起,让我们删除 Hello World 类,并添加一个名为IssueList的类。

...
class IssueList extends React.Component {
}
...

现在让我们添加一个render()方法。在这个方法中,让我们添加每个新占位符类的一个实例,用一条<hr>或水平线分隔。正如您在前面部分的练习中看到的,由于render()的返回必须是一个单一元素,所以这些元素必须包含在<div>或 React Fragment组件中。组件Fragment就像一个封闭的<div>,但是它对 DOM 没有影响。

让我们在IssueListrender()方法中使用这样一个Fragment组件:

...
  render() {
    return (
      <React.Fragment>
        <h1>Issue Tracker</h1>
        <IssueFilter />
        <hr />
        <IssueTable />
        <hr />
        <IssueAdd />
      </React.Fragment>
    );
  }
...

最后,让我们实例化IssueList类,而不是实例化一个HelloWorld类,我们将把它放在contents div 下。

...

const element = <HelloWorld />;

const element = <IssueList />;

...

理想情况下,每个组件都应该作为一个独立的文件来编写。但是目前,我们只有占位符,所以为了简洁起见,我们将所有的类保存在同一个文件中。此外,您还没有学会如何将多个类文件放在一起。在后面的阶段,当类扩展到它们的实际内容,并且我们也有方法从另一个类构建或引用一个类时,我们将把它们分离出来。

清单 3-2 显示了带有所有组件类的App.jsx文件的新内容。

class IssueFilter extends React.Component {
  render() {
    return (
      <div>This is a placeholder for the issue filter.</div>
    );
  }
}

class IssueTable extends React.Component {
  render() {
    return (
      <div>This is a placeholder for a table of issues.</div>
    );
  }
}

class IssueAdd extends React.Component {
  render() {
    return (
      <div>This is a placeholder for a form to add an issue.</div>
    );
  }
}

class IssueList extends React.Component {
  render() {
    return (
      <React.Fragment>
        <h1>Issue Tracker</h1>
        <IssueFilter />
        <hr />
        <IssueTable />
        <hr />
        <IssueAdd />
      </React.Fragment>
    );
  }
}

const element = <IssueList />;

ReactDOM.render(element, document.getElementById('contents'));

Listing 3-2App.jsx: Composing Components

这段代码的效果将是一个无趣的页面,如图 3-2 所示。

img/426054_2_En_3_Chapter/426054_2_En_3_Fig2_HTML.jpg

图 3-2

通过组合组件跟踪问题

练习:组成组件

  1. 在开发人员控制台中检查 DOM。您看到任何对应于React.Fragment组件的 HTML 元素了吗?与使用一个<div>元素来封装各种元素相比,您认为这有什么用?

本章末尾有答案。

使用属性传递数据

组成没有任何变量的组件并不那么有趣。应该可以将不同的输入数据从父组件传递到子组件,并在不同的实例上进行不同的呈现。在问题跟踪器应用中,可以用不同输入实例化的一个这样的组件是显示单个问题的表行。根据不同的输入(问题),该行可以显示不同的数据。新的 UI 结构如图 3-3 所示。

img/426054_2_En_3_Chapter/426054_2_En_3_Fig3_HTML.jpg

图 3-3

带有问题行的问题列表 UI 层次结构

因此,让我们创建一个名为IssueRow的组件,然后在IssueTable中多次使用它,传入不同的数据来显示不同的问题,就像这样:

...
class IssueTable extends React.Component {
  render() {
    return (
      <table>
        <thead>
          <tr>
            <th>ID</th>
            <th>Title</th>
          </tr>
        </thead>
        <tbody>
          <IssueRow /> {/* somehow pass Issue 1 data to this */}
          <IssueRow /> {/* somehow pass Issue 2 data to this */}
        </tbody>
      </table>
    );
  }
}
...

注意

JSX 本身不支持注释。为了添加注释,必须添加一个具有 JavaScript 样式注释的 JavaScript 片段。因此,表单{/* ... */}可用于在 JSX 内放置评论。像<!-- ... -->这样使用 HTML 风格的注释是行不通的。

事实上,在任何 JSX 代码片段中切换到 JavaScript 世界的方法就是使用花括号。在前一章中,我们用它来显示 Hello World 消息,这是一个使用语法{message}名为message的 JavaScript 变量。

将数据传递给子组件的最简单方法是在实例化组件时使用属性。我们在上一章中使用了title属性,但这是一个最终影响 DOM 元素的属性。任何自定义属性也可以以类似的方式从IssueTable传递:

...
    <IssueRow issue_title="Title of the first issue" />
...

我们使用名称issue_title而不是简单的title来避免这个自定义属性和 HTML title属性之间的混淆。现在,在孩子的render()方法中,属性的值可以通过一个叫做props的特殊对象变量来访问,这个变量可以通过this访问器获得。例如,issue_title的值是如何在IssueRow组件的单元格中显示的:

...
    <td>{this.props.issue_title}</td>
...

在本例中,我们传递了一个简单的字符串。其他数据类型甚至 JavaScript 对象都可以这样传递。通过使用花括号({})而不是引号,可以传递任何 JavaScript 表达式,因为花括号切换到 JavaScript 世界。

因此,让我们将问题的标题(作为字符串)、ID(作为数字)和行样式(作为对象)从IssueTable传递到IssueRow。在IssueRow类中,我们将使用这些传入的属性来显示 ID 和标题,并通过this.props访问这些属性来设置行的样式。

清单 3-3 中显示了完整的IssueRow类的代码。

class IssueRow extends React.Component {
  render() {
    const style = this.props.rowStyle;
    return (
      <tr>
        <td style={style}>{this.props.issue_id}</td>
        <td style={style}>{this.props.issue_title}</td>
      </tr>
    );
  }
}

Listing 3-3App.jsx: IssueRow Component, Accessing Passed-in Properties

我们为表格单元格使用了属性style,就像我们在常规 HTML 中使用它一样。但是请注意,这并不是真正的 HTML 属性。相反,它是一个被传递给内置 React 组件<td>属性。只是将td组件中的style属性解释并设置为 HTML style属性。大多数情况下,像style一样,属性的名称与 HTML 属性相同,但对于少数引起与 JavaScript 保留字冲突的属性,命名要求不同。因此,在 JSX,class HTML 属性需要是className。此外,HTML 属性中的连字符需要替换为骆驼大小写的名称,例如,max-length在 JSX 变成了maxLength

在位于 https://reactjs.org/docs/dom-elements.html 的 React 文档中可以找到 DOM 元素的完整列表以及如何指定这些元素的属性。

现在我们有一个IssueRow组件接收属性,让我们从父组件IssueTable传递它们。ID 和标题都很简单,但是我们需要传递的样式在 React 和 JSX 中有特殊的规范约定。

React 不需要 CSS 类型的字符串,而是需要将其指定为具有特定约定的对象,该约定包含一系列 JavaScript 键值对。这些键与 CSS 样式名相同,除了它们不是破折号(如border-collapse),而是骆驼大小写(如borderCollapse)。这些值是 CSS 样式值,就像在 CSS 中一样。指定像素值也有一种特殊的简写方式;你可以只用一个数字(比如 4)来代替字符串"4px"

让我们给这些行一个像素的银色边框和一些填充,比如说四个像素。封装该规范的样式对象如下:

...
    const rowStyle = {border: "1px solid silver", padding: 4};
...

这可以在实例化时使用rowStyle={rowStyle}传递给IssueRow组件。这个和其他变量可以传递给IssueRow,同时像这样实例化它:

...
<IssueRow rowStyle={rowStyle} issue_id={1}
  issue_title="Error in console when clicking Add" />
...

注意,我们没有对问题 ID 使用类似字符串的引号,因为它是一个数字,也没有对rowStyle使用类似字符串的引号,因为它是一个对象。我们使用花括号,这使得它成为一个 JavaScript 表达式。

现在,让我们构造IssueTable组件,它本质上是一个<table>,有一个标题行和两列(ID 和 title),以及两个硬编码的IssueRow组件。让我们也为表格指定一个内联样式来指示折叠的边框,并使用相同的rowStyle变量来指定标题行样式,使其看起来一致。

清单 3-4 显示了修改后的IssueTable组件类。

class IssueTable extends React.Component {
  render() {
    const rowStyle = {border: "1px solid silver", padding: 4};
    return (
      <table style={{borderCollapse: "collapse"}}>
        <thead>
          <tr>
            <th style={rowStyle}>ID</th>
            <th style={rowStyle}>Title</th>
          </tr>
        </thead>
        <tbody>
          <IssueRow rowStyle={rowStyle} issue_id={1}
            issue_title="Error in console when clicking Add" />
          <IssueRow rowStyle={rowStyle} issue_id={2}
            issue_title="Missing bottom border on panel" />
        </tbody>
      </table>
    );
  }
}

Listing 3-4App.jsx: IssueTable Passing Data to IssueRow

3-4 显示了代码中这些变化的影响。

img/426054_2_En_3_Chapter/426054_2_En_3_Fig4_HTML.jpg

图 3-4

将数据传递给子组件

练习:使用属性传递数据

  1. 尝试为表格添加一个属性border=1,就像我们在普通 HTML 中做的那样。会发生什么?为什么呢?提示:阅读 React API 参考的“DOM Elements”一节中标题为“所有支持的 HTML 属性”的部分。

  2. 为什么表格的内嵌样式中有一个双花括号?提示:与另一种风格相比,我们声明了一个变量并使用它,而不是内联指定它。

  3. 花括号是在 JSX 标记中间转义成 JavaScript 的一种方式。将这与 PHP 等其他模板语言中的类似技术进行比较。

本章末尾有答案。

使用子 Node 传递数据

还有另一种方法将数据传递给其他组件,即使用组件的类似 HTML 的 Node 的内容。在子组件中,可以使用名为this.props.children的特殊字段this.props来访问它。

就像在常规 HTML 中一样,React 组件可以嵌套。在 Hello World 示例中,我们在一个<div>中嵌套了一个<h1>元素。当组件被转换为 HTML 元素时,元素以相同的顺序嵌套。React 组件可以像<div>一样工作,接受嵌套元素。在这种情况下,JSX 表达式将需要包含开始和结束标记,并在其中嵌套元素。

但是,当父 React 组件渲染时,子组件不会自动位于其下,因为父 React 组件的结构需要确定子组件将出现的确切位置。因此,React 让父组件使用this.props.children访问子元素,并让父组件决定它需要显示在哪里。当需要将其他组件包装在父组件中时,这非常有用。例如,添加边框和填充的包装器<div>可以这样定义:

...
class BorderWrap extends React.Component {
  render() {
    const borderedStyle = {border: "1px solid silver", padding: 6};
    return (
      <div style={borderedStyle}>
        {this.props.children}
      </div>
    );
  }
}
...

然后,在呈现过程中,任何组件都可以用填充的边框包装,如下所示:

...
    <BorderWrap>
      <ExampleComponent />
    </BorderWrap>
...

因此,可以使用这种技术将其作为< IssueRow >的子内容嵌入,而不是将问题标题作为属性传递给IssueRow,如下所示:

...
  <IssueRow issue_id={1}>Error in console when clicking Add</IssueRow>
...

现在,在IssueRowrender()方法中,它将需要被称为this.props.children,而不是被称为this.props.issue_title,就像这样:

...
   <td style={borderedStyle}>{this.props.children}</td>
...

让我们修改应用,使用这种将数据从IssueTable传递到IssueRow的方法。让我们也传入一个嵌套的标题元素作为子元素,它是一个<div>,包含一段强调的文本。这一变化如清单 3-5 所示。

...
class IssueRow extends React.Component {
...
    return (
      <tr>
        <td style={style}>{this.props.issue_id}</td>
        <td style={style}>{this.props.issue_title}</td>
        <td style={style}>{this.props.children}</td>
      </tr>
    );
...
}
...
...
class IssueTable extends React.Component {
...
        <tbody>
          <IssueRow rowStyle={rowStyle} issue_id={1}
            issue_title="Error in console when clicking Add" />
          <IssueRow rowStyle={rowStyle} issue_id={2}
            issue_title="Missing bottom border on panel" />
          <IssueRow rowStyle={rowStyle} issue_id={1}>
            Error in console when clicking Add
          </IssueRow>
          <IssueRow rowStyle={rowStyle} issue_id={2}>
            <div>Missing <b>bottom</b> border on panel</div>
          </IssueRow>
        </tbody>
...

Listing 3-5App.jsx: Using Children Instead of Props

这些变化对输出的影响很小,只在第二期的标题中看到一点点格式。如图 3-5 所示。

img/426054_2_En_3_Chapter/426054_2_En_3_Fig5_HTML.jpg

图 3-5

将数据传递给子组件

练习:使用子 Node 传递数据

  1. 什么时候以propschildren的形式传递数据比较合适?提示:想想我们想要传递的是什么。

本章末尾有答案。

动态构图

在这一节中,我们将用一系列问题中以编程方式生成的组件集替换我们的硬编码组件集IssueRow。在后面的章节中,我们将通过从数据库获取问题列表来变得更加复杂,但是现在,我们将使用一个简单的内存 JavaScript 数组来存储问题列表。

让我们将问题的范围从仅仅一个 ID 和一个标题扩展到尽可能多的领域。清单 3-6 展示了这个内存数组,它在文件App.jsx的开头被全局声明。它只有两个问题。字段due在第一条记录中未定义,以确保我们处理这是一个可选字段的事实。

const issues = [
  {
    id: 1, status: New', owner: 'Ravan', effort: 5,
    created: new Date('2018-08-15'), due: undefined,
    title: 'Error in console when clicking Add',
  },
  {
    id: 2, status: 'Assigned', owner: 'Eddie', effort: 14,
    created: new Date('2018-08-16'), due: new Date('2018-08-30'),
    title: 'Missing bottom border on panel',
  },
];

Listing 3-6App.jsx

: In-Memory Array of Issues

您可以添加更多的示例问题,但两个问题足以演示动态合成。现在,让我们修改IssueTable类来使用这个问题数组,而不是硬编码的列表。在IssueTable class' render()方法中,让我们遍历问题数组,并从中生成一个IssueRows数组。

为此,Arraymap()方法很方便,因为我们可以将一个问题对象映射到一个IssueRow实例。此外,让我们传递issue对象本身,而不是将每个字段作为属性传递,因为有许多字段作为对象的一部分。这是一种在表体中就地实现的方法:

...
    <tbody>
      {issues.map(issue => <IssueRow rowStyle={rowStyle} issue={issue}/>)}
    </tbody>
...

如果你想使用一个for循环而不是map()方法,你不能在 JSX 中这样做,因为 JSX 并不是真正的模板语言。它只允许花括号内的 JavaScript 表达式。我们必须在render()方法中创建一个变量,并在 JSX 中使用它。出于可读性考虑,我们还是这样为问题行集创建变量:

...
  const issueRows = issues.map(issue => <IssueRow rowStyle={rowStyle} issue={issue}/>);
...

现在,我们可以将IssueTable中的两个硬编码问题组件替换为<tbody>元素中的这个变量,如下所示:

...
    <tbody>
      {issueRows}
    </tbody>
...

在其他框架和模板语言中,使用模板创建多个元素需要在模板语言中使用特殊的for循环结构(例如 AngularJS 中的ng-repeat)。但是在 React 中,常规 JavaScript 可以用于所有编程结构。这不仅为您提供了 JavaScript 操纵模板的全部能力,还减少了您需要学习和记忆的结构数量。

IssueTable类中的标题行现在需要为每个问题字段提供一列,所以让我们也这样做。但是现在,为每个单元格指定样式变得很乏味,所以让我们为表格创建一个类,将其命名为table-bordered,并使用 CSS 来为表格和每个表格单元格设置样式。这种风格需要成为index.html的一部分,清单 3-7 显示了对该文件的修改。

...
  <script src="https://unpkg.com/@babel/polyfill@7/dist/polyfill.min.js"></script>
  <style>
    table.bordered-table th, td {border: 1px solid silver; padding: 4px;}
    table.bordered-table {border-collapse: collapse;}
  </style>
</head>
...

Listing 3-7index.html: Styles for Table Borders

现在,我们可以从所有的表格单元格和表格标题中删除rowStyle。需要做的最后一件事是用一个名为key的属性来标识IssueRow的每个实例。这个键的值可以是任何值,但是它必须唯一地标识一行。React 需要这个key,这样它就可以在情况发生变化时优化差异的计算,例如,当插入一个新行时。我们可以使用问题的 ID 作为键,因为它唯一地标识了行。

清单 3-8 显示了最终的IssueTable类,它包含一组动态生成的IssueRow组件和修改后的头部。

class IssueTable extends React.Component {
  render() {
    const issueRows = issues.map(issue =>
      <IssueRow key={issue.id} issue={issue} />
    );

    return (
      <table className="bordered-table">
        <thead>
          <tr>
            <th>ID</th>
            <th>Status</th>
            <th>Owner</th>
            <th>Created</th>
            <th>Effort</th>
            <th>Due Date</th>
            <th>Title</th>
          </tr>
        </thead>
        <tbody>
          {issueRows}
        </tbody>
      </table>
    );
  }
}

Listing 3-8App.jsx: IssueTable Class with IssueRows Dynamically Generated and Modified Header

IssueRow的变化相当简单。必须删除内联样式,还需要添加几列,每个添加的字段一列。因为 React 不会在要显示的对象上自动调用toString(),所以日期必须显式地转换为字符串。toString()方法会产生一个很长的字符串,所以让我们用toDateString()来代替。由于字段due是可选的,我们需要在调用字段toDateString()之前检查它是否存在。一种简单的方法是在如下表达式中使用三元运算符? - ::

...
  issue.due ? issue.due.toDateString() : ''
...

三元运算符非常方便,因为它是一个 JavaScript 表达式,可以直接用来代替显示字符串。否则,要使用if-then-else语句,代码必须在 JSX 部分之外,在render()方法实现的开始。新的IssueRow类如清单 3-9 所示。

class IssueRow extends React.Component {
  render() {
    const issue = this.props.issue;
    return (
      <tr>
        <td>{issue.id}</td>
        <td>{issue.status}</td>
        <td>{issue.owner}</td>
        <td>{issue.created.toDateString()}</td>
        <td>{issue.effort}</td>
        <td>{issue.due ? issue.due.toDateString() : ''}</td>
        <td>{issue.title}</td>
      </tr>
    );
  }
}

Listing 3-9App.jsx: New IssueRow Class Using Issue Object Property

经过这些更改后,屏幕应该如图 3-6 所示。

img/426054_2_En_3_Chapter/426054_2_En_3_Fig6_HTML.jpg

图 3-6

发出从数组以编程方式构造的行

练习:动态构图

  1. 我们使用问题的id字段作为键值。还有什么其他的钥匙可以用?你会选择哪一个?

  2. 在上一节中,我们将问题的每个字段作为单独的属性传递给了IssueRow。在本节中,我们传递了整个问题对象。为什么呢?

  3. 不要使用局部变量issueRows,尝试直接在<tbody>中使用映射表达式。有用吗?它告诉我们什么?

本章末尾有答案。

摘要

在本章中,我们创建了问题跟踪器主页的一个准系统版本。我们开始使用 React 类,而不是简单的元素,其中一些只是占位符,用来描述我们尚未开发的组件。我们通过编写细粒度的单个组件并将它们放在一起(组合)到一个封闭组件中来实现这一点。我们还将参数或数据从封装组件传递到其子组件,从而重用组件类并使用不同的数据对其进行不同的呈现,动态地使用map()来基于输入数据的数组生成组件。

这些组件除了根据输入数据呈现自己之外,没有做太多事情。在下一章,我们将看到用户交互如何影响数据和改变组件的外观。

练习答案

练习:React 类

  1. 编译将失败,并出现错误“相邻的 JSX 元素必须用封闭标记括起来”。render()方法只能有一个返回值,因此,它只能返回一个元素。将两个<div>放在另一个< div >中是一种解决方案,或者如错误消息所示,使用一个Fragment组件是另一种解决方案,我们将在后面的章节中讨论。

  2. 如果是 React 错误,React 会在浏览器的 JavaScript 控制台中打印错误。控制台中也会显示常规的 JavaScript 错误,但显示的代码不是原代码;就是编译好的代码。我们将在后面的章节中学习如何使用原始源代码进行调试。

练习:组成组件

  1. 不,没有封闭元素。由IssueList返回的所有元素都直接出现在contents div 下。在这种情况下,我们可以很容易地使用一个<div>来包含元素。

    But imagine a situation where a list of table-rows needs to be returned, like this:

    ...
      <tr> {/* contents of row 1 */} </tr>
      <tr> {/* contents of row 2 */} </tr>
    ...

    然后,调用组件将这些行放在一个<tbody>元素下。添加一个<div>来包含这些行会导致无效的 DOM 树,因为<tbody>中不能有<div>。在这种情况下,碎片是唯一的选择。

练习:使用属性传递数据

  1. 将不显示边框。React 解释每个元素属性的方式与 HTML 解析器不同。边框属性不是受支持的属性之一。React 完全忽略了 border 属性。

  2. 外面的大括号表示属性值是一个 JavaScript 表达式。内部大括号指定一个对象,它是属性的值。

  3. React 的花括号和 PHP 的<?php ... ?>类似,略有区别。标签内的内容是成熟的程序,而在 JSX,你只能有 JavaScript 表达式。所有像for循环这样的编程结构都是在 JSX 之外用普通 JavaScript 编写的。

练习:使用子 Node 传递数据

  1. 对于传递任何类型的数据都很灵活和有用。另一方面,children只能是一个*元素,*也可以深度嵌套。因此,如果您有简单的数据,就将其作为props传递。如果您要传递一个组件,如果它嵌套很深并且自然地出现在子组件中,您可以使用children。组件也可以作为props来传递,通常是当您想要传递多个组件或者组件不是父组件的自然子内容时。

练习:动态构图

  1. 属性的另一个选择是数组索引,因为它也是惟一的。如果键是一个像 UUID 这样的大值,您可能会认为使用数组索引更有效,但实际上并非如此。React 使用键识别该行。如果它找到了相同的键,它就假定这是同一行。如果该行没有更改,它不会重新呈现该行。

    因此,如果插入一行,如果行的键是对象的 ID,React 将更有效地移动现有的行,而不是重新呈现整个表。如果使用数组索引,它会认为插入行之后的每一行都已更改,并重新呈现每一行。

  2. 传递整个对象显然更简洁。只有当被传递的属性数量是对象的全部属性的一个小的子集时,我才会选择传递单个属性。

  3. 它是有效的,尽管事实上我们在表达式中有 JSX。花括号内的任何内容都被解析为 JavaScript 表达式。但是因为我们在 JavaScript 表达式上使用了 JSX 变换,所以这些片段也将经过变换。这是可能的嵌套更深,并使用另一套花括号内嵌套的 JSX,等等。