Skip to content

Latest commit

 

History

History
834 lines (630 loc) · 27.1 KB

File metadata and controls

834 lines (630 loc) · 27.1 KB

八、构建复杂 React 组件

在本章中,我们将通过构建应用中最复杂的组件,即Collection组件的子组件,将您迄今为止学到的关于 React 组件的所有知识付诸实践。本章中我们的目标是获得扎实的反应经验并培养我们的反应肌肉。让我们开始吧!

创建 TweetList 组件

如您所知,我们的Collection组件有两个子组件:CollectionControlsTweetList

我们将首先构建TweetList组件。创建以下~/snapterest/source/components/TweetList.js文件:

import React, { Component } from 'react';
import Tweet from './Tweet'; 
import TweetUtils from '../utils/TweetUtils';

const listStyle = {
  padding: '0'
};

const listItemStyle = {
  display: 'inline-block',
  listStyle: 'none'
};

class TweetList extends Component {

  getTweetElement = (tweetId) => {
    const { tweets, onRemoveTweetFromCollection } = this.props;
    const tweet = tweets[tweetId];
    let tweetElement;

    if (onRemoveTweetFromCollection) {
      tweetElement = (
        <Tweet
          tweet={tweet}
          onImageClick={onRemoveTweetFromCollection}
        />
      );
    } else {
      tweetElement = <Tweet tweet={tweet}/>;
    }

    return (
      <li style={listItemStyle} key={tweet.id}>
        {tweetElement}
      </li>
    );
  }

  render() {
    const tweetElements = TweetUtils
      .getListOfTweetIds()
      .map(this.getTweetElement);

    return (
      <ul style={listStyle}>
        {tweetElements}
      </ul>
    );
  }
}

export default TweetList;

TweetList组件呈现推文列表:

render() {
  const tweetElements = TweetUtils
    .getListOfTweetIds()
    .map(this.getTweetElement);

  return (
    <ul style={listStyle}>
      {tweetElements}
    </ul>
  );
}

首先,我们创建一个Tweet元素列表:

const tweetElements = TweetUtils
  .getListOfTweetIds()
  .map(this.getTweetElement);

TweetUtils.getListOfTweetIds()方法返回一个 tweet id 数组。

然后,对于该数组中的每个 tweetID,我们创建一个Tweet组件。为此,我们将在 tweet id 数组中调用map()方法,并将this.getTweetElement方法作为参数传递:

getTweetElement = (tweetId) => {
  const { tweets, onRemoveTweetFromCollection } = this.props;
  const tweet = tweets[tweetId];
  let tweetElement;

  if (onRemoveTweetFromCollection) {
    tweetElement = (
      <Tweet
        tweet={tweet}
        onImageClick={onRemoveTweetFromCollection}
      />
    );
  } else {
    tweetElement = <Tweet tweet={tweet} />;
  }

  return (
    <li style={listItemStyle} key={tweet.id}>
      {tweetElement}
    </li>
  );
}

getTweetElement()方法返回一个包裹在<li>元素中的Tweet元素。我们已经知道,Tweet组件有一个可选的onImageClick属性。我们希望何时提供此可选属性,何时不提供?

有两种情况。在第一个场景中,用户将单击 tweet 图像以将其从 tweet 集合中删除。在这个场景中,我们的Tweet组件将对click事件做出反应,因此我们需要提供onImageClick属性。在第二个场景中,用户将导出没有用户交互的 tweet 静态集合。在这种情况下,我们不需要提供onImageClick属性。

这正是我们在getTweetElement()方法中所做的:

const { tweets, onRemoveTweetFromCollection } = this.props;
const tweet = tweets[tweetId];
let tweetElement;

if (onRemoveTweetFromCollection) {
  tweetElement = (
    <Tweet
      tweet={tweet}
      onImageClick={onRemoveTweetFromCollection}
    />
  );
} else {
  tweetElement = <Tweet tweet={tweet}/>;
}

我们创建一个tweet常量,用于存储一条 tweet,其 ID 由tweetId参数提供。然后,我们创建一个常量来存储父Collection组件传递的this.props.onRemoveTweetFromCollection属性。

接下来,我们检查this.props.onRemoveTweetFromCollection属性是否由Collection组件提供。如果是,那么我们创建一个具有onImageClick属性的Tweet元素:

tweetElement = (
  <Tweet
    tweet={tweet}
    onImageClick={onRemoveTweetFromCollection}
  />
);

如果没有提供,那么我们创建一个没有handleImageClick属性的Tweet元素:

tweetElement = <Tweet tweet={tweet} />;

我们在以下两种情况下使用TweetList组件:

  • Collection组件中呈现 tweet 集合时使用此组件。在这种情况下,提供了onRemoveTweetFromCollection属性*。*
  • 在呈现表示Collection组件中 tweet 集合的 HTML 标记字符串时使用此组件。在这种情况下,不提供onRemoveTweetFromCollection属性*。*

一旦我们创建了Tweet元素并将其放入tweetElement变量中,我们将返回具有内联样式的<li>元素:

return (
  <li style={listItemStyle} key={tweet.id}>
    {tweetElement}
  </li>
);

除了style属性外,我们的<li>元素还有key属性。React 使用它来标识动态创建的每个子元素。我建议您在阅读更多关于动态儿童的内容 https://facebook.github.io/react/docs/lists-and-keys.html

这就是getTweetElement()方法的工作原理。因此,TweetList组件返回一个无序的Tweet元素列表:

return (
  <ul style={listStyle}>
    {tweetElements}
  </ul>
);

创建 CollectionControls 组件

现在,既然您理解了Collection组件呈现的内容,那么让我们讨论一下它的子组件。我们将从CollectionControls开始。创建以下~/snapterest/source/components/CollectionControls.js文件:

import React, { Component } from 'react';
import Header from './Header';
import Button from './Button';
import CollectionRenameForm from './CollectionRenameForm';
import CollectionExportForm from './CollectionExportForm';

class CollectionControls extends Component {
  state = {
    name: 'new',
    isEditingName: false
  };

  getHeaderText = () => {
    const { name } = this.state;
    const { numberOfTweetsInCollection } = this.props;
    let text = numberOfTweetsInCollection;

    if (numberOfTweetsInCollection === 1) {
      text = `${text} tweet in your`;
    } else {
      text = `${text} tweets in your`;
    }

    return (
      <span>
        {text} <strong>{name}</strong> collection
      </span>
    );
  }

  toggleEditCollectionName = () => {
    this.setState(prevState => ({
      isEditingName: !prevState.isEditingName
    }));
  }

  setCollectionName = (name) => {
    this.setState({
      name,
      isEditingName: false
    });
  }

  render() {
    const { name, isEditingName } = this.state;
    const {
      onRemoveAllTweetsFromCollection,
      htmlMarkup
    } = this.props;

    if (isEditingName) {
      return (
        <CollectionRenameForm
          name={name}
          onChangeCollectionName={this.setCollectionName}
          onCancelCollectionNameChange={this.toggleEditCollectionName}
        />
      );
    }

    return (
      <div>
        <Header text={this.getHeaderText()}/>

        <Button
          label="Rename collection"
          handleClick={this.toggleEditCollectionName}
        />

        <Button
          label="Empty collection"
          handleClick={onRemoveAllTweetsFromCollection}
        />

        <CollectionExportForm htmlMarkup={htmlMarkup} />
      </div>
    );
  }
}

export default CollectionControls;

顾名思义,CollectionControls组件呈现一个用户界面来控制集合。这些控件允许用户执行以下操作:

  • 重命名集合
  • 清空集合
  • 导出集合

收藏有一个名称。默认情况下,此名称为new,用户可以更改。集合名称显示在由CollectionControls组件呈现的标题中。此组件是存储集合名称的最佳候选组件,由于更改名称将需要组件重新呈现,我们将在组件的状态对象中存储该名称:

state = {
  name: 'new',
  isEditingName: false
};

CollectionControls组件可以呈现集合控制元素或表单来更改集合名称。用户可以在两者之间切换。我们需要一种表示这两种状态的方法,我们将使用isEditingName属性来实现这一目的。默认情况下,isEditingName设置为false;因此,当安装CollectionControls组件时,用户不会看到更改集合名称的表单。让我们来看一下它的方法:

render() {
  const { name, isEditingName } = this.state;
  const {
    onRemoveAllTweetsFromCollection,
    htmlMarkup
  } = this.props;

  if (isEditingName) {
    return (
      <CollectionRenameForm
        name={name}
        onChangeCollectionName={this.setCollectionName}
        onCancelCollectionNameChange={this.toggleEditCollectionName}
      />
    );
  }

  return (
    <div>
      <Header text={this.getHeaderText()}/>

      <Button
        label="Rename collection"
        handleClick={this.toggleEditCollectionName}
      />

      <Button
        label="Empty collection"
        handleClick={onRemoveAllTweetsFromCollection}
      />

      <CollectionExportForm htmlMarkup={htmlMarkup}/>
    </div>
  );
}

首先,我们检查组件状态的this.state.isEditingName属性是否设置为true。如果是,则CollectionControls组件返回呈现表单以更改集合名称的CollectionRenameForm组件:

<CollectionRenameForm
  name={name}
  onChangeCollectionName={this.setCollectionName}
  onCancelCollectionNameChange={this.toggleEditCollectionName}
/>

CollectionRenameForm组件呈现一个表单来更改集合名称。它接收三个属性:

  • name属性,该属性引用当前集合名称
  • 引用组件方法的onChangeCollectionNameonCancelCollectionNameChange属性

我们将在本章后面实现CollectionRenameForm组件。现在让我们来仔细研究一下。

setCollectionName = (name) => {
  this.setState({
    name,
    isEditingName: false
  });
}

setCollectionName()方法更新集合的名称,并隐藏一个表单,通过更新组件的状态来编辑集合名称。当用户提交新集合名称时,我们将调用此方法。

现在,让我们来看看 Apple T0.方法:

toggleEditCollectionName = () => {
  this.setState(prevState => ({
    isEditingName: !prevState.isEditingName
  }));
}

此方法通过使用!运算符将isEditingName属性设置为其当前布尔值的相反值来显示或隐藏集合的名称编辑表单。当用户点击重命名集合取消按钮时,我们将调用此方法,即显示或隐藏集合名称更改表单。

如果CollectionControls组件状态的this.state.isEditingName属性设置为false,则返回采集控件:

return (
  <div>
    <Header text={this.getHeaderText()}/>

    <Button
      label="Rename collection"
      handleClick={this.toggleEditCollectionName}
    />

    <Button
      label="Empty collection"
      handleClick={onRemoveAllTweetsFromCollection}
    />

    <CollectionExportForm htmlMarkup={htmlMarkup}/>
  </div>
);

我们将Header组件、两个Button组件和CollectionExportForm组件包装在div元素中。您已经熟悉上一章中的Header组件。它接收一个引用字符串的text属性。但是,在这种情况下,我们不直接传递字符串,而是调用this.getHeaderText()函数:

<Header text={this.getHeaderText()} />

依次,this.getHeaderText()返回一个字符串。让我们仔细研究一下这个方法:

getHeaderText = () => {
  const { name } = this.state;
  const { numberOfTweetsInCollection } = this.props;
  let text = numberOfTweetsInCollection;

  if (numberOfTweetsInCollection === 1) {
    text = `${text} tweet in your`;
  } else {
    text = `${text} tweets in your`;
  }

  return (
    <span>
      {text} <strong>{name}</strong> collection
    </span>
  );
}

此方法根据集合中的 tweet 数量为标题生成字符串。此方法的重要特性是,它不仅返回字符串,而且返回封装该字符串的 React 元素树。首先,我们创建numberOfTweetsInCollection常量。它存储一个集合中的 tweet 数量。然后我们创建一个text变量,并在一个集合中为其分配若干 tweets。此时,text变量存储一个整数值。我们的下一个任务是根据的整数值将正确的字符串连接到它:

  • 如果numberOfTweetsInCollection1,那么我们需要连接' tweet in your'
  • 否则,我们需要连接' tweets in your'

创建标题字符串后,我们将返回以下元素:

return (
  <span>
    {text} <strong>{name}</strong> collection
  </span>
);

封装在<span>元素中的最后一个字符串由text变量的值、集合名称和collection关键字组成;考虑这个例子:

1 tweet in your new collection.

一旦getHeaderText()方法返回该字符串,它将作为属性传递给Header组件。我们在CollectionControls组件render()方法中的下一个采集控制元素是Button

<Button
  label="Rename collection"
  handleClick={this.toggleEditCollectionName}
/>

我们将Rename collection字符串传递给它的label属性,this.toggleEditCollectionName方法传递给它的handleClick属性。因此,此按钮将具有Rename collection标签,并将切换表单以更改集合名称。

下一个采集控制元素是我们的第二个Button组件:

<Button
  label="Empty collection"
  handleClick={onRemoveAllTweetsFromCollection}
/>

正如您所猜测的,它将有一个Empty collection标签,并且它将从一个集合中删除所有推文。

我们的最终收集控制元素为CollectionExportForm

<CollectionExportForm htmlMarkup={htmlMarkup} />

此元素接收表示集合的 HTML 标记字符串,并将呈现一个按钮。我们将在本章后面创建此组件。

现在,由于您了解了组件 To0T0 将呈现什么,让我们更深入地研究它的子组件中的 T2。我们将从CollectionRenameForm组件开始。

创建 CollectionRenameForm 组件

首先,让我们创建这个~/snapterest/source/components/CollectionRenameForm.js文件:

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

const inputStyle = {
  marginRight: '5px'
};

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

    const { name } = props;

    this.state = {
      inputValue: name
    };
  }

  setInputValue = (inputValue) => {
    this.setState({
      inputValue
    });
  }

  handleInputValueChange = (event) => {
    const inputValue = event.target.value;
    this.setInputValue(inputValue);
  }

  handleFormSubmit = (event) => {
    event.preventDefault();

    const { onChangeCollectionName } = this.props;
    const { inputValue: collectionName } = this.state;

    onChangeCollectionName(collectionName);
  }

  handleFormCancel = (event) => {
    event.preventDefault();

    const {
      name: collectionName,
      onCancelCollectionNameChange
    } = this.props;

    this.setInputValue(collectionName);
    onCancelCollectionNameChange();
  }

  componentDidMount() {
    this.collectionNameInput.focus();
  }

  render() {
    const { inputValue } = this.state;

    return (
      <form className="form-inline" onSubmit={this.handleSubmit}>

        <Header text="Collection name:"/>
        <div className="form-group">
          <input
            className="form-control"
            style={inputStyle}
            onChange={this.handleInputValueChange}
            value={inputValue}
            ref={input => { this.collectionNameInput = input; }}
          />
        </div>

        <Button
          label="Change"
          handleClick={this.handleFormSubmit}
        />
        <Button
          label="Cancel"
          handleClick={this.handleFormCancel}
        />
      </form>
    );
  }
}

export default CollectionRenameForm;

此组件呈现一个表单以更改集合名称:

render() {
  const { inputValue } = this.state;

  return (
    <form className="form-inline" onSubmit={this.handleSubmit}>

      <Header text="Collection name:"/>
      <div className="form-group">
        <input
          className="form-control"
          style={inputStyle}
          onChange={this.handleInputValueChange}
          value={inputValue}
          ref={input => this.collectionNameInput = input}
        />
      </div>

      <Button
        label="Change"
        handleClick={this.handleFormSubmit}
      />
      <Button
        label="Cancel"
        handleClick={this.handleFormCancel}
      />
    </form>
  );
}

我们的<form>元素包含四个元素,如下所示:

  • 一个Header组件
  • 一个<input>元素
  • 两个Button组件

Header组件呈现"Collection name:"字符串。<input>元素被包装在<div>元素中,其className属性设置为form-group。这个名称是我们在第 5 章中讨论的引导框架的一部分,使您的 React 组件反应。它用于布局和样式设置,而不是 React 应用逻辑的一部分。

<input>元素有很多属性。让我们仔细研究一下:

<input
  className="form-control"
  style={inputStyle}
  onChange={this.handleInputValueChange}
  value={inputValue}
  ref={input => { this.collectionNameInput = input; }}
/>

以下是对上述代码中使用的属性的描述:

  • className属性设置为form-control。它是另一个类名,是引导框架的一部分。我们将使用它来设计样式。
  • 此外,我们使用style属性将自己的样式应用于这个input元素,该属性使用单一样式规则引用inputStyle对象,即marginRight
  • value属性设置为组件状态this.state.inputValue中存储的当前值。
  • onChange属性引用的handleInputValueChange方法是onchange事件处理程序。
  • ref属性是一种特殊的 React 属性,可以附加到任何组件。它采用回调函数,该函数将在安装和卸载组件后立即执行。它允许我们访问 React 组件呈现的 DOMinput元素。

我希望您关注最后三个属性:valueonChangerefvalue属性设置为组件状态的属性,更改该值的唯一方法是更新其状态。另一方面,我们知道用户可以与输入字段交互并更改其值。这种行为是否适用于我们的组件?不会。每当用户键入时,我们输入字段的值都不会更改。这是因为组件控制着<input>,而不是用户。在我们的CollectionRenameForm组件中,<input>的值始终反映this.state.inputValue属性的值,无论用户类型如何。用户不受控制,但CollectionRenameForm组件受控制。

那么,我们如何确保我们的输入字段对用户输入做出反应呢?我们需要监听用户输入,并更新CollectionRenameForm组件的状态,这反过来将使用更新的值重新呈现输入字段。在每个输入的change事件上这样做将使我们的输入看起来像往常一样工作,用户可以自由更改其值。

为此,我们为<input>元素提供了引用组件this.handleInputValueChange方法的onChange属性:

handleInputValueChange = (event) => {
  const inputValue = event.target.value;
  this.setInputValue(inputValue);
}

正如我们在第 4 章中所讨论的,创建第一个 React 组件,React 将SyntheticEvent的实例传递给事件处理程序。handleInputValueChange()方法接收具有target属性且具有value属性的event对象。此value属性存储用户在输入字段中键入的字符串。我们将该字符串传递到我们的this.setInputValue()方法中:

setInputValue = (inputValue) => {
  this.setState({
    inputValue
  });
}

setInputValue()方法是一种使用新输入值更新组件状态的方便方法。反过来,此更新将使用更新的值重新呈现<input>元素。

安装CollectionRenameForm组件时,初始输入值是多少?让我们来看看这个:

constructor(props) {
  super(props);

  const { name } = props;

  this.state = {
    inputValue: name
  };
}

如您所见,我们从父组件传递集合的名称,并使用它设置组件的初始状态。

挂载此组件后,我们希望在输入字段上设置焦点,以便用户可以立即开始编辑集合的名称。我们知道,一旦一个组件被插入到 DOM 中,React 就会调用它的componentDidMount()方法。此方法是我们设置focus的最佳时机:

componentDidMount() {
  this.collectionNameInput.focus();
}

为此,我们通过引用this.collectionNameInput来获取输入元素,并对其调用focus()函数。

我们如何在componentDidMount()方法中引用 DOM 元素?请记住,我们为我们的input 元素提供了ref属性。然后,我们将回调函数传递给该ref属性,该属性又将 DOM 输入元素的引用分配给this.collectionNameInput。现在我们可以通过访问this.collectionNameInput属性来获取该引用。

最后,让我们讨论两个表单按钮:

  • Change按钮提交表单并更改集合名称
  • Cancel按钮提交表单,但不更改集合名称

我们将从一个Change按钮开始:

<Button
  label="Change"
  handleClick={this.handleFormSubmit}
/>

当用户点击时,调用this.handleFormSubmit方法:

handleFormSubmit = (event) => {
  event.preventDefault();

  const { onChangeCollectionName } = this.props;
  const { inputValue: collectionName } = this.state;

  onChangeCollectionName(collectionName);
}

我们取消submit事件,然后从组件的状态获取集合名称,并将其传递给this.props.onChangeCollectionName()函数调用。onChangeCollectionName函数由父CollectionControls组件传递。调用此函数将更改集合的名称。

现在让我们讨论第二个表单按钮:

<Button
  label="Cancel"
  handleClick={this.handleFormCancel}
/>

当用户点击时,调用this.handleFormCancel方法:

handleFormCancel = (event) => {
  event.preventDefault();

  const {
    name: collectionName,
    onCancelCollectionNameChange
  } = this.props;

  this.setInputValue(collectionName);
  onCancelCollectionNameChange();
}

再次,我们取消一个submit事件,然后获取父CollectionControls组件作为属性传递的原始集合名称,并将其传递给我们的this.setInputValue()函数。然后,我们调用隐藏收集控件的this.props.onCancelCollectionNameChange()函数。

那是我们的CollectionRenameForm组件。接下来,让我们创建我们在CollectionRenameForm组件中重复使用了两次的Button组件。

创建按钮组件

创建以下~/snapterest/source/components/Button.js文件:

import React from 'react';

const buttonStyle = {
  margin: '10px 0'
};

const Button = ({ label, handleClick }) => (
  <button
    className="btn btn-default"
    style={buttonStyle}
    onClick={handleClick}
  >
    {label}
  </button>
);

export default Button;

Button组件呈现一个按钮。

注意,我们没有声明类,而是定义了一个名为Button的简单函数。这是创建 React 组件的功能方式。事实上,当组件的目的纯粹是渲染一些用户界面元素时,无论是否使用任何道具,建议您使用这种方法。

您可以将这个简单的 React 组件视为一个“纯”函数,它以props对象的形式接受输入,并一致地返回 JSX 作为输出,无论您调用该函数多少次。

理想情况下,大多数组件都应该以“纯”JavaScript 函数的方式创建。当然,当您的组件具有状态时,这是不可能的,但是对于所有无状态组件来说,这是一个机会!现在看看我们迄今为止创建的所有组件,看看是否可以将它们重写为“纯”函数,而不是使用类。

我建议您在上阅读更多关于功能组件和类组件的信息 https://facebook.github.io/r

您可能想知道,如果您可以只使用<button>元素,那么为按钮创建专用组件的好处是什么?将组件视为<button>元素的包装器以及它附带的其他东西。在我们的例子中,大多数<button>元素具有相同的样式,因此将<button>和样式对象封装在组件中并重用该组件是有意义的。因此,专用的Button组件。它希望从父组件接收两个属性:

  • label属性是按钮的标签
  • handleClick属性是一个回调函数,当用户单击此按钮时调用

现在,是时候创建我们的CollectionExportForm组件了。

创建 CollectionExportForm 组件

CollectionExportForm组件负责将收藏导出到第三方网站(http://codepen.io 。一旦你的收藏在 CodePen 上,你就可以保存它并与你的朋友分享。让我们来看看如何做到这一点。

创建~/snapterest/source/components/CollectionExportForm.js文件:

import React from 'react';

const formStyle = {
  display: 'inline-block'
};

const CollectionExportForm = ({ htmlMarkup }) => (
  <form
      action="http://codepen.io/pen/define"
      method="POST"
      target="_blank"
      style={formStyle}
    >
      <input type="hidden" name="data" value={htmlMarkup}/>
      <button type="submit" className="btn btn-default">
        Export as HTML
      </button>
    </form>
);

export default CollectionExportForm;

CollectionExportForm组件使用<input><button>元素呈现表单。<input>元素是隐藏的,其值设置为 HTML 标记字符串,父组件将作为htmlMarkup属性传递给该字符串。<button>元素是此表单中唯一用户可见的元素。当用户点击导出为 HTML按钮时,一个集合将提交给 CodePen,并在新窗口中打开。然后,用户可以修改并共享该集合。

祝贺至此,您已经使用 React 构建了一个功能齐全的 web 应用。让我们看看它是如何工作的。

首先,确保我们在第 2 章为您的项目安装强大工具中安装和配置的 Snapkite 引擎正在运行。导航到~/snapkite-engine/并运行以下命令:

npm start

然后,打开一个新的终端窗口,导航到~/snapterest/,并运行以下命令:

npm start

现在在您的 web 浏览器中打开~/snapterest/build/index.html。您将看到新的推文出现。单击它们以将其添加到您的收藏中。再次单击它们以从集合中删除单个 tweet。点击清空收藏按钮删除收藏中的所有推文。点击重命名收藏按钮,输入新收藏名称,点击更改按钮。最后,点击导出为 HTML按钮,将您的收藏导出到CodePen.io。如果您对本章或之前的章节有任何疑问,请转至https://github.com/fedosejev/react-essentials 并创建一个新的问题。

总结

在本章中,您创建了TweetListCollectionControlsCollectionRenameFormCollectionExportFormButton组件。您已完成构建一个功能齐全的 React 应用。

在下一章中,我们将使用 Jest 测试此应用,并使用 Flux 和 Redux 对其进行增强。