我们决定在 React 应用中实现 Flux 体系结构的原因是我们希望有一个更易于维护的数据流。在上一章中,我们实现了AppDispatcher、TweetActionCreators和TweetStore。让我们快速记住它们的用途:
TweetActionCreators:创建并分派动作AppDispatcher:将所有动作发送到所有门店TweetStore:存储和管理应用数据
我们的数据流中唯一缺失的部分如下:
- 使用
TweetActionCreators创建动作并启动数据流 - 使用
TweetStore获取数据
这里有几个重要的问题要问:在我们的应用中,数据流从哪里开始?我们的数据是什么?如果我们回答了这些问题,我们将了解从何处开始重构应用以适应 Flux 体系结构。
Snapterest 允许用户接收和收集最新推文。我们的应用关注的唯一数据是 tweets。因此,我们的数据流从接收新推文开始。目前,我们的应用的哪个部分负责接收新推文?您可能还记得我们的Stream组件有以下componentDidMount()方法:
componentDidMount() {
SnapkiteStreamClient.initializeStream(this.handleNewTweet);
}是的,目前,我们在呈现Stream组件后启动了一个新的推特流。等等,您可能会问,“我们不是知道 React 组件应该只关心呈现用户界面吗?”您是对的。不幸的是,目前,Stream组件负责两件不同的事情:
- 呈现
StreamTweet组件 - 启动数据流
显然,这是未来可能出现的维护问题。让我们借助 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 架构仍然存在:
- 我们的应用收到一条新的 tweet。
- 它创建并分派一个新操作。
- 没有商店向调度员注册,因此没有人接收新操作;因此,什么也没有发生。
现在你们可以清楚地看到,反应和 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组件如何始终拥有最新推文:
- 我们使用
getTweet()方法将组件的初始 tweet 设置为从TweetStore获得的最新 tweet。 - 然后,我们聆听
TweetStore中的变化。 - 当
TweetStore更改其 tweet 时,我们使用getTweet()方法将组件的状态更新为从TweetStore获得的最新 tweet。 - 当组件即将卸载时,我们停止监听
TweetStore中的更改。
这就是 React 组件如何与焊剂存储器相互作用。
在我们继续增强应用的其余部分之前,让我们先看看当前的数据流:
app.js:接收新的推文,并为每条推文呼叫TweetActionCreatorsTweetActionCreators:这会创建并发送一条新的推文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_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 creator 函数。
导航到~/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操作
现在,当我们创建了CollectionStore和CollectionActionCreators模块后,我们可以开始重构 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组件只有呈现Stream和Collection组件的render()方法。因为它不再管理 tweet 的收集,所以我们也不需要将任何属性传递给Stream和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组件可以管理最新的 tweet,Collection组件可以管理 tweet 的收集,而Application组件不再需要管理任何东西,因此它变成了一个包装Stream和Collection的容器组件附加 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,并将更少的属性传递给子组件:CollectionControls和TweetList。
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为受控表单组件。这意味着其输入值存储在组件的状态中,更新该值的唯一方法是更新组件的状态。它有一个初始值,它应该从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();
}这里的重要区别在于,当用户提交表单时,我们将创建一个新操作,在集合存储中设置一个新名称:
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组件呈现 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呈现一个推文图像,用户可以点击该图像将其添加到推文集合中。你可能已经猜到,当用户点击 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 库进一步简化应用的体系结构。