第 5 章 让您的 React 组件具有响应式

第 5 章 让您的 React 组件具有响应式

Flying
2018-09-09 / 0 评论 / 413 阅读 / 正在检测是否收录...

现在你知道了如何创建有状态和无状态的 React 组件,我们可以开始将 React 组件组合在一起构建更复杂的用户界面。事实上,现在是我们开始构建名为 Snapterest 的 web 应用的时候了,我们在第 2 章为你的项目安装强大的工具中讨论了这一点。在此过程中,你将学习如何规划 React 应用并创建可组合的 React 组件。让我们开始吧。

使用 React 解决问题

在开始为 web 应用编写代码之前,需要考虑 web 应用将要解决的问题。最重要的是要理解,尽早明确地定义问题是成功解决方案的最重要一步——一个有用的 web 应用。如果你未能在开发过程的早期定义问题,或者定义的不准确,那么稍后将不得不停止,重新思考你正在做的事情,扔掉你已经编写的一段代码,然后重新编写一段代码。这是一种浪费的方法,作为一名专业的软件开发人员,你的时间不仅对你非常宝贵,而且对你的组织也非常宝贵,因此明智地进行投资符合你的最佳利益。在本书的前面,我强调了使用 React 的好处之一是代码重用,这意味着你可以在更短的时间内完成更多的工作。然而,在我们查看 React 代码之前,让我们先讨论一下这个问题。

我们将构建 Snapterest,这是一个 web 应用,它可以实时从 Snapkite-engine 服务器接收推文,并一次向用户显示一条推文。我们实际上不知道 Snapterest 何时会收到一条新的推文,但当它收到时,它将显示该条新推文至少 1.5 秒,以便用户有足够的时间查看并点击它。点击一条推文会将其添加到现有的推文集合或创建一个新的推文。最后,用户将能够将其集合导出为 HTML 标签代码。

这是对我们将要构建的内容非常高级的描述。让我们将其分解为较小的任务列表:

snapterest-steps.jpg

步骤如下:

  1. 从 Snapkite-engine 服务器实时接收推文。
  2. 每次显示一条推文至少 1.5 秒。
  3. 用户单击事件时添加推文集合。
  4. 显示集合中的推文列表。
  5. 为集合创建 HTML 标签代码并将其导出。
  6. 用户单击事件时从集合中删除推文。

你能确定哪些任务可以使用 React 解决吗?请记住,React 是一个用户界面库,因此任何描述用户界面以及与该用户界面交互的内容都可以使用 React 解决。在前面的列表中,React 可以处理除第一个任务之外的所有任务,因为它描述的是数据获取,而不是用户界面。步骤 1 将通过我们将在下一章讨论的另一个库来解决。步骤 2 和 4 描述了需要显示的内容。它们是 React 组件的完美候选者。步骤 3 和 6 描述了用户事件,正如我们在第 4 章创建你的第一个 React 组件中所看到的,用户事件处理可以封装在 React 组件中。你能想到如何用 React 解决第 5 步吗?记得在第 3 章创建你的第一个 React 元素中,我们讨论了 ReactDOMServer.renderToStaticMarkup() 方法,它将 React 元素渲染为静态 HTML 标签字符串。这正是我们解决步骤 5 所需要的。

现在,当我们为每个任务确定了一个潜在的解决方案时,让我们考虑如何将它们组合起来,并创建一个功能齐全的 web 应用。

有两种方法可以构建可组合的 React 应用:

  • 首先,你可以从构建单个 React 组件开始,然后将它们组合成更高级别的 React 组件,向上移动组件层次结构
  • 你可以从最顶层的 React 元素开始,然后实现其子组件,向下移动组件层次结构

第二种策略的优点是我们可以看到并理解应用体系结构的全貌,我认为在我们考虑如何实现单个功能之前,了解所有内容是如何结合在一起的很重要。

规划 React 应用

在规划 React 应用时,我们需要遵循两条简单的准则:

  • 每个 React 组件应该代表 web 应用中的单个用户界面元素。它应该封装可能被重用的最小元素。
  • 应将多个 React 组件组合成一个单独的 React 组件。最终,你的整个用户界面应该封装在一个 React 组件中。

react-components-hierarchy.jpg

我们将从最顶层的 React 组件 Application 开始。它将封装我们的整个 React 应用,它将有两个子组件:StreamCollection 组件。Stream 组件将负责连接到推文流,接收和显示最新的推文。Stream 组件将有两个子组件:StreamTweetHeaderStreamTweet 组件将负责显示最新的推文,它将由 HeaderTweet 组件组成。Header 组件将渲染标头,它将没有子组件。Tweet 组件将渲染来自推文的图片。请注意,我们计划重用 Header 组件两次。

Collection 组件将负责显示 CollectionControls 和推文列表。它将有两个子组件:CollectionControlsTweetListCollectionControls 组件将有两个子组件:CollectionRenameForm 组件和 CollectionExportForm 组件,前者将渲染表单来重命名集合,后者将渲染表单并将集合导出到 CodePen 服务,它 是一个 HTML、CSS 和 JavaScript 练习场网站。有关 CodePen 的详细信息,请访问http://codepen.io。正如你可能已经注意到的,我们将重用 CollectionRenameFormCollectionControls 组件中的 HeaderButton 组件。我们的 TweetList 组件将渲染推文列表。每一条推文都将由一个推文组件渲染。我们将在 Collection 组件中再次重用 Header 组件。事实上,我们总共将重复使用 Header 组件五次。那对我们来说是一场胜利。正如我们在上一章中所讨论的,我们应该尽可能多地保持 React 组件无状态。因此,11 个组件中只有 5 个将存储状态,如下所示:

  • Application
  • CollectionControls
  • CollectionRenameForm
  • Stream
  • StreamTweet

当我们有了一个计划,就可以开始实施了。

创建 React 容器组件

让我们从编辑应用的主 JavaScript 文件开始。替换 ~/snapterest/source/app.js 文件,包含以下代码片段:

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

import Application from './components/Application';

ReactDOM.render(<Application />, document.getElementById('react-application'));

该文件中只有四行代码,正如你所猜测的,我们使用 getElementById('react-application') 作为<application /> 组件的部署目标,并将 <application /> 渲染到 DOM。web 应用的整个用户界面将封装在一个 React 组件(application)中。

接下来,转到 ~/snapterest/source/components/ 并新建 Application.js 文件:

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

class Application extends Component {
  state = {
    collectionTweets: {},
  };

  addTweetToCollection = (tweet) => {
    const { collectionTweets } = this.state;

    collectionTweets[tweet.id] = tweet;

    this.setState({
      collectionTweets: collectionTweets,
    });
  };

  removeTweetFromCollection = (tweet) => {
    const { collectionTweets } = this.state;

    delete collectionTweets[tweet.id];

    this.setState({
      collectionTweets: collectionTweets,
    });
  };

  removeAllTweetsFromCollection = () => {
    this.setState({
      collectionTweets: {},
    });
  };

  render() {
    const {
      addTweetToCollection,
      removeTweetFromCollection,
      removeAllTweetsFromCollection,
    } = this;

    return (
      <div className="container-fluid">
        <div className="row">
          <div className="col-md-4 text-center">
            <Stream onAddTweetToCollection={addTweetToCollection} />
          </div>
          <div className="col-md-8">
            <Collection
              tweets={this.state.collectionTweets}
              onRemoveTweetFromCollection={removeTweetFromCollection}
              onRemoveAllTweetsFromCollection={removeAllTweetsFromCollection}
            />
          </div>
        </div>
      </div>
    );
  }
}

export default Application;

这个组件的代码比 app.js 文件多得多,但是这个代码可以很容易地分为三个逻辑部分:

  • 导入依赖模块
  • 定义 React 组件类
  • 将 React 组件类导出为模块

Application.js 文件第一部分逻辑使用 import 导入依赖项模块:

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

我们的 Application 组件将有两个子组件需要导入:

  • Stream 组件将渲染用户界面的流部分
  • Collection 组件将渲染用户界面的集合部分

我们还需要将 React 库作为另一个模块导入。

Application.js 文件的第二部分逻辑创建 React Application 组件类,方法如下:

  • addTweetToCollection()
  • removeTweetFromCollection()
  • removeAllTweetsFromCollection()
  • render()

只有 render() 方法是 React API 的一部分。所有其他方法都是该组件封装的应用逻辑的一部分。在讨论该组件在其 render() 方法中渲染的内容后,我们将仔细研究它们中的每一个:

render() {
  const {
    addTweetToCollection,
    removeTweetFromCollection,
    removeAllTweetsFromCollection
  } = this;

  return (
    <div className="container-fluid">
      <div className="row">
        <div className="col-md-4 text-center">
          <Stream onAddTweetToCollection={addTweetToCollection}/>
        </div>
        <div className="col-md-8">
          <Collection
            tweets={this.state.collectionTweets}
            onRemoveTweetFromCollection=
            {removeTweetFromCollection}
            onRemoveAllTweetsFromCollection=
            {removeAllTweetsFromCollection}
          />
        </div>
      </div>
    </div>
  );
}

如你所见,它使用 Bootstrap 框架定义了我们的网页布局。如果你不熟悉 Bootstrap,强烈建议你访问http://getbootstrap.com并阅读文档。学习此框架将使你能够快速轻松地原型化用户界面。即使你不了解 Bootstrap,也很容易理解发生了什么。我们将网页分成两列:一列较小,一列较大。较小的一个包含我们的 StreamReact 组件,较大的一个则包含我们的 Collection 组件。

你可以想象我们的网页被分成两个不相等的部分,它们都包含 React 组件。
这是我们使用 Stream 组件的方式:

<Stream onAddTweetToCollection={addTweetToCollection}/>

Stream 组件有一个 onAddTweetToCollection 属性,我们的 Application 组件将自己的 addTweetToCollection() 方法作为该属性值传递。

addTweetToCollection() 方法将推文添加到集合中。这是我们在应用中的自定义方法之一。我们不需要 this 关键字,因为该方法被定义为一个箭头函数,因该组件自动成为函数的执行范围。

让我们看看 addTweetToCollection() 方法的作用:

addTweetToCollection = (tweet) => {
  const { collectionTweets } = this.state;

  collectionTweets[tweet.id] = tweet;

  this.setState({
    collectionTweets: collectionTweets,
  });
};

该方法引用存储在当前状态中的推文集合,向 collectionTweets 对象添加新的推文,并通过调用 setState() 方法更新状态。在 Stream 组件内调用 addTweetToCollection() 方法时,新的推文 作为参数传递。这是子组件如何更新其父组件状态的示例。

这是 React 中的一个重要机制,其工作原理如下:

  1. 父组件将回调函数作为属性传递给它的子组件。该子组件可以通过 this.props 引用访问此回调函数。
  2. 每当子组件想要更新父组件的状态时,它都会调用回调函数,并将所有必要的数据传递给新的父组件状态。
  3. 父组件会更新其状态,正如你已经知道的,该状态会更新并触发 render() 方法,该方法会根据需要重新渲染所有子组件。

这是子组件与父组件交互的方式。这种交互允许子组件将应用的状态管理委托给其父组件,而它只关心如何渲染自己。现在,当你学习了这种模式后,你将反复使用它,因为大多数 React 组件应该保持无状态。只有少数父组件应该存储和管理应用的状态。这种最佳实践使我们能够根据两个不同的关注点对 React 组件进行逻辑分组:

  • 管理应用的状态并渲染它
  • 仅渲染,将应用的状态管理和委派父组件

我们的 Application 组件有第二个子组件 Collection

<Collection
  tweets="{this.state.collectionTweets}"
  onRemoveTweetFromCollection="{removeTweetFromCollection}"
  onRemoveAllTweetsFromCollection="{removeAllTweetsFromCollection}"
/>

该组件有许多属性:

  • tweets:当前的推文 s 集合
  • onRemoveTweetFromCollection:从集合中删除特定推文的函数
  • onRemoveAllTweetsFromCollection:从集合中删除所有 tweet 的函数

你可以看到,Collection 组件的属性只关心如何执行以下操作:

  • 访问应用的状态
  • 更改应用的状态

正如你所猜测的,onRemoveTweetFromCollectiononRemoveAllTweetsFromCollection 函数允许 Collection 组件改变 Application 组件的状态。另一方面,tweets 属性将 Application 组件的状态传递到 Collection 组件,以便它可以获得对该状态的进行只读访问。

你能看出 ApplicationCollection 组件之间的单向数据流吗?以下是它的工作原理:

  1. collectionTweets 数据在 Application 组件的构造方法中初始化数据。
  2. collectionTweets 数据作为 tweets 属性传递给 Collection 组件。
  3. Collection 组件调用 removeTweetFromCollectionremoveAllTweetsFromCollection 函数,以更新 Application 组件中的 collectionTweets 数据,然后循环再次开始。
请注意,Collection 组件不能直接改变 Application 组件的状态。Collection 组件通过 this.props 对象对该状态有只读访问权限。更新父组件状态的唯一方法是调用父组件传递的回调函数。在 Collection 组件中,这些回调函数是 this.props.onRemoveTweetFromCollectionthis.props.onRemoveAllTweetsFromCollection

这个关于 React 组件层次结构中数据流的简单心理模型将帮助我们增加使用的组件数量,而不会增加用户界面工作的复杂性。例如,它可以有多达 10 个级别的嵌套 React 组件,如下所示:

react-components-nested.jpg

如果Componen G 想要改变根 Component A 的状态,它将以与 Component BComponent F 完全相同的方式进行,或者像这个层次结构中的任何其他 Component 一样。然而,在 React 中,你不应该将数据从 Component A 直接传递到 Component G。相反,你应该首先将数据传递到 Component B,然后传递到 Component C,然后再传递到 Component D,依此类推,直到你最终到达 Component GComponent BComponent F 将必须携带一些实际上只针对 Component G 的“传输”属性。这看起来可能是浪费时间,但这种设计使我们很容易调试应用,并分析它的工作原理。当然我们可以找到一些优化应用架构的策略。其中之一是使用 Flux 设计模式,另一个是使用 Redux 库。我们将在本书稍后讨论这两个问题。

在我们讨论完 Application 组件之前,让我们先看看改变其状态的两种方法:

removeTweetFromCollection = (tweet) => {
  const { collectionTweets } = this.state;

  delete collectionTweets[tweet.id];

  this.setState({
    collectionTweets: collectionTweets,
  });
};

removeTweetFromCollection() 方法从我们存储在 Application 组件状态中的推文集合中删除推文。它从组件的状态中获取当前 collectionTweets 对象,从该对象中删除给定 id 的推文,并使用更新过的 collectionTweets 对象更新组件的状态。

另一方面,removeAllTweetsFromCollection() 方法从组件的状态中删除所有推文:

removeAllTweetsFromCollection = () => {
  this.setState({
    collectionTweets: {},
  });
};

这两个方法都是从子级的 Collection 组件调用的,因为该组件没有其他方法来改变 Application 组件的状态。

总结

在本章中,你学习了如何使用 React 解决问题。我们首先将问题分解为较小的单个问题,接下来讨论如何使用 React 解决这些问题。然后我们创建了一个需要实现的 React 组件列表。最后,我们创建了第一个可组合的 React 组件,并了解了父组件如何与其子组件交互。

下一章,我们将实现子组件,并了解 React 的生命周期方法。

9

评论 (0)

取消