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