Skip to content

Latest commit

 

History

History
1201 lines (870 loc) · 37.3 KB

File metadata and controls

1201 lines (870 loc) · 37.3 KB

十一、准备 React 应用,将 Flux 用于无痛维护

我们决定在 React 应用中实现 Flux 体系结构的原因是我们希望有一个更易于维护的数据流。在上一章中,我们实现了AppDispatcherTweetActionCreatorsTweetStore。让我们快速记住它们的用途:

  • TweetActionCreators:创建并分派动作
  • AppDispatcher:将所有动作发送到所有门店
  • TweetStore:存储和管理应用数据

我们的数据流中唯一缺失的部分如下:

  • 使用TweetActionCreators创建动作并启动数据流
  • 使用TweetStore获取数据

这里有几个重要的问题要问:在我们的应用中,数据流从哪里开始?我们的数据是什么?如果我们回答了这些问题,我们将了解从何处开始重构应用以适应 Flux 体系结构。

Snapterest 允许用户接收和收集最新推文。我们的应用关注的唯一数据是 tweets。因此,我们的数据流从接收新推文开始。目前,我们的应用的哪个部分负责接收新推文?您可能还记得我们的Stream组件有以下componentDidMount()方法:

componentDidMount() {
  SnapkiteStreamClient.initializeStream(this.handleNewTweet);
}

是的,目前,我们在呈现Stream组件后启动了一个新的推特流。等等,您可能会问,“我们不是知道 React 组件应该只关心呈现用户界面吗?”您是对的。不幸的是,目前,Stream组件负责两件不同的事情:

  • 呈现StreamTweet组件
  • 启动数据流

显然,这是未来可能出现的维护问题。让我们借助 Flux 来解耦这两个不同的关注点。

与 Flux 的解耦关系

首先,我们将创建一个名为WebAPIUtils的新实用模块。在~/snapterest/source/utils/目录中创建WebAPIUtils.js文件:

import SnapkiteStreamClient from ‘snapkite-stream-client’;
import { receiveTweet } from ../actions/TweetActionCreators’;

function initializeStreamOfTweets() {
  SnapkiteStreamClient.initializeStream(receiveTweet);
}

export { initializeStreamOfTweets };

在这个实用模块中,我们首先导入SnapkiteStreamClient库和TweetActionCreators。然后,我们创建initializeStreamOfTweets()函数来初始化新 tweet 流,就像Stream组件的componentDidMount()方法一样。除了一个关键的区别外:每当SnapkiteStreamClient收到一条新的 tweet,它就会调用TweetActionCreators.receiveTweet方法,将一条新 tweet 作为参数传递给它:

SnapkiteStreamClient.initializeStream(receiveTweet);

请记住,receiveTweet函数期望接收一个tweet参数:

function receiveTweet(tweet) {
  // ... create and dispatch ‘receive_tweet’ action
}

然后,此 tweet 将作为receiveTweet()函数创建的新操作对象的属性进行调度。

然后,WebAPIUtils模块导出我们的initializeStreamOfTweets()函数。

现在我们有了一个模块,该模块带有一种方法,可以在我们的 Flux 体系结构中启动数据流。我们应该在哪里进口和调用它?因为它与Stream组件是解耦的,事实上,它根本不依赖于任何 React 组件,我们甚至可以在 React 渲染任何东西之前使用它。让我们在app.js文件中使用它:

import React from ‘react’;
import ReactDOM from ‘react-dom’;
import Application from ./components/Application’;
import { initializeStreamOfTweets } from ./utils/WebAPIUtils’;

initializeStreamOfTweets();

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

如您所见,我们需要做的就是导入并调用initializeStreamOfTweets()方法:

import { initializeStreamOfTweets } from ./utils/WebAPIUtils’;

initializeStreamOfTweets();

我们在调用 React 的render()方法之前执行此操作:

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

事实上,作为一个实验,您可以完全删除代码的ReactDOM.render()行,并在TweetActionCreators.receiveTweet函数中放入一条 log 语句。例如,运行以下代码:

function receiveTweet(tweet) {

  console.log("I’ve received a new tweet and now will dispatch it together with a new action.");

  const action = {
    type: ‘receive_tweet’,
    tweet
  };

  AppDispatcher.dispatch(action);
}

现在运行npm start命令。然后,在 web 浏览器中打开~/snapterest/build/index.html,您将看到页面上呈现的以下文本:

我即将学习 React.js 的要点。

现在打开 JavaScript 控制台,您将看到以下输出:

[Snapkite Stream Client] Socket connected
I’ve received a new tweet and now will dispatch it together with a new action.

对于我们的应用收到的每个新 tweet,都会打印出此日志消息。尽管我们没有渲染任何 React 组件,但我们的 Flux 架构仍然存在:

  1. 我们的应用收到一条新的 tweet。
  2. 它创建并分派一个新操作。
  3. 没有商店向调度员注册,因此没有人接收新操作;因此,什么也没有发生。

现在你们可以清楚地看到,反应和 Flux 是两个完全不依赖的独立事物。

但是,我们确实希望呈现我们的 React 组件。毕竟,在前面的十章中,我们已经花了很多精力来创建它们!要做到这一点,我们需要把我们的TweetStore商店付诸行动。你能猜到我们应该在哪里使用它吗?这里有一个提示:在一个 React 组件中,它需要一条 tweet 来呈现我们的老组件Stream

重构流组件

现在,随着 Flux 体系结构的就位,我们将重新思考 React 组件如何获得需要渲染的数据。如您所知,对于 React 组件,通常有两个数据源:

  • 调用另一个库,例如,调用jQuery.ajax()方法,或者在我们的例子中调用SnapkiteStreamClient.initializeStream()
  • 通过props对象从父组件接收数据

我们希望我们的 React 组件不使用任何外部库来接收数据。相反,从现在起,他们将从商店获得相同的数据。记住这个计划,让我们重构Stream组件。

下面是它现在的样子:

import React from ‘react’;
import SnapkiteStreamClient from ‘snapkite-stream-client’;
import StreamTweet from ./StreamTweet’;
import Header from ./Header’;

class Stream extends React.Component {
  constructor() {
    super();

    this.state = {
      tweet: null
    };
  }

  componentDidMount() {
    SnapkiteStreamClient.initializeStream(this.handleNewTweet);
  }

  componentWillUnmount() {
    SnapkiteStreamClient.destroyStream();
  }

  handleNewTweet = tweet => {
    this.setState({
      tweet
    });
  }

  render() {
    const { tweet } = this.state;
    const { onAddTweetToCollection } = this.props;
    const headerText = "Waiting for public photos from Twitter...";

    if (tweet) {
      return (
        <StreamTweet
          tweet={tweet}
          onAddTweetToCollection={onAddTweetToCollection}
        />
      );
    }

    return (
      <Header text={headerText} />
    );
  }
}

export default Stream;

首先,让我们把去掉componentDidMount()componentWillUnmount()handleNewTweet()方法,导入TweetStore存储:

import React from ‘react’;
import SnapkiteStreamClient from ‘snapkite-stream-client’;
import StreamTweet from ./StreamTweet’;
import Header from ./Header’;
import TweetStore from ../stores/TweetStore’;

class Stream extends React.Component {
  state = {
    tweet: null
  }

  render() {
    const { tweet } = this.state;
    const { onAddTweetToCollection } = this.props;
    const headerText = "Waiting for public photos from Twitter...";

    if (tweet) {
      return (
        <StreamTweet
          tweet={tweet}
          onAddTweetToCollection={onAddTweetToCollection}
        />
      );
    }

    return (
      <Header text={headerText} />
    );
  }
}

export default Stream;

也不再需要导入snapkite-stream-client模块。

接下来,我们需要改变Stream组件获取初始 tweet 的方式。让我们更新其初始状态:

state = {
  tweet: TweetStore.getTweet()
}

就代码而言,这看起来可能是一个小的变化,但它是一个重大的架构改进。我们现在使用getTweet()方法从TweetStore存储中获取数据。在上一章中,我们讨论了存储如何在 Flux 中公开公共方法,以允许应用的其他部分从中获取数据。getTweet()方法是这些公共方法之一的一个示例,称为getters

您可以从存储中获取数据,但不能像这样直接在存储中设置数据。商店没有公开的setter方法。它们是特意设计的,考虑到这一限制,因此当您使用 Flux 编写应用时,您的数据只能朝一个方向流动。当您需要维护 Flux 应用时,这将使您受益匪浅。

现在我们知道了如何获得最初的 tweet,但是如何获得稍后将到达的所有其他新 tweet 呢?我们可以创建一个计时器并重复调用TweetStore.getTweet();然而,这并不是最好的解决方案,因为它假设我们不知道TweetStore何时更新新的推文。然而,我们确实知道这一点。

怎样请记住,在上一章中,我们在TweetStore对象上实现了以下公共方法,即addChangeListener()方法:

addChangeListener(callback) {
  this.on(‘change’, callback);
}

我们还实施了removeChangeListener()方法:

removeChangeListener(callback) {
  this.removeListener(‘change’, callback);
}

对。我们可以让TweetStore告诉我们它什么时候改变数据。为此,我们需要调用它的addChangeListener()方法,并向它传递一个回调函数,TweetStore将为每个新 tweet 调用该函数。问题是在我们的Stream组件中,我们将TweetStore.addChangeListener()方法称为何处?

由于每个组件的生命周期只需要向TweetStore添加一次change事件监听器,因此componentDidMount()是一个完美的候选者。在Stream组件中添加以下componentDidMount()方法:

componentDidMount() {
  TweetStore.addChangeListener(this.onTweetChange);
}

在这里,我们将自己的change事件侦听器this.onTweetChange添加到TweetStore。现在当TweetStore改变它的数据时,它将触发我们的this.onTweetChange方法。我们将很快创建此方法。

不要忘记,在卸载 React 组件之前,我们需要删除任何事件侦听器。为此,在Stream组件中添加以下componentWillUnmount()方法:

componentWillUnmount() {
  TweetStore.removeChangeListener(this.onTweetChange);
}

删除事件侦听器与添加它非常相似。我们调用TweetStore.removeChangeListener()方法并将this.onTweetChange方法作为参数传递。

现在,是时候在我们的Stream组件中创建onTweetChange方法了:

onTweetChange = () => {
  this.setState({
    tweet: TweetStore.getTweet()
  });
}

如您所见,它使用TweetStore.getTweet()方法使用TweetStore中存储的新 tweet 更新组件的状态。

我们需要在Stream组件中进行最后一项更改。在本章后面,您将了解到我们的StreamTweet组件不再需要handleAddTweetToCollection()回调函数;因此,在此组件中,我们将更改以下代码段:

return (
  <StreamTweet
    tweet={tweet}
    onAddTweetToCollection={onAddTweetToCollection}
  />
);

将其替换为以下代码:

return (<StreamTweet tweet={tweet} />);

现在让我们看看我们新重构的Stream组件:

import React from ‘react’;
import StreamTweet from ./StreamTweet’;
import Header from ./Header’;
import TweetStore from ../stores/TweetStore’;

class Stream extends React.Component {
  state = {
    tweet: TweetStore.getTweet()
  }

  componentDidMount() {
    TweetStore.addChangeListener(this.onTweetChange);
  }

  componentWillUnmount() {
    TweetStore.removeChangeListener(this.onTweetChange);
  }

  onTweetChange = () => {
    this.setState({
      tweet: TweetStore.getTweet()
    });
  }

  render() {
    const { tweet } = this.state;
    const { onAddTweetToCollection } = this.props;
    const headerText = "Waiting for public photos from Twitter...";

    if (tweet) {
      return (<StreamTweet tweet={tweet}/>);
    }

    return (<Header text={headerText}/>);
  }
}

export default Stream;

让我们回顾一下我们的Stream组件如何始终拥有最新推文:

  1. 我们使用getTweet()方法将组件的初始 tweet 设置为从TweetStore获得的最新 tweet。
  2. 然后,我们聆听TweetStore中的变化。
  3. TweetStore更改其 tweet 时,我们使用getTweet()方法将组件的状态更新为从TweetStore获得的最新 tweet。
  4. 当组件即将卸载时,我们停止监听TweetStore中的更改。

这就是 React 组件如何与焊剂存储器相互作用。

在我们继续增强应用的其余部分之前,让我们先看看当前的数据流:

  • app.js:接收新的推文,并为每条推文呼叫TweetActionCreators
  • TweetActionCreators:这会创建并发送一条新的推文
  • AppDispatcher:将所有动作发送到所有门店
  • TweetStore:向调度器注册,并在从调度器接收到的每个新操作上发出更改事件
  • Stream:监听TweetStore中的变化,从TweetStore获取新的推文,用新的推文更新状态,并重新呈现

您能看到我们现在如何扩展 React 组件、动作创建者和存储的数量,并且仍然能够维护 Snapterest 吗?对于 Flux,它将始终是单向数据流。无论我们将实现多少新功能,它都将是相同的心智模型。从长远来看,当我们需要维护我们的应用时,我们将受益匪浅。

我有没有提到我们将在应用中进一步调整 Flux?接下来,让我们就这样做。

创建集合存储

Snapterest 不仅存储最新的推文,还存储用户创建的推文集合。让我们用 Flux 重构这个特性。

首先,让我们创建一个集合存储。导航到~/snapterest/source/stores/目录并创建CollectionStore.js文件:

import AppDispatcher from ../dispatcher/AppDispatcher’;
import { EventEmitter } from ‘events’;

const CHANGE_EVENT = ‘change’;

let collectionTweets = {};
let collectionName = ‘new’;

function addTweetToCollection(tweet) {
  collectionTweets[tweet.id] = tweet;
}

function removeTweetFromCollection(tweetId) {
  delete collectionTweets[tweetId];
}

function removeAllTweetsFromCollection() {
  collectionTweets = {};
}

function setCollectionName(name) {
  collectionName = name;
}

function emitChange() {
  CollectionStore.emit(CHANGE_EVENT);
}

const CollectionStore = Object.assign(
  {}, EventEmitter.prototype, {
  addChangeListener(callback) {
    this.on(CHANGE_EVENT, callback);
  },

  removeChangeListener(callback) {
    this.removeListener(CHANGE_EVENT, callback);
  },

  getCollectionTweets() {
    return collectionTweets;
  },

  getCollectionName() {
    return collectionName;
  }
}
);

function handleAction(action) {

  switch (action.type) {
    case ‘add_tweet_to_collection’:
      addTweetToCollection(action.tweet);
      emitChange();
      break;

    case ‘remove_tweet_from_collection’:
      removeTweetFromCollection(action.tweetId);
      emitChange();
      break;

    case ‘remove_all_tweets_from_collection’:
      removeAllTweetsFromCollection();
      emitChange();
      break;

    case ‘set_collection_name’:
      setCollectionName(action.collectionName);
      emitChange();
      break;

    default: // ... do nothing

  }
}

CollectionStore.dispatchToken = AppDispatcher.register(handleAction);

export default CollectionStore;

CollectionStore是一家更大的商店,但其结构与TweetStore相同。

首先,我们导入依赖项,并将change事件名称分配给CHANGE_EVENT变量:

import AppDispatcher from ../dispatcher/AppDispatcher’;
import { EventEmitter } from ‘events’;

const CHANGE_EVENT = ‘change’;

然后,我们定义了我们的数据和四种改变数据的私有方法:

let collectionTweets = {};
let collectionName = ‘new’;

function addTweetToCollection(tweet) {
  collectionTweets[tweet.id] = tweet;
}

function removeTweetFromCollection(tweetId) {
  delete collectionTweets[tweetId];
}

function removeAllTweetsFromCollection() {
  collectionTweets = {};
}

function setCollectionName(name) {
  collectionName = name;
}

如您所见,我们将 tweet 集合存储在一个最初为空的对象中,并且我们还存储最初设置为new的集合名称。然后,我们创建了三个变异为collectionTweets的私有函数:

  • addTweetToCollection():顾名思义,它将tweet对象添加到collectionTweets对象中
  • removeTweetFromCollection():从collectionTweets对象中删除tweet对象
  • removeAllTweetsFromCollection():通过将collectionTweets设置为空对象,将tweet对象从collectionTweets中移除

然后,我们定义了一个名为setCollectionName的私有函数,它将现有集合名称更改为新的集合名称。

这些功能被认为是私有的,因为它们在CollectionStore模块之外无法访问;例如,您无法在任何其他模块中访问它们:

CollectionStore.setCollectionName(‘impossible’);

如前所述,这样做是为了在应用中强制执行单向数据流。

我们创建了发出change事件的emitChange()方法。

然后,我们创建CollectionStore对象:

const CollectionStore = Object.assign(
  {}, EventEmitter.prototype, {
  addChangeListener(callback) {
    this.on(CHANGE_EVENT, callback);
  },

  removeChangeListener(callback) {
    this.removeListener(CHANGE_EVENT, callback);
  },

  getCollectionTweets() {
    return collectionTweets;
  },

  getCollectionName() {
    return collectionName;
  }
});

这与TweetStore对象非常相似,除了两种方法:

  • getCollectionTweets():返回一组推文
  • getCollectionName():返回集合名称

这些方法可在CollectionStore.js文件之外访问,并应在 React 组件中使用,以从CollectionStore获取数据。

然后,我们创建handleAction()函数:

function handleAction(action) {
  switch (action.type) {

    case ‘add_tweet_to_collection’:
      addTweetToCollection(action.tweet);
      emitChange();
      break;

    case ‘remove_tweet_from_collection’:
      removeTweetFromCollection(action.tweetId);
      emitChange();
      break;

    case ‘remove_all_tweets_from_collection’:
      removeAllTweetsFromCollection();
      emitChange();
      break;

    case ‘set_collection_name’:
      setCollectionName(action.collectionName);
      emitChange();
      break;

    default: // ... do nothing

  }
}

此函数处理由AppDispatcher分派的动作,但与CollectionStore模块中的TweetStore不同,我们可以处理多个动作。事实上,我们可以处理与推特收集相关的四个操作:

  • add_tweet_to_collection:这会在收藏中添加一条推文
  • remove_tweet_from_collection:这将从集合中删除推文
  • remove_all_tweets_from_collection:这将删除集合中的所有推文
  • set_collection_name:设置集合名称

记住所有的商店都会接收所有的动作,所以CollectionStore也会接收receive_tweet动作,但我们只是在这个商店中忽略它,就像TweetStore忽略add_tweet_to_collectionremove_tweet_from_collectionremove_all_tweets_from_collectionset_collection_name一样。

然后,我们用AppDispatcher注册handleAction回调,并将dispatchToken保存在CollectionStore对象中:

CollectionStore.dispatchToken = AppDispatcher.register(handleAction);

最后,我们将CollectionStore作为一个模块导出:

export default CollectionStore;

既然我们已经准备好了集合存储,接下来让我们创建 action creator 函数。

创建 CollectionCreators

导航到~/snapterest/source/actions/并创建该CollectionActionCreators.js文件:

import AppDispatcher from ../dispatcher/AppDispatcher’;

function addTweetToCollection(tweet) {
  const action = {
    type: ‘add_tweet_to_collection’,
    tweet
  };

  AppDispatcher.dispatch(action);
}

function removeTweetFromCollection(tweetId) {
  const action = {
    type: ‘remove_tweet_from_collection’,
    tweetId
  };

  AppDispatcher.dispatch(action);
}

function removeAllTweetsFromCollection() {
  const action = {
    type: ‘remove_all_tweets_from_collection’
  };

  AppDispatcher.dispatch(action);
}

function setCollectionName(collectionName) {
  const action = {
    type: ‘set_collection_name’,
    collectionName
  };

  AppDispatcher.dispatch(action);
}

export default {
  addTweetToCollection,
  removeTweetFromCollection,
  removeAllTweetsFromCollection,
  setCollectionName
};

对于我们在CollectionStore中处理的每个动作,我们都有一个动作创建者函数:

  • addTweetToCollection():创建并发送带有新 tweet 的add_tweet_to_collection动作
  • removeTweetFromCollection():这将创建并发送带有必须从集合中删除的 tweet ID 的remove_tweet_from_collection操作
  • removeAllTweetsFromCollection():创建并发送remove_all_tweets_from_collection动作
  • setCollectionName():创建并发送带有新集合名称的set_collection_name操作

现在,当我们创建了CollectionStoreCollectionActionCreators模块后,我们可以开始重构 React 组件,以采用 Flux 架构。

重构应用组件

我们从哪里开始重构我们的组件?让我们从组件层次结构中最顶层的 React 组件Application开始。

目前,我们的Application组件存储并管理 tweet 的收集。让我们删除此功能,因为它现在由集合存储管理。

Application组件中删除constructor()addTweetToCollection()removeTweetFromCollection()removeAllTweetsFromCollection()方法:

import React from ‘react’;
import Stream from ./Stream’;
import Collection from ./Collection’;

class Application extends React.Component {
  render() {
    const {
      collectionTweets
    } = this.state;

    return (
      <div className="container-fluid">
        <div className="row">
          <div className="col-md-4 text-center">
            <Stream onAddTweetToCollection={this.addTweetToCollection}/>

          </div>
          <div className="col-md-8">
            <Collection
              tweets={collectionTweets}
              onRemoveTweetFromCollection={this.removeTweetFromCollection}
              onRemoveAllTweetsFromCollection={this.removeAllTweetsFromCollection}
            />
          </div>
        </div>
      </div>
    );
  }
}

export default Application;

现在Application组件只有呈现StreamCollection组件的render()方法。因为它不再管理 tweet 的收集,所以我们也不需要将任何属性传递给StreamCollection组件。

更新Application组件的render()功能,如下所示:

render() {
  return (
    <div className="container-fluid">
      <div className="row">
        <div className="col-md-4 text-center">
          <Stream/>
        </div>
        <div className="col-md-8">
          <Collection/>
        </div>
      </div>

    </div>
  );
}

Flux 架构的采用使得Stream组件可以管理最新的 tweet,Collection组件可以管理 tweet 的收集,而Application组件不再需要管理任何东西,因此它变成了一个包装StreamCollection的容器组件附加 HTML 标记中的组件。

事实上,您可能已经注意到,我们当前版本的Application组件是一个很好的候选功能组件:

import React from ‘react’;
import Stream from ./Stream’;
import Collection from ./Collection’;

const Application = () =>(
  <div className="container-fluid">
    <div className="row">
      <div className="col-md-4 text-center">
        <Stream />
      </div>
      <div className="col-md-8">
        <Collection />
      </div>
    </div>
  </div>
);

export default Application;

我们的Application组件现在简单多了,它的标记看起来干净多了。这提高了组件的可维护性。做得好!

重构集合组件

接下来,让我们重构Collection组件。将现有的Collection组件更换为以下组件:

import React, { Component } from ‘react’;
import ReactDOMServer from ‘react-dom/server’;
import CollectionControls from ./CollectionControls’;
import TweetList from ./TweetList’;
import Header from ./Header’;
import CollectionUtils from ../utils/CollectionUtils’;
import CollectionStore from ../stores/CollectionStore’;

class Collection extends Component {
  state = {
    collectionTweets: CollectionStore.getCollectionTweets()
  }

  componentDidMount() {
    CollectionStore.addChangeListener(this.onCollectionChange);
  }

  componentWillUnmount() {
    CollectionStore.removeChangeListener(this.onCollectionChange);
  }

  onCollectionChange = () => {
    this.setState({
      collectionTweets: CollectionStore.getCollectionTweets()
    });
  }

  createHtmlMarkupStringOfTweetList() {
    const htmlString = ReactDOMServer.renderToStaticMarkup(
      <TweetList tweets={this.state.collectionTweets}/>
    );

    const htmlMarkup = {
      html: htmlString
    };

    return JSON.stringify(htmlMarkup);
  }

  render() {
    const { collectionTweets } = this.state;
    const numberOfTweetsInCollection = CollectionUtils
      .getNumberOfTweetsInCollection(collectionTweets);
    let htmlMarkup;

    if (numberOfTweetsInCollection > 0) {
      htmlMarkup = this.createHtmlMarkupStringOfTweetList();

      return (
        <div>
          <CollectionControls
            numberOfTweetsInCollection={numberOfTweetsInCollection}
            htmlMarkup={htmlMarkup}
          />

          <TweetList tweets={collectionTweets} />
        </div>
      );
    }

    return (<Header text="Your collection is empty" />);
  }
}

export default Collection;

我们在这里改变了什么?有几件事。首先,我们导入了两个新模块:

import CollectionUtils from ../utils/CollectionUtils’;
import CollectionStore from ../stores/CollectionStore’;

我们在第 9 章中创建了CollectionUtils模块,使用 Jest测试 React 应用,在本章中,我们正在使用它。CollectionStore是我们获取数据的来源。

接下来,您应该能够发现四种方法的熟悉模式:

  • 在初始状态下,我们将 tweet 的集合设置为当时存储在CollectionStore中的内容。您可能还记得,CollectionStore提供了getCollectionTweets()方法从中获取数据。
  • componentDidMount()方法中,我们将change事件监听器this.onCollectionChange添加到CollectionStore。每当推文集合更新时,CollectionStore将调用我们的this.onCollectionChange回调函数通知Collection组件该更改。
  • componentWillUnmount()方法中,我们移除添加到componentDidMount()方法中的change事件侦听器。
  • onCollectionChange()方法中,我们将组件的状态设置为当时CollectionStore中存储的任何状态。更新组件的状态会触发重新渲染。

Collection组件的render()方法现在更简单、更干净:

render() {
  const { collectionTweets } = this.state;
  const numberOfTweetsInCollection = CollectionUtils
    .getNumberOfTweetsInCollection(collectionTweets);
  let htmlMarkup;

  if (numberOfTweetsInCollection > 0) {
    htmlMarkup = this.createHtmlMarkupStringOfTweetList();

    return (
      <div>
        <CollectionControls
          numberOfTweetsInCollection={numberOfTweetsInCollection}
          htmlMarkup={htmlMarkup}
        />

        <TweetList tweets={collectionTweets}/>
      </div>
    );
  }

  return (<Header text="Your collection is empty"/>);
}

我们使用CollectionUtils模块获取集合中的大量 tweet,并将更少的属性传递给子组件:CollectionControlsTweetList

重构 CollectionControls 组件

CollectionControls组件也得到了一些重大改进。让我们先看看重构版本,然后讨论更新的内容和原因:

import React, { Component } from ‘react’;
import Header from ./Header’;
import Button from ./Button’;
import CollectionRenameForm from ./CollectionRenameForm’;
import CollectionExportForm from ./CollectionExportForm’;
import CollectionActionCreators from ../actions/CollectionActionCreators’;
import CollectionStore from ../stores/CollectionStore’;

class CollectionControls extends Component {
  state = {
    isEditingName: false
  }

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

    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
    }));
  }

  removeAllTweetsFromCollection = () => {
    CollectionActionCreators.removeAllTweetsFromCollection();
  }

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

    if (isEditingName) {
      return (
        <CollectionRenameForm
          name={name}
          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;

首先,我们导入两个附加模块:

import CollectionActionCreators from ../actions/CollectionActionCreators’;
import CollectionStore from ../stores/CollectionStore’;

请注意,我们不再管理此组件中的集合名称。相反,我们从CollectionStore模块获得:

const name = CollectionStore.getCollectionName();

然后,我们做一个关键的改变。我们将setCollectionName()方法替换为新的removeAllTweetsFromCollection()

removeAllTweetsFromCollection = () => {
  CollectionActionCreators.removeAllTweetsFromCollection();
}

当用户点击Empty Collection按钮时,调用removeAllTweetsFromCollection()方法。此用户操作触发removeAllTweetsFromCollection()操作创建者函数,该函数创建操作并将操作分派到存储区。依次,CollectionStore从集合中删除所有 tweet 并发出change事件。

接下来,让我们重构CollectionRenameForm组件。

重构 CollectionRenameForm 组件

CollectionRenameForm为受控表单组件。这意味着其输入值存储在组件的状态中,更新该值的唯一方法是更新组件的状态。它有一个初始值,它应该从CollectionStore得到,所以让我们来实现它。

首先,导入CollectionActionCreatorsCollectionStore模块:

import CollectionActionCreators from ../actions/CollectionActionCreators’;
import CollectionStore from ../stores/CollectionStore’;

现在,我们需要删除其现有的constructor()方法:

constructor(props) {
  super(props);

  const { name } = props;

  this.state = {
    inputValue: name
  };
}

将上述代码替换为以下代码:

state = {
  inputValue: CollectionStore.getCollectionName()
}

正如您所看到的,唯一的区别在于,现在我们从CollectionStore中获得了初始inputValue

接下来我们更新handleFormSubmit()方法:

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

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

  onChangeCollectionName(collectionName);
}

使用以下内容更新前面的代码:

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

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

  CollectionActionCreators.setCollectionName(collectionName);

  onCancelCollectionNameChange();
}

这里的重要区别在于,当用户提交表单时,我们将创建一个新操作,在集合存储中设置一个新名称:

CollectionActionCreators.setCollectionName(collectionName);

最后,我们需要在handleFormCancel()方法中更改集合名称的来源:

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

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

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

将前面的代码更改为以下代码:

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

  const {
    onCancelCollectionNameChange
  } = this.props;

  const collectionName = CollectionStore.getCollectionName();

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

再一次,我们从一个收藏商店获得了收藏名称:

const collectionName = CollectionStore.getCollectionName();

这就是我们需要在CollectionRenameForm组件中更改的全部内容。接下来让我们重构TweetList组件。

重构 TweetList 组件

TweetList组件呈现 tweet 列表。每个 tweet 都是一个Tweet组件,用户可以点击该组件将其从集合中删除。你觉得它可以利用CollectionActionCreators吗?

对。让我们将CollectionActionCreators模块添加到其中:

import CollectionActionCreators from ../actions/CollectionActionCreators’;

然后,我们将创建removeTweetFromCollection()回调函数,当用户单击 tweet 图像时将调用该函数:

removeTweetFromCollection = tweet => {
  CollectionActionCreators.removeTweetFromCollection(tweet.id);
}

如您所见,它通过将 tweet ID 作为参数传递给removeTweetFromCollection()函数来创建一个新操作。

最后,我们需要确保实际调用了removeTweetFromCollection()。在getTweetElement()方法中,找到以下行:

const { tweets, onRemoveTweetFromCollection } = this.props;

现在将其替换为以下代码:

const { tweets } = this.props;
const onRemoveTweetFromCollection = this.removeTweetFromCollection;

我们都用这个组件完成了。StreamTweet是我们重构之旅的下一步。

重构 StreamTweet 组件

StreamTweet呈现一个推文图像,用户可以点击该图像将其添加到推文集合中。你可能已经猜到,当用户点击 tweet 图像时,我们将创建并发送一个新动作。

首先,将CollectionActionCreators模块导入StreamTweet组件:

import CollectionActionCreators from ../actions/CollectionActionCreators’;

然后,添加一个新的addTweetToCollection()方法:

addTweetToCollection = tweet => {
  CollectionActionCreators.addTweetToCollection(tweet);
}

当用户点击 tweet 图像时,应调用addTweetToCollection()回调函数。我们来看一下render()方法中的这一行:

<Tweet
  tweet={tweet}
  onImageClick={onAddTweetToCollection}
/>

用以下代码行替换前面的代码:

<Tweet
  tweet={tweet}
  onImageClick={this.addTweetToCollection}
/>

最后,我们需要替换以下行:

const { tweet, onAddTweetToCollection } = this.props; 

改用这个:

const { tweet } = this.props;

StreamTweet组件现在已完成。

建设和超越

这就是将 Flux 体系结构集成到 React 应用中所需的全部工作。如果您将 React 应用与无 Flux 应用和有 Flux 应用进行比较,您将很快发现,当 Flux 是应用的一部分时,理解应用的工作原理是多么容易。您可以在了解更多关于 Flux 的信息 https://facebook.github.io/flux/

我认为现在是检查一切是否处于完美工作状态的好时机。让我们构建并运行 Snapterest!

导航到~/snapterest并在终端窗口中运行以下命令:

npm start

确保您正在运行我们在第 2 章为您的项目安装强大工具中安装和配置的 Snapkite 引擎应用。现在在 web 浏览器中打开~/snapterest/build/index.html文件。您应该会看到新的推文出现在左侧,一次一条。单击一条 tweet 将其添加到右侧显示的集合中。

它有用吗?检查 JavaScript 控制台是否有任何错误。没有错误?

祝贺您将 Flux 体系结构集成到我们的 React 应用中!

总结

在本章中,我们完成了应用的重构,以使用 Flux 体系结构。您了解了如何将反应与 Flux 结合起来,以及 Flux 具有哪些优势。

在下一章中,我们将使用 Redux 库进一步简化应用的体系结构。