如果您以前使用过 React,您可能已经听说过 Flux。如果没有,不要担心。Flux 是用于构建 React 用户界面的架构模式。我们将从一个方向的数据流模式开始,该模式将引导我们不断变化。让 Flux 滴答滴答的每一点都很重要,我强烈建议您在本章花些时间。您至少应该了解两点:如何分离代码以及如何使用 Flux 将应用程序拆分为多个部分。这些连接在一起的小型服务负责现代移动应用程序所需的一切。
在我们深入研究 Flux 架构之前,让我们先看看这个模式的历史背景。我想让你明白为什么要引入它。
看着 Facebook 开发者谈论 Flux 架构,我有一种直觉,他们从模型视图控制器(MVC模式切换到 Flux。MVC 模式将业务模型与视图标记和编码逻辑分离。逻辑由一个称为控制器的函数封装,它将工作委托给服务。因此,我们说我们的目标是精益控制器。
然而,在更大的范围内,比如在 Facebook 上看到的,看起来这种模式是不够的。由于它允许双向数据流,因此很快就变得难以理解,甚至难以跟踪。由事件引起的一个更改可以在整个应用程序中循环并级联效果。想象一下,如果你必须在这样一个架构中找到一个 bug。
React 针对上述问题的解决方案从单向数据绑定开始。这意味着视图图层由组件维护,只有组件可以更新视图。生成的本机代码由组件的render
函数计算并显示给最终用户。如果视图层需要响应用户的操作,它只能调度由组件处理的事件。不能直接改变状态或道具。
让我们看看下图,它说明了这个概念:
App块表示本机视图层的状态。在图中,组件简化为:道具、状态、render
函数和事件侦听器。一旦道具或状态发生任何变化,观察者就会调用render
函数来更新本机视图。一旦用户执行了一个操作,相应的事件就会被分派,然后由事件侦听器拾取。
在双向数据绑定模式中,App层不需要调度事件。它可以直接修改组件的状态。我们也可以用事件侦听器来模拟。其中一个例子是受控输入,我们在第 2 章、视图模式中了解到了这一点。
"With great freedom comes great responsibility."
你可能已经听过这句话了。这种情绪适用于我们分派和处理的事件。让我们来讨论一些问题。
首先,要侦听事件,您需要创建一个事件侦听器。什么时候应该创建它?通常,我们在带有标记的组件中创建事件侦听器,并使用onClick={this.someEventListener}
进行注册。如果此事件需要对完全不同的组件进行更改,该怎么办?在这种情况下,我们需要将侦听器提升到组件树中的某个容器中。
当我们这样做的时候,我们注意到我们将越来越多的组件紧密地耦合在一起,将越来越多的侦听器传递给道具链。如果可能的话,这是我们想要避免的噩梦。
因此,Flux 引入了 Dispatcher 的概念。Dispatcher 向所有注册的组件发送一个事件。这样,每个组件都可以对与其相关的事件做出反应,同时忽略不相关的事件。我们将在本章后面讨论这个概念。
如您所见,使用单向数据绑定是不够的。我们可能会很快陷入模拟双向数据绑定的陷阱,或者遇到事件问题,如前一节所述。
一切都归结为一个问题:我们能应付吗?对于大规模应用,答案通常是否。我们需要一个可预测的模型,以确保我们能够迅速发现发生了什么以及为什么。如果这些事件在我们的应用程序中到处发生,开发人员显然必须花费大量时间找出导致检测到的 bug 的具体原因。
我们怎样才能缩小这个问题的范围?答案是限制。我们需要对事件流进行一些限制。这就是 Flux 架构的用武之地。
Flux 架构对组件之间的通信造成了一些限制。主要原则是无处不在的行动。应用程序视图层通过向调度器发送操作对象来响应用户操作。调度员的角色是将每个动作发送到订阅的存储。您可以有许多商店,每个商店可以根据用户的操作采取不同的行动。
例如,假设您正在构建一个基于购物车的应用程序。用户可以点击屏幕将某些项目添加到购物车中,相应的操作将被调度,您的购物车商店将对此作出反应。此外,分析商店可能会跟踪此类项目是否已添加到用户的购物车中。两者都对同一动作对象做出反应,并根据需要使用信息。最后,将使用新状态更新视图图层。
为了增强 MVC 体系结构,让我们提醒自己它的外观:
操作由其各自的控制器处理,这些控制器可以访问模型(数据表示)。视图通常与模型耦合,并可根据需要对其进行更新。
When I was reading this architecture for the first time, I struggled to understand it. Let me give you some tips if you haven't work with it yourself yet:
- 操作:将其视为用户的操作,例如点击按钮、滚动和导航更改。
- 控制器:这是负责处理操作和显示适当本机视图的部件。
- 模型:这是一个数据结构,保存与视图分离的信息。视图需要一个模型来根据设计直观地显示它。
- 视图:这是最终用户看到的。该视图描述了所有标记代码,这些代码稍后可以进行样式化。视图有时与样式耦合,称为一个整体。
随着应用程序的增长,这个小小的体系结构迟早会变成如下所示:
在这个图中,我试图通过在模型的结构中创建缩进来显示一些模型依赖于其他模型。视图也是如此。这不应该被认为是坏的。一般来说,这种架构在某种程度上是有效的。当您发现一个 bug 并发现自己无法定位出哪里出了问题以及为什么出了问题时,就会出现问题。更准确地说,你失去了对信息流的控制。你发现自己身处一个如此多的事情同时发生的地方,以至于你无法轻易预测失败的原因,也无法预测失败的原因。有时,您甚至很难重现错误或验证它是否是一个错误。
查看图表,您可以发现模型视图通信中的一个问题:它是双向的。这就是软件多年来一直在做的事情。一些才华横溢的人意识到,在客户机环境中,我们可以承受单向数据流。这将有效地使体系结构具有可预测性。如果我们的控制器只有一系列的输入数据,然后应该提供一个新的视图状态,它会感觉更清晰。单元测试可以提供一系列数据,例如输入,并在输出上断言。类似地,跟踪服务可以记录任何错误并保存输入数据序列。
让我们看看数据流流量:
所有操作都通过 Dispatcher,然后发送到注册的存储回调。最后,将存储内容映射到视图。
这可能会随着时间的推移而变得复杂,如下图所示:
您可能有各种存储,用于不同的视图或视图分区。我们的视图被合成为用户看到的最终视图。如果某些内容发生更改,则会向存储区发送另一个操作。这些存储计算新状态并刷新视图。
这要简单得多。我们现在可以跟踪操作,并查看哪些操作导致了商店中不必要的更改。
在深入研究 Flux 之前,让我们使用 Flux 架构创建一个简单的应用程序。为此,我们将使用 Facebook 提供的 Flux 库。该库包含我们根据新 Flux 流使应用程序运行所需的所有部分。安装 Flux 和immutable
库。immutable
对于进一步的优势也至关重要,因为我们越来越熟悉 Flux:
yarn add flux immutable
我们将在 Flux 中构建的应用程序是一个任务应用程序。我们已经创建的一个将需要一些调整。首先要做的是创建Dispatcher
、任务存储和任务操作。
Flux 包为我们的体系结构提供了基础。例如,让我们为 Tasks 应用程序实例化Dispatcher
:
// src / Chapter 4_ Flux patterns / Example 1 / src / data / AppDispatcher.js
import { Dispatcher } from 'flux';
export default new Dispatcher();
Dispatcher
将用于分派操作,但我们需要先创建操作。我将遵循文档建议并创建操作类型作为第一步:
// src / Chapter 4_ Flux patterns / Example 1 / src / data / TasksActionTypes.js
const ActionTypes = {
ADD_TASK: 'ADD_TASK'
};
export default ActionTypes;
现在我们已经创建了类型,我们应该继续操作创建者本身,如下所示:
// src / Chapter 4_ Flux patterns / Example 1 / src / data / TaskActions.js
import TasksActionTypes from './TasksActionTypes';
import AppDispatcher from './AppDispatcher';
const Actions = {
addTask(task) {
AppDispatcher.dispatch({
type: TasksActionTypes.ADD_TASK,
task
});
}
};
export default Actions;
到目前为止,我们已经有了行动和工具来调度它们。缺少的部分是Store
,它将对动作做出反应。让我们创建TodoStore
:
// src / Chapter 4_ Flux patterns / Example 1 / src / data / TaskStore.js
import Immutable from 'immutable';
import { ReduceStore } from 'flux/utils';
import TasksActionTypes from './TasksActionTypes';
import AppDispatcher from './AppDispatcher';
class TaskStore extends ReduceStore {
constructor() {
super(AppDispatcher);
}
getInitialState() {
return Immutable.List([]);
}
reduce(state, action) {
switch (action.type) {
case TasksActionTypes.ADD_TASK:
return state; // <= placeholder, to be replaced!!!
default:
return state;
}
}
}
export default new TaskStore();
为了创建存储,我们从flux/utils
导入ReduceStore
。应该扩展 store 类以提供必要的 API 方法。我们将在后面的部分中介绍这些。现在,您应该已经发现您需要使用构造函数中的super
将Dispatcher
传递给上层阶级。
另外,让我们实现ADD_TASK
的reduce
案例。可以将相同的流调整为要创建的任何其他动作类型:
reduce(state, action) {
switch (action.type) {
case TasksActionTypes.ADD_TASK:
if (!action.task.name) {
return state;
}
return state.push({
name: action.task.name,
description: action.task.description,
likes: 0
});
default:
return state;
}
}
由于我们现在拥有 Flux 架构的所有位(Action
、Dispatcher
、Store
和View
,我们可以将它们连接在一起。为此,flux/utils 公开了一个方便的容器工厂方法。请注意,我将重用上一个任务应用程序中的视图。为了清晰起见,我已移除了 likes 计数器:
// src / Chapter 4 / Example 1 / src / App.js
import { Container } from 'flux/utils';
import TaskStore from './data/TaskStore';
import AppView from './views/AppView';
const getStores = () => [TaskStore];
const getState = () => ({ tasks: TaskStore.getState() });
export default Container.createFunctional(AppView, getStores, getState);
If you have not followed this book from the start, please note that we are using container component here. This pattern is fairly important to understand and we went through it in Chapter 1, React Component Patterns. There, you can learn how to create container components from scratch.
我们的应用程序现在配备了 Flux 架构工具。我们需要做的最后一件事是重构以遵循我们的新原则。
为此,我们的任务如下:
- 使用任务初始化存储,而不是将 JSON 数据直接传递给视图。
- 创建一个添加任务表单,在提交时发送一个
ADD_TASK
操作。
第一个相当简单:
// src / Chapter 4_ Flux patterns / Example 1 / src / data / TaskStore.js
import data from './tasks.json';
class TaskStore extends ReduceStore {
// ...
getInitialState() {
return Immutable.List([...data.tasks]);
}
// ...
第二个要求我们使用Input
组件。让我们创建一个单独的文件来负责整个功能。在这个文件中,我们将创建 name 和 description 的状态、一个分派ADD_TASK
动作的handleSubmit
函数和一个带有表单视图标记的render
函数:
// src / Chapter 4_ Flux patterns / Example 1 / src / views / AddTaskForm.js
export const INITIAL_ADD_TASK_FORM_STATE = {
name: '',
description: ''
};
class AddTaskForm extends React.Component {
constructor(props) {
super(props);
this.handleSubmit.bind(this);
}
state = INITIAL_ADD_TASK_FORM_STATE;
handleSubmit = () => {
TaskActions.addTask({
name: this.state.name,
description: this.state.description
});
this.setState(INITIAL_ADD_TASK_FORM_STATE);
};
render = () => (
<View style={styles.container}>
<TextInput
style={styles.input}
placeholder="Name"
onChangeText={name => this.setState({ name })}
value={this.state.name}
/>
<TextInput
style={styles.input}
placeholder="Description"
onChangeText={d => this.setState({ description: d })}
value={this.state.description}
/>
<Button
title="Add task"
onPress={() => this.handleSubmit()}
/>
</View>
);
}
// ... styles
功能齐全的应用程序如下所示:
现在我们已经创建了第一个遵循 Flux 架构的应用程序,是时候深入研究 API 了。
让我们以一种更形式化的方式来看 Flux 架构。下面是一个小图,显示了简化架构的外观:
Flux diagram from official documentation: https://github.com/facebook/flux
上图中的每个零件在循环链中都有自己的用途:
- Dispatcher:应用程序中发生的事情的管理者。这将管理操作并将它们提供给已注册的回调。所有操作都需要通过调度程序。调度器必须公开用于注册/注销回调的
register
和unregister
方法,并且必须公开用于调度操作的dispatch
方法。 - 存储:应用程序由多个存储组成,这些存储在 Dispatcher 中注册回调。每个存储都需要公开一个接受
Dispatcher
参数的公共constructor
方法。构造函数负责向给定的调度程序注册此存储实例。 - React 视图:上一章介绍了此主题。如果你从一开始就没有读过这本书,请看一看。
- 动作创建者:这些创建者将数据合成一个动作对象,并将其传递给调度程序。此过程可能涉及数据获取和其他获取必要数据的方法,动作创造者可能导致副作用。我们将在下一节介绍这个主题。动作创建者必须在结尾返回普通动作对象。
You can find the full API reference for each piece under the following link: https://facebook.github.io/flux/.
副作用是在被调用函数之外发生的应用程序状态更改,确切地说,是指除其返回值以外的任何状态更改。
以下是一些副作用的例子:
- 修改全局变量
- 修改父作用域链中的变量
- 在屏幕上写字
- 写入文件
- 任何网络请求,例如 AJAX 请求
This section on side effects is meant to get you ready for the next chapter, where we will talk about pure functions in the context of Redux. Also, we will push these ideas much further in Chapter 9, Functional Programming Patterns, where you will learn how we can benefit from functional programming practices, such as mutable and immutable
objects, higher order functions, and monads.
副作用操纵不是函数属性的状态。因此,当我们孤立地观察函数时,很难评估该函数是否对应用程序有任何负面影响。这不仅在单元测试中是正确的;当涉及到数学证明时,它也很麻烦。一些必须安全的大型应用程序可以努力构建一个防弹的数学模型。这类应用程序使用的数学工具超出了本书的内容。
当副作用被隔离时,可能会作为我们应用程序的数据提供者。他们可以在最佳时刻将数据“注入”到流中,从那时起,数据就被视为一个变量。从一个没有副作用的功能转到另一个。这样一个没有副作用的功能链更容易调试,在某些情况下,还可以重播。所谓重播,我指的是传递完全相同的输入数据以评估输出,并查看其是否符合业务标准。
让我们从 MVC 和 Flux 的角度来看这个概念的实际方面。
如果我们遵循经典的 MVC 架构,我们将按照如下方式分离关注点:模型、视图和控制器。此外,视图可能会获得直接更新模型的公开函数。如果发生这种情况,可能会引发副作用。
有几个地方可能会产生副作用:
- 控制器初始化
- 控制器相关服务(此服务是一个解耦的专用逻辑)
- 视图,使用作为回调公开的控制器相关服务
- 在某些情况下,在模型更新时(服务器-客户端双向模型)
我相信你甚至可以想出更多。
这种自由付出了巨大的代价。我们可以有几乎无限数量的途径与副作用交织在一起,如下所示:
- 副作用=>控制器=>模型=>视图
- 控制器=>副作用=>模型=>视图
- 控制器=>视图=>模型=>副作用
这就扼杀了我们在整个应用程序上以一种功能性的、无副作用的方式进行推理的能力。
MVC 通常如何处理这个问题?答案很简单,大多数时候这个架构并不关心它。只要我们能够断言应用程序按照单元测试的预期工作,我们就会很高兴。
但后来 Facebook 出现了,声称我们可以在前端做得更好。由于前端的特殊性,我们可以在流程上更具组织性和自以为是,而不会造成显著的性能损失。
在流量中,我们仍然保留选择副作用触发位置的自由,但我们必须尊重单向流。
Flux 中可能的副作用的一些示例包括:
- 在用户单击时下载数据,然后将其发送给 Dispatcher
- Dispatcher 在将数据发送到已注册回调之前下载数据
- 存储开始同步副作用,以保留更新所需的数据
一个好主意是强制副作用只发生在 Flux 架构中的一个位置。我们只能对动作触发器执行副作用。例如,当用户点击触发SHOW_MORE
操作时,我们首先下载数据,然后将完整对象发送给 Dispatcher。因此,调度器和任何存储都不需要执行副作用。这个好主意在**Redux Thunk 中使用。**我们将在下一章学习 Redux 和 Redux Thunk。
副作用对于理解本书中更高级的材料至关重要。现在我们已经了解了副作用,让我们进入章节总结。
总之,对于大规模应用来说,助焊剂是一项非常好的发明。它解决了经典 MVC 模式难以解决的问题。事件是单向的,这使得通信更加可预测。应用程序的域可以很容易地映射到存储,然后由域专家进行维护。
由于采用了一种经过深思熟虑的模式,包括调度器、存储和操作,所有这些功能都可用。在本章中,我们使用 Facebook 的官方图书馆flux-utils
制作了基于流量的小应用程序。
连接了所有这些部分之后,我们准备深入研究一个特定方面。有几种模式可用于将商店提升到另一个级别。其中之一是 Redux 库。我们将在下一章中探讨 Redux 提供的不同功能。
- Facebook 为什么要脱离传统的 MVC 架构? 回答:Facebook 发现了 MVC 在与 Facebook 所需的大规模合作时遇到的问题。在前端应用程序中,视图和模型紧密耦合。双向数据流使情况更糟:很难调试数据如何在模型和视图之间转换,以及哪些部分负责最终状态。
- Flux 架构的主要好处是什么? 回答:观看视频黑客方式:反思进一步阅读部分中提到的 Facebook上的 Web 应用开发,或参见替换 MVC部分。
- 你能画一张 Flux 架构图吗?您能用绘制并连接到图表的 web API 详细地完成这项工作吗? 应答:查看详细流量图部分。
- 调度员的角色是什么? 回答:如果您需要再次查看完整的解释,请查看Flux 介绍或详细 Flux 图。
- 你能举四个副作用的例子吗? 回答:勾选流量介绍。
- 在 Flux 架构中如何解耦副作用? 回答:查看中的章节,了解 Flux中的副作用。
- 官方 Flux 文档页面可在找到 https://facebook.github.io/flux/ 。
- GitHub 存储库中的流量示例可在中找到 https://github.com/facebook/flux/tree/master/examples 。
- Facebook 会议视频(F8 2014)名为黑客之路:重新思考 Facebook上的 Web 应用开发,可在**上获取 https://www.youtube.com/watch?v=nYkdrAPrdcw 。** *** React Native-Yoav Amit中的焊剂,Wix 工程技术讲座可在获取 https://www.youtube.com/watch?v=m-rMK5ZZM5k。**