我们决定在我们的 React 应用中实现 Flux 架构的原因是我们希望有一个更容易维护的数据流。上一章实现了 AppDispatcher
、TweetActionCreators
和 TweetStore
。让我们快速记住它们的用途:
TweetActionCreators
:创建和调度 actionAppDispatcher
:将所有的 action 调度到所有 storesTweetStore
:用于存储和管理应用数据
数据流中唯一缺失的部分如下:
- 使用
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 架构仍然存在:
- 我们的应用收到一条新的推文。
- 它创建并调度一个新的 action。
- 没有 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
何时更新了一条新的推文。然而,我们确实知道这一点。
怎么做?在上一章中,我们实现了 TweetStor
e 对象的 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
组件是如何始终保存最新消息的:
- 我们将组件的初始推文设置为使用
getTweet()
方法从TweetStore
获取的最新推文。 - 然后,我们在
TweetStore
中监听变化。 - 当
TweetStore
改变它的推文时,我们使用getTweet()
方法将组件的状态更新为从TweetStore
获取的最新推文。 - 当组件即将卸载时,我们停止监听
TweetStore
中的变化。
这就是 React 组件与 Flux store 交互的方式。
在继续使我们的应用的其余部分 Flux 增强之前,让我们看看当前的数据流:
app.js
:接收新的推文并为每条推文调用TweetActionCreators
TweetActionCreators
:创建并派发一个带有新推文的新 actionAppDispatcher
:它将所有的 action 调度到所有的 storesTweetStore
:它注册 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
对象添加到collectionTweet
s 对象中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_collection
、remove_tweet_from_collection
、remove_all_tweets_from_collection
和 set_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 的参数是必须从集合中删除的消息 IDremoveAllTweetsFromCollection()
:创建和调度remove_all_tweets_from_collection
actionsetCollectionName()
:创建并调度add_tweet_to_ collection
action,并使用新的集合名称
现在当我们创建了 CollectionStore
和 CollectionActionCreators
模块后,我们可以采用 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 标记封装了 Stream
和 Collection
组件。
事实上,你可能已经注意到,我们当前版本的 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
模块获取集合中的一些推文,传递给子组件 CollectionControls
和 TweetList
的属性更少。
重构 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
获取的初始值,让我们来实现它。
首先,导入 CollectionActionCreators
和 CollectionStore
模块:
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 库进一步简化应用的架构。
评论 (0)