第 11 章 准备使用 Flux 轻松维护 React 应用

第 11 章 准备使用 Flux 轻松维护 React 应用

Flying
2018-12-10 / 0 评论 / 322 阅读 / 正在检测是否收录...

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

  • TweetActionCreators:创建和调度 action
  • AppDispatcher:将所有的 action 调度到所有 stores
  • TweetStore:用于存储和管理应用数据

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

  • 使用 TweetActionCreators 创建 action 并启动数据流
  • 使用 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() 函数来初始化一个新的推文流,就像在 Stream 组件的 componentDidMount() 方法中一样。除了一个关键的区别:每当 SnapkiteStreamClient 收到一条新消息时,它都会调用 TweetActionCreators.receiveTweet 方法将一条新消息作为参数传递给它:

SnapkiteStreamClient.initializeStream(receiveTweet);

记住,receiveTweet 函数接受的参数是 tweet

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

然后,这条消息将作为 receiveTweet() 函数创建的新 action 对象的属性分发。

然后,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 函数中放入一个日志语句。例如,运行以下代码:

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命令。然后,在浏览器中打开 ~/snapterest/build/index.html ,你会看到下面的文本呈现在页面上:

I am about to learn the essentials of React.js.

现在打开 JavaScript 控制台,你会看到如下输出:

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

应用收到的每一条新消息都会打印出这条日志消息。即使我们没有渲染任何 React 组件,我们的 Flux 架构仍然存在:

  1. 我们的应用收到一条新的推文。
  2. 它创建并调度一个新的 action。
  3. 没有 store 注册到 dispatcher,所以没有人接收新的 action;因此,什么也没有发生。

现在你可以清楚地看到 React 和 Flux 是两个完全不依赖的东西。

然而,我们确实想渲染我们的 React 组件。毕竟,我们在前 10 章中已经花了很多精力来创建它们!要做到这一点,我们需要将 TweetStore 存储到 action。你能猜到我们应该在哪里使用它吗?这里有一个提示:在一个 React 组件中,它需要一条 tweet 来渲染自己——我们的旧 Stream 组件。

重构 Stream 组件

现在有了 Flux 架构,我们将重新思考我们的 React 组件如何获取它们需要渲染的数据。如你所知,一个 React 组件通常有两个数据源:

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

我们希望 React 组件不使用任何外部库来接收数据。相反,从现在开始,他们将从 store 获取相同的数据。记住这个计划,让我们重构我们的 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 组件获取初始消息的方式。让我们更新它的初始状态:

state = {
  tweet: TweetStore.getTweet(),
};

代码方面,这可能看起来只是一个小变化,但却是一个重大的架构改进。我们现在使用 getTweet() 方法从 TweetStore 中获取数据。在上一章中,我们讨论了 store 如何在 Flux 中暴露公共方法,以便应用的其他部分从它们中获取数据。getTweet() 方法是这些公共方法中的一个例子,它们被称为 _getter_。

你可以 store 中获取数据,但你不能像那样直接在 store 中设置数据。store 没有公共的 setter 方法。它们在设计时特意考虑了这种限制,以便当你使用 Flux 编写应用时,数据只能朝一个方向流动。当你需要维护你的 Flux 应用时,这将给你带来很多好处。

现在我们知道了如何获取初始的推文,但如何获取后面所有的新推文呢?我们可以创建一个定时器,并反复调用 TweetStore.getTweet(); 然而,这并不是最好的解决方案,因为它假设我们不知道 TweetStore 何时更新了一条新的推文。然而,我们确实知道这一点。

怎么做?在上一章中,我们实现了 TweetStore 对象的 addChangeListener() 方法,如下所示:

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

我们也实现了 removeChangeListener() 方法:

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

没错,我们可以让 TweetStore 告诉我们它什么时候改变了数据。为此,我们需要调用它的 addChangeListener()方法,并传入一个回调函数,TweetStore 会在收到每条新消息时调用该函数。问题是在 Stream 组件中,我们应该在哪里调用 TweetStore.addChangeListener() 方法呢?

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

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

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

不要忘记,在卸载 React 组件之前,我们需要删除所有事件监听器。为此,将以下 componentWillUnmount() 方法添加到 Stream 组件:

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

删除事件监听器与添加它非常相似。只需调用 TweetStore.removeChangeListener() 方法并传入 this.onTweetChange 方法作为参数。

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

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

如你所见,它使用 TweetStore.gettweet() 方法更新了组件的状态,并将一条新消息存储在 TweetStore 中。

我们需要在 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() 方法从 TweetStore 获取的最新推文。
  2. 然后,我们在 TweetStore 中监听变化。
  3. TweetStore 改变它的推文时,我们使用 getTweet() 方法将组件的状态更新为从 TweetStore 获取的最新推文。
  4. 当组件即将卸载时,我们停止监听 TweetStore 中的变化。

这就是 React 组件与 Flux store 交互的方式。

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

  • app.js:接收新的推文并为每条推文调用 TweetActionCreators
  • TweetActionCreators:创建并派发一个带有新推文的新 action
  • AppDispatcher:它将所有的 action 调度到所有的 stores
  • TweetStore:它注册 dispatcher,并在 dispatcher 接收到每个新 action 时派发 change 事件
  • Stream:监听 TweetStore 中的变化,从 TweetStore 获取一条新的推文,用一条新的推文 更新状态,并重新渲染

你能看到我们现在如何扩展 React 组件、action 创建者和 store 的数量,同时仍然能够维护 Snapterest 吗?使用 Flux,它将永远是单向的数据流。不管我们要实现多少新特性,心智模型都是一样的。从长远来看,当我们需要维护我们的应用时,我们将受益匪浅。

我是否提到过我们将在应用中更多地使用 Flux 呢?接下来,让我们来做这个。

创建 CollectionStore

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 是一个更大的 store,但它具有与 TweetStore 相同的结构。

首先,我们导入依赖项并给 CHANGE_EVENT 变量指定一个 change 事件名称:

import AppDispatcher from '../dispatcher/AppDispatcher';
import { EventEmitter } from 'events';

const CHANGE_EVENT = 'change';

然后,定义数据和 4 个修改这些数据的私有方法:

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

如你所见,我们将推文集合存储在一个对象中,该对象最初为空,我们还将集合名称最初设置为 new。然后,创建三个私有函数来修改 collectionTweets

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

然后,我们定义了一个私有函数 setCollectionName,它会修改 collectionName,把现有的集合名改成新的集合名。

这些函数被认为是私有的,因为它们在 CollectionStore 模块之外不可访问。例如,你不能在任何其他模块中这样访问它们:

CollectionStore.setCollectionName('impossible');

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

我们创建了 emitChange() 方法来触发 change 事件。

然后创建 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 调度 action,但与 CollectionStore 模块中的 TweetStore 不同,我们可以处理多个 actions。实际上,我们可以处理与消息集合相关的 4 个 actions。

  • add_tweet_to_collection:向集合中添加一条推文
  • remove_tweet_from_collection:从集合中删除一条推文
  • remove_all_tweets_from_collection:从集合中删除所有推文
  • set_collection_name:设置集合名称

请记住,所有的数据存储都会接收到所有的 actions,所以 CollectionStore 也会接收到 receive_tweet action,但我们在这个数据存储中忽略了它,就像 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 创建器函数。

创建 CollectionActionCreators

转到 ~/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 中处理的每个 action,我们都有一个 action 创建器函数:

  • addTweetToCollection():创建并调度 add_tweet_to_ collection action,包含一条新的推文
  • removeTweetFromCollection():创建并调度 remove_ tweet_from_collection action,该 action 的参数是必须从集合中删除的消息 ID
  • removeAllTweetsFromCollection():创建和调度 remove_all_tweets_from_collection action
  • setCollectionName():创建并调度 add_tweet_to_ collection action,并使用新的集合名称

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

重构 Application 组件

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

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

删除 constructor()addTweetToCollection()removeTweetFromCollection () 以及 Application 组件的 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 组件只有 render()方法,用于渲染流和 Collection 组件。由于它不再管理推文的集合,我们也不需要向流和 Collection 组件传递任何属性。

更新 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 组件管理最新的推文,Collection 组件管理推文集合,而 Application 组件不再需要管理任何东西,所以它变成了一个容器组件,用额外的 HTML 标记封装了 StreamCollection 组件。

事实上,你可能已经注意到,我们当前版本的 Application 组件很适合作为一个函数式 React 组件:

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 组件。用下面的代码替换现有的 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 章使用 Jest 测试 React 应用时创建了 CollectionUtils 模块,本章我们将使用它。CollectionStore 是我们获取数据的地方。

接下来,你应该能够发现这四个方法的熟悉模式:

  • 在初始状态下,我们将推文集合设置为当时存储在 CollectionStore 中的内容。你可能还记得,CollectionStore 提供了 getCollectionTweets() 方法来从中获取数据。
  • componentDidMount() 方法中,我们添加 change 事件监听器。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 模块获取集合中的一些推文,传递给子组件 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() action 创建函数,该函数创建并调度该 action 到 store。接着,CollectionStore 从集合中删除所有消息并派发 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();
};

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

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 组件,用户可以单击它将其从集合中删除。你觉得它可以使用 CollectionActionCreators 吗?

是可以的。让我们把 CollectionActionCreators 模块添加进去:

import CollectionActionCreators from '../actions/CollectionActionCreators';

然后,创建 removeTweetFromCollection() 回调函数,当用户点击推文图片时调用该函数:

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

如你所见,它使用 removeTweetFromCollection() 函数创建了一个新 action,将消息 ID 作为参数传递给它。

最后,我们需要确保 removeTweetFromCollection()真的被调用了。在 getTweetElement()方法中,找到下面这行代码:

const { tweets, onRemoveTweetFromCollection } = this.props;

现在用下面的代码替换它:

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

我们已经完成了这个组件。重构之旅的下一站是 StreamTweet 组件。

重构 StreamTweet 组件

StreamTweet 渲染一个推文图像,用户可以点击它将其添加到推文集合中。你可能已经猜到了,我们将在用户单击推文图像时创建并调度一个新的 action。

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

import CollectionActionCreators from '../actions/CollectionActionCreators';

然后,新增 addTweetToCollection() 方法:

addTweetToCollection = (tweet) => {
  CollectionActionCreators.addTweetToCollection(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 应用中所需要做的所有工作。如果你比较一下没有使用 Flux 和使用 Flux 的 React 应用,你会很快发现使用 Flux 时,理解你的应用如何工作的是多么容易。你可以访问 https://facebook.github.io/flux了解更多关于 Flux 的信息。

我认为现在是检查一切是否正常的好时机。让我们构建并运行 Snapterest!

转到 ~/snapterest,在终端窗口中执行以下命令:

npm start

请确保运行的是我们在第 2 章中为你的项目安装强大的工具的 Snapkite 引擎应用。现在在浏览器中打开 ~/snapterest/build/index.html 文件。你将看到新推文出现在左侧,每次一条。点击一条推文,将其添加到右侧的集合中。

它有效果吗?检查 JavaScript 控制台是否有错误。没有错误吗?恭喜你已经将 Flux 架构集成到我们的 React 应用中!

总结

在本章中,我们使用 Flux 架构完成了应用的重构。你了解了如何将 React 与 Flux 结合,以及 Flux 所提供的优势。

下一章,我们将使用 Redux 库进一步简化应用的架构。

9

评论 (0)

取消