构建 web 应用的过程有一个特性,它在某种程度上反映了生命本身的进化过程—它永远不会结束。与构建桥梁不同,构建 web 应用没有表示开发过程结束的自然状态。由您或您的团队决定何时停止开发过程并发布您已经构建的内容。
在本书中,我们已经到了可以停止开发 Snapterest 的地步。现在,我们有一个小的 React.js 应用,它的基本功能非常简单。
这还不够吗?
不完全是。在本书的前面,我们讨论了维护 web 应用的过程在时间和精力上比开发它的过程要昂贵得多。如果我们选择在当前状态下完成 Snapterest 的开发,我们还将选择开始维护它的过程。
我们准备好维护 Snapterest 了吗?我们是否知道它的当前状态是否允许我们以后在不进行任何重大代码重构的情况下引入新功能?
为了回答这些问题,让我们从实现细节开始,探索我们的应用架构:
app.js
文件呈现我们的Application
组件Application
组件管理一组 tweet,并呈现我们的Stream
和Collection
组件Stream
组件从SnapkiteStreamClient
库接收新的推文,并呈现StreamTweet
和Header
组件Collection
组件呈现CollectionControls
和TweetList
组件
停在那里。您能告诉我们的应用中数据是如何流动的吗?你知道它在哪里输入我们的申请表吗?一条新的推文怎么会出现在我们的收藏中?让我们更仔细地检查数据流:
- 我们使用
SnapkiteStreamClient
库在Stream
组件中接收新的 tweet。 - 这个新的 tweet 然后从
Stream
传递到StreamTweet
组件。 StreamTweet
组件将其传递给Tweet
组件,该组件渲染推特图像。- 用户单击该 tweet 图像以将其添加到其集合中。
Tweet
组件通过handleImageClick(tweet)
回调函数将tweet
对象传递给StreamTweet
组件。StreamTweet
组件通过onAddTweetToCollection(tweet)
回调函数将tweet
对象传递给Stream
组件。Stream
组件通过onAddTweetToCollection(tweet)
回调函数将tweet
对象传递给Application
组件。Application
组件向collectionTweets
对象添加tweet
并更新其状态。- 状态更新触发
Application
组件重新呈现,该组件反过来使用更新的 tweet 集合重新呈现Collection
组件。 - 然后,
Collection
组件的子组件也可以变异我们的 tweet 集合。
你感到困惑吗?从长远来看,您可以依赖这种体系结构吗?你认为它容易维护吗?我不这么认为。
让我们确定当前体系结构的关键问题。我们可以看到,新数据通过Stream
组件进入我们的 React 应用。然后它一直向下移动到组件层次结构中的Tweet
组件。然后,它会一直移动到Application
组件,在那里存储和管理它。
为什么我们要在Application
组件中存储和管理我们收集的推文?因为Application
是另外两个组件的父组件:Stream
和Collection
。他们都需要能够改变我们收集的推文。为了适应这种情况,我们的Application
组件需要将回调函数传递给两个组件:
-
Stream
组件:<Stream onAddTweetToCollection={this.addTweetToCollection} />
-
Collection
组件:<Collection tweets={collectionTweets} onRemoveTweetFromCollection={this.removeTweetFromCollection} onRemoveAllTweetsFromCollection={this.removeAllTweetsFromCollection} />
Stream
组件获取onAddTweetToCollection()
函数,将 tweet 添加到集合中。Collection
组件获得onRemoveTweetFromCollection()
函数从集合中删除一条 tweet,onRemoveAllTweetsFromCollection()
函数从集合中删除所有 tweet。
这些回调函数随后向下传播到组件层次结构,直到它们到达实际调用它们的某个组件。在我们的应用中,onAddTweetToCollection()
函数只在Tweet
组件中调用。让我们看看它需要多少次从一个组件传递到另一个组件,然后才能在一个 Ty2 T2 组件中调用它:
Application > Stream > StreamTweet > Tweet
onAddTweetToCollection()
函数未在Stream
和StreamTweet
组件中使用,但它们都将其作为属性,以便将其传递给子组件。
Snapterest 是一个小型 React 应用,因此此问题相当麻烦,但稍后,如果您决定添加新功能,此麻烦将很快成为维护的噩梦:
Application > ComponentA > ComponentB > ComponentC > ComponentD > ComponentE > ComponentF > ComponentG > Tweet
为了防止这种情况发生,我们将解决两个问题:
- 我们将更改新数据进入应用的方式
- 我们将更改组件获取和设置数据的方式
我们将重新思考在 Flux 的帮助下,数据如何在应用中流动。
Flux是 Facebook 的应用架构,它补充了 React。它不是一个框架或库,而是如何构建可伸缩的客户端应用这一常见问题的解决方案。
使用 Flux 架构,我们可以重新思考数据在应用内部的流动方式。Flux 确保我们所有的数据只在单一方向上流动。这有助于我们分析应用的工作原理,而不管它有多小或多大。有了 Flux,我们可以添加新功能,而不会破坏应用的复杂性或其心智模型。
您可能已经注意到,React 和 Flux 共享同一个核心概念单向数据流。这就是为什么他们能很好地合作。我们知道 React 组件中的数据是如何流动的,但是 Flux 是如何实现单向数据流的呢?
使用 Flux,我们将应用的关注点分为四个逻辑实体:
- 行动
- 调度员
- 商店
- 意见
动作是我们想要更改应用状态时创建的对象。例如,当我们的应用收到一条新 tweet 时,我们创建一个新动作。action 对象有一个type
属性,用于标识它是什么动作以及应用需要转换到新状态的任何其他属性。下面是一个动作对象的示例:
const action = {
type: 'receive_tweet',
tweet
};
如您所见,这是一个receive_tweet
类型的动作,它具有tweet
属性,这是我们的应用收到的一个新 tweet 对象。通过查看操作的类型,您可以猜测此操作在应用状态中所代表的更改。对于我们的应用接收到的每一条新 tweet,它都会创建一个receive_tweet
动作。
这一行动将走向何方?我们的应用的哪个部分会执行此操作?动作被分派到存储区。
存储区负责管理应用的数据。它们提供访问数据的方法,但不提供更改数据的方法。如果要更改存储中的数据,必须创建并分派一个操作。
我们知道如何创建一个动作,但如何调度它?顾名思义,您可以使用调度器来完成此任务。
调度员负责将所有操作分派到所有门店:
- 所有存储都向调度器注册。它们提供回调函数。
- 所有操作都由调度程序调度到所有已向调度程序注册的存储区。
这就是 Flux 体系结构中数据流的外观:
Actions > Dispatcher > Stores
您可以看到,dispatcher 在数据流中扮演着中心元素的角色。所有动作都由它来调度。商店向它登记。所有操作都是同步调度的。不能在前一次行动调度的中间调度一个动作。在 Flux 体系结构中,任何操作都不能跳过 dispatcher。
现在让我们实现这个数据流。我们将首先创建一个调度器。Facebook 为我们提供了一个调度器的实现,我们可以重用它。让我们利用这一点:
-
Navigate to the
~/snapterest
directory and run the following command:npm install --save flux
flux
模块带有Dispatcher
功能,我们将重用该功能。 -
接下来,在我们项目的
~/snapterest/source/dispatcher
目录中创建一个名为dispatcher
的新文件夹。现在在其中创建AppDispatcher.js
文件:import { Dispatcher } from 'flux'; export default new Dispatcher();
首先,我们导入 Facebook 提供的Dispatcher
,然后创建并导出一个新的实例。现在我们可以在应用中使用这个实例。
接下来,我们需要一种创建和分派操作的方便方法。对于每个操作,让我们创建一个函数来创建和分派该操作。在 Flux 体系结构中,这些函数称为 action creator 函数。
让我们在项目的~/snapterest/source/actions
目录中创建一个名为actions
的新文件夹。然后,我们将在其中创建TweetActionCreators.js
文件:
import AppDispatcher from '../dispatcher/AppDispatcher';
function receiveTweet(tweet) {
const action = {
type: 'receive_tweet',
tweet
};
AppDispatcher.dispatch(action);
}
export { receiveTweet };
我们的动作创建者需要一个调度器来分派动作。我们将导入之前创建的AppDispatcher
:
import AppDispatcher from '../dispatcher/AppDispatcher';
然后,我们将创建我们的第一个动作创建者receiveTweet()
:
function receiveTweet(tweet) {
const action = {
type: 'receive_tweet',
tweet
};
AppDispatcher.dispatch(action);
}
receiveTweet()
函数以tweet
对象为参数,创建type
属性设置为receive_tweet
的action
对象。它还将tweet
对象添加到我们的action
对象中,现在每个商店都会收到这个tweet
对象。
最后,receiveTweet()
动作创建者通过调用AppDispatcher
对象上的dispatch()
方法来分派我们的action
对象:
AppDispatcher.dispatch(action);
dispatch()
方法将action
对象分派给AppDispatcher
调度器注册的所有存储。
然后我们输出我们的receiveTweet
方法:
export { receiveTweet };
到目前为止,我们已经创建了AppDispatcher
和TweetActionCreators
。接下来,让我们创建我们的第一个商店。
如前所述,在 Flux 体系结构中存储和管理数据。它们向 React 组件提供数据。我们将创建一个简单的存储,用于管理我们的应用从 Twitter 接收的新 tweet。
在我们项目的~/snapterest/source/stores
目录中创建一个名为stores
的新文件夹。然后,在其中创建TweetStore.js
文件:
import AppDispatcher from '../dispatcher/AppDispatcher';
import EventEmitter from 'events';
let tweet = null;
function setTweet(receivedTweet) {
tweet = receivedTweet;
}
function emitChange() {
TweetStore.emit('change');
}
const TweetStore = Object.assign({}, EventEmitter.prototype, {
addChangeListener(callback) {
this.on('change', callback);
},
removeChangeListener(callback) {
this.removeListener('change', callback);
},
getTweet() {
return tweet;
}
});
function handleAction(action) {
if (action.type === 'receive_tweet') {
setTweet(action.tweet);
emitChange();
}
}
TweetStore.dispatchToken = AppDispatcher.register(handleAction);
export default TweetStore;
TweetStore.js
文件实现了一个简单的存储。我们可以将其分为四个逻辑部分:
- 导入依赖模块,创建私有数据和方法
- 使用公共方法创建
TweetStore
对象 - 创建操作处理程序并向 dispatcher 注册存储
- 将
dispatchToken
分配给我们的TweetStore
对象并将其导出
在我们商店的第一个逻辑部分中,我们只是导入商店需要的依赖模块:
import AppDispatcher from '../dispatcher/AppDispatcher';
import EventEmitter from 'events';
因为我们的存储需要向调度器注册,所以我们导入AppDispatcher
模块。接下来,我们导入EventEmitter
类,以便能够从存储中添加和删除事件侦听器:
import EventEmitter from 'events';
导入所有依赖项后,我们将定义存储管理的数据:
let tweet = null;
TweetStore
对象管理一个简单的 tweet 对象,我们最初将其设置为null
,以确定我们还没有收到新的 tweet。
接下来,让我们创建两个私有方法:
function setTweet(receivedTweet) {
tweet = receivedTweet;
}
function emitChange() {
TweetStore.emit('change');
}
setTweet()
函数使用receiveTweet
对象更新tweet
。emitChange
函数在TweetStore
对象上发出change
事件。这些方法是TweetStore
模块的专用方法,在模块之外无法访问。
TweetStore.js
文件的第二个逻辑部分是创建TweetStore
对象:
const TweetStore = Object.assign({}, EventEmitter.prototype, {
addChangeListener(callback) {
this.on('change', callback);
},
removeChangeListener(callback) {
this.removeListener('change', callback);
},
getTweet() {
return tweet;
}
});
我们希望我们的存储能够在其状态发生更改时通知应用的其他部分。我们将为此使用事件。每当我们的商店更新其状态时,它就会发出change
事件。任何对商店状态变化感兴趣的人都可以收听此change
事件。他们需要添加事件侦听器功能,我们的存储将在每个change
事件中触发该功能。为此,我们的存储定义了添加事件侦听器(侦听change
事件)的addChangeListener()
方法和删除change
事件侦听器的removeChangeListener()
方法。但是,addChangeListener()
和removeChangeListener()
依赖于EventEmitter.prototype
对象提供的方法。所以我们需要将方法从EventEmitter.prototype
对象复制到TweetStore
对象。这就是Object.assign()
函数的作用:
targetObject = Object.assign(
targetObject,
sourceObject1,
sourceObject2
);
Object.assign()
将sourceObject1
和sourceObject2
拥有的房产复制到targetObject
后返回targetObject
。在我们的例子中,sourceObject1
是EventEmitter.prototype
,而sourceObject2
是定义我们商店方法的对象文字:
{
addChangeListener(callback) {
this.on('change', callback);
},
removeChangeListener(callback) {
this.removeListener('change', callback);
},
getTweet() {
return tweet;
}
}
Object.assign()
方法返回targetObject
以及从所有源对象复制的属性。这就是我们的TweetStore
对象所做的。
您是否注意到我们将getTweet()
函数定义为TweetStore
对象的一种方法,而setTweet()
函数没有这样做。为什么呢?
稍后,我们将导出TweetStore
对象,这意味着它的所有属性将可供应用的其他部分使用。我们希望他们能够从TweetStore
获取数据,但不能通过调用setTweet()
直接更新数据。相反,在任何存储中更新数据的唯一方法是创建一个操作并将其(使用调度程序)调度到已向该调度程序注册的存储中。当存储获得该操作时,它可以决定如何更新其数据。
这是 Flux 架构的一个非常重要的方面。商店完全可以控制其数据的管理。它们只允许应用中的其他部分读取该数据,而不允许直接写入。只有操作才能改变存储中的数据。
TweetStore.js
文件的第三个逻辑部分是创建一个操作处理程序,并向调度器注册存储。
首先,我们创建 action handler 函数:
function handleAction(action) {
if (action.type === 'receive_tweet') {
setTweet(action.tweet);
emitChange();
}
}
handleAction()
函数将action
对象作为参数,并检查其类型属性。在 Flux 中,所有商店都会获得所有动作,但并非所有商店都对所有动作感兴趣,因此每个商店都必须决定自己感兴趣的动作。为此,存储必须检查操作类型。在我们的TweetStore
存储中,我们检查动作类型是否为receive_tweet
,这意味着我们的应用收到了一条新的推文。如果是这样,那么我们的TweetStore
调用它的私有setTweet()
函数,用一个来自action
对象的新对象更新tweet
对象,即action.tweet
。当商店更改其数据时,它需要告诉所有对数据更改感兴趣的人。为此,它调用它的私有emitChange()
函数,该函数发出change
事件并触发应用中其他部分创建的所有事件侦听器。
我们的下一个任务是向调度器注册TweetStore
存储。要向 dispatcher 注册存储,您需要调用 dispatcher 的register()
方法,并将存储的操作处理程序函数作为回调函数传递给它。每当分派器分派一个动作时,它都会调用该回调函数并将动作对象传递给它。
让我们来看看我们的例子:
TweetStore.dispatchToken = AppDispatcher.register(handleAction);
我们在AppDispatcher
对象上调用register()
方法,并将handleAction
函数作为参数传递。register()
方法返回标识TweetStore
存储的令牌。我们将该令牌保存为TweetStore
对象的属性。
TweetStore.js
文件的第四个逻辑部分是导出TweetStore
对象:
export default TweetStore;
这就是创建简单商店的方法。现在,既然我们已经实现了我们的第一个 action creator、dispatcher 和 store,那么让我们重温一下 Flux 体系结构,看看它是如何工作的:
- 商店向调度器注册自己。
- 动作创建者通过调度器创建动作并将其分派到商店。
- 存储检查相关操作并相应地更改其数据。
- 存储通知所有正在侦听数据更改的人。
你可能会说,这是有道理的,但是什么触发了动作创造者呢?谁正在收听商店更新?这些都是很好的问题。答案在下一章中等待着你。
在本章中,您分析了 React 应用的体系结构。您学习了 Flux 体系结构背后的核心概念,并实现了一个调度器、一个动作创建者和一个存储。
在下一章中,我们将把它们集成到我们的 React 应用中,并使我们的体系结构为维护做好准备。