Flux 作为一种全新的方式,用于支持建立复杂的可扩展用户界面。当你在网上搜寻Flux的相关资料时,能了解到的大概也就是类似以上这些内容了。但我们该如何定义这样一种全新的方式呢?又是什么让其优于其他前端架构呢?
Flux 是一套模式
我们可能首先会在理解上遇到严酷的现实——Flux 并非软件包;其实,它是一套方便我们去遵循的架构模式。这听起来似乎令人失望,不过别难过,有很好的理由不将其作为一个框架来实现。相比实际实现,在贯穿本书的内容中,我们将会看到Flux 作为一套模式而存在的价值。现在,让我们开始了解Flux 中的一些上层架构模式吧。
1 . 数据入口
在传统前端架构设计中,我们很少考虑如何处理系统的数据入口。我们可能对此有个初步的方案,但是并不具体。例如,通过MVC(模型-视图-控制器)架构,让控制器来控制数据流。通常,这很有用。但另一方面,控制器实际控制的只是当数据已经存在后所发生的事情。那么控制器该如何在一开始就获取数据呢?如下图所示。
初看此图,似乎没什么问题。以箭头标识的数据流应该很容易跟踪。但数据从哪里来的呢?例如,通过用户事件,视图可以创建新的数据,并传递给控制器;根据各控制器之间的层次关系,一个控制器可以产生新数据并传递给另一个控制器。但关于控制器,它能自己创建数据给自己使用吗?
在类似这样的图中,这些问题看起来并不重要。但是,如果我们尝试将它扩展到拥有数百个类似组件后,数据入口在这个系统中的地位就非常重要了。因为Flux 是一种用于复杂系统的可扩展架构,所以在这种架构模式下,数据入口是十分重要的。
2 . 状态管理
状态是我们在前端开发中经常需要处理的。不幸的是,我们难以在无任何副作用的情况下整合所有的纯函数,这有两个原因:第一,我们的代码需要与DOM 有正向或反向的交互,这也是用户在界面中所能感知到的;第二,我们不能把程序里所有的数据都存在DOM中(至少现在不能),这些数据会因为时间的推移和用户的操作而发生变化。
在Web 应用中,并没有现存的状态管理的方法,但有多种方式来限制状态改变的数量,以及规定如何发生改变。例如,纯函数不能修改任何状态,它们只能创建新数据。以下是 一个类似的示例。
正如你所看到的,此处的纯函数并没有副作用,因为,任何对它们的调用,都不会导致状态的变化。如果状态的改变是不可避免的,那么为什么还要采用这种做法?我们的方法是规定状态在哪里改变。例如,如果只允许部分组件类型可以修改程序里的数据状态,这样,我们就可以掌控哪些源可以引起状态变化。
Flux 十分擅长控制状态在哪里发生改变。在后面部分,我们会看到Flux 存储器(Stores)如何管理这些状态的改变。Flux 如何管理状态的重要性所在,是它在架构层上的处理。设计一套规则来决定哪些组件类型可以变更程序数据,这让我们感到很困扰,而相对于此,通过Flux 则不需要花费什么精力来考虑在哪里更改状态。
3 . 保持同步更新
数据入口点是同步更新的重要概念。也就是说,除了管理该状态变化的来源,我们还必须要管理这些相对于其他事务的变化顺序。如果数据入口点就是我们的数据,这时候我们应该同步地将状态的改变应用到系统中的所有数据。
让我们花点时间想想这为什么如此重要。在系统中数据被异步更新时,我们必须考虑竞争条件。竞争条件可能会产生问题,因为一个数据可能依赖于另一个,如果它们以错误的顺序更新,我们会遇到一连串的问题。下图说明了这个问题。
当事务是异步的时,我们无法控制何时发生状态改变。因此,我们所能做的就是等待异步更新发生,然后检查数据,并确保满足所有的数据依赖。没有自动化工具为我们处理这些依赖,我们只能写很多代码来检查状态。
Flux 通过确保同步更新数据存储器解决了这个问题。这意味着,上图所示的情况不会发生。下图更好地展示了Flux 是如何处理当今典型的JavaScript 应用程序中的数据同步问题的。
4 . 信息架构
我们常常忘记自己置身于IT 行业,应该围绕信息发展技术。然而近期,风向大变,在新形势下,我们在探讨信息之前被迫考虑实现。通常,应用中所用的数据源暴露出的数据,并不是用户想要的。我们需要JavaScript 将这些原始的数据变成用户可接受的数据。这就是我们的信息架构。
这是否意味着Flux 被用于设计信息架构,而不是软件架构?并非如此。实际上,Flux组件被实现为真实软件的组件,用于执行实际计算。诀窍是,Flux 模式使我们可以将信息架构作为首要的设计考量。不是非得通过各种组件及其实现问题来进行筛选,而是我们可以确保得到正确的信息给用户。
一旦我们的信息架构初具规模,更大的应用程序就会接踵而至,作为一种我们试图传达给用户信息的自然扩展。从数据中产生信息是困难的部分。我们不仅仅需要从大量的数据源中提取信息,并且这些信息也必须对用户产生价值。在任何项目中犯这种错误都将面临巨大风险。当处理正确时,我们就可以继续处理特定的应用程序组件,如按钮控件的状态等。
Flux 架构保持数据在存储器中进行转换。存储器是一个信息工厂,原始的数据进入,新的信息产出。存储器控制数据如何进入系统、同步状态变化、定义状态如何变化。当我们深入了解存储器后,将看到它们如何成为信息架构的支柱。
Flux 并不是一个框架
现在,我们已经对Flux 的上层模式进行了一定的探索,让我们再来想一下:什么是Flux?其实,它只是一组可以应用到前端JavaScript 程序中的架构模式。因为Flux 将信息放在首位,从而使其具有良好的扩展性。信息是软件中很难扩展的一部分,而Flux 推进了对信息架构的处理。
那么,为什么不将Flux 模式实现为一个框架呢?如果这样的话,Flux 需要提供一个标准实现给大家使用,像其他的大型可扩展开源项目一样,其代码也需要随着项目的发展而不断更新。
现在主要的问题是,Flux 是在架构层上运行的,它用于解决阻碍已有程序扩展的信息问题,以满足用户需求。如果Facebook 决定以一个框架的形式去发布Flux,那么就会遇到类似其他框架发展的困扰。例如,一些框架的组件不能在工作中以最适合的方式实现,如果不侵入框架,那就很难去实现一个更好的方案。
Facebook 决定放弃实现完整的Flux 是多么棒啊!他们确实提供了少量的Flux 组件实现,但仅供参考。这些实现是有用的,但主要用来作为我们理解运作机制的起点,例如分发器如何像预期那样工作。我们可以随意以我们了解的相同的Flux 架构模式来进行实现。
Flux 并不是一个框架,这是否意味着我们需要去自行实现Flux?不,不需要。实际上,开发者正在实现Flux 库,并将它们开源。一些Flux 库很贴近Flux 的架构模式;另一些Flux实现库有自己独到的理解,使用它们并不会有错,只要合适就行。Flux 模式旨在基于JavaScript 开发解决通用概念问题,因此在深入探讨Flux 实现之前要掌握其定义。
Flux 的设计思路问题解决方案
如果Flux 只是架构模式的集合,而不是一个软件框架,那么它能解决什么样的问题呢?我们将从架构角度来看Flux 所能解决的设计思路问题,包括:单向数据流、可追溯性、一致性、组件分层和低耦合组件。这些问题都会在我们的软件中产生风险,尤其是大型可扩展应用。Flux 可帮助我们摆脱这些问题。
1 . 数据流向
我们正在建立一个信息架构,使得具有复杂功能的应用能够在此之上构建。数据流入系统,并最终到达终点,从而结束整个流程。在入口点和终止点之间所发生的就决定了Flux架构的数据流,如下图所示。
数据流的概念是一个很好的抽象,因为这可以很好地去可视化数据的流向,你可以很清楚地描述它如何进入系统,然后从一个点移动到另一个点,最终流动停止。但在它之前,会有些副作用发生,那就是上图的中间块所关心的问题,因为我们不能确切地知道这个数据流是如何到达终点的。
比方说,我们的架构并不对数据流有任何限制。任何组件都允许将数据传递给其他组件,无论组件的层次在哪里。请看下图。
正如你所见,我们的系统已经很明确地定义了数据的入口和出口。这太棒了,因为这意味着我们可以很自信地说出流经系统的数据流。但问题是,系统中各组件之间的数据流并没有在这张图中展现。图中数据流是没有方向可循的,更确切地说,是多方向的,这糟糕透了。
Flux 是一个单向数据流架构。这意味着,上图中的组件层是不可能出现的。现在的问题是,为什么说多向数据流不好?有时候,我们会觉得数据在各组件之间以任意方向传递
是很方便的,这并不是个问题,因为传递数据不会破坏我们的架构。然而,当数据在系统中的移动是多方向的时,我们需要花更多的精力去为它们同步。简单来说,如果数据没有按照一致的方向进行流动,就有出错的可能。
Flux 强制数据流的方向,因而降低了因组件更新顺序不当而破坏系统的可能性。无论什么数据进入系统,都应当按照同样的顺序流入系统,如下图所示。
2 . 可回溯性
我们知道,当数据流单向地从系统进入组件中的时候,很容易预测和跟踪所有可能会产生的影响。相反,当一个组件向其他任何一个组件发送数据的时候,却很难捕捉到数据是如何到达的。为什么会这样?通过调试器,我们现在很容易地可以在运行时遍历任何一级,无论有多复杂。但这个概念的问题在于,我们只是调试我们所需要的。
Flux 架构中的数据流本质上是可预测的。这对于许多活动设计都是十分重要的,而不只是局限于调试。所以在用了Flux 架构的应用中,程序开发者能很直观地知道即将发生什么。这种预期是很关键的,因为这样可避免让我们设计出死胡同一样的程序。当我们能够很容易地弄清楚因果时,就可以将大部分时间花在构建应用的功能上,因为这才是用户真正关心的。
3 . 通知的一致性
在Flux 应用中,我们从一个组件向另一个组件发送数据时,需要保持数据流向的一致性。在保持一致的时候,还需要考虑系统中的数据流向机制。
例如,分发和订阅就是一个在组件间通信十分流行的机制。这种方法所带来的好处就是,我们的组件可以相互通信,并且保持一定程度上的解耦。事实上,这在前端开发中是相当普遍的,因为组件的通信都是由用户的事件所驱动的。这些事件可以被认为是“即发即弃”的。任何其他组件想在某种方式上响应这些事件,都需要去订阅这个特定的事件。
分发和订阅的机制虽然有它的优点,但它在架构上也遇到了挑战,尤其是在大型复杂的应用中。例如,为了开发某项新功能,我们增加了几个新组件,但是以下这几点一直会困扰着我们:这些组件应该接受来自已有组件的消息更新吗?它们会得到这些更新响应的通知吗?它们应该先响应吗?这就是复杂性所带来的数据依赖问题。
分发和订阅带来的另一个挑战就是,我们有时需要先订阅一个通知,而后取消该订阅。这样,当我们试图在拥有众多组件的系统中,去尝试保持组件完整的生命周期,不仅十分困难,而且会增加丢失事件捕获的几率,从而破坏了一致性。
Flux 方法,则是通过维持一个静态的内部组件信息树,来规避上面的问题,从而将消息分发到每个组件中去。换句话说,程序开发者并不需要去挑选组件所需要订阅的事件,而只需要区分出分发给它们的事件中哪些是相关的,剩下的则可以忽略。下面是关于Flux如何分发消息的示意图。
Flux 分发器给每个组件发送事件,没有其他机制可以绕过这种方式。我们需要实现组件内的逻辑来判断此消息是否有用,以取代对消息结构的篡改而导致的难以扩展的问题。并在组件内部来声明对其他组件的依赖,这样对接下来的数据流向很有帮助。
4 . 简捷的架构分层
分层是一种对组件进行组织的很好的架构方式。一方面是因为分层能够对应用的各种模块进行明确的分类,另一方面是因为分层可以对通信路径做出限制。后一点与Flux 架构尤其相关,因为保持其数据流的单向性是十分重要的,而对分层做限制比对独立组件做限制要容易得多。下面所示的是Flux 分层的示意图。
上图主要描绘了数据在三个分层之间是如何流动的。(微信后台回复“Flux 组件”,我们将会对Flux 组件的类型进行引导性解释。)
从上图中可以看到,数据从一个分层流向下一个分层,并且保持同一个方向。Flux 仅有几层,应用的规模以组件数量来衡量,而分层的数量仍然是固定的。当给一个已经很庞大的应用增添新特性时,会使其复杂度达到上限。所以,除了限制分层数量和数据流方向,Flux 架构也限制了哪些分层可以与其他分层进行通信。
例如,动作层(Action Layer)可以与视图层(View Layer)进行通信,并且该通信是单向的。我们可以编写Flux 设计的分层,并且不允许跳过某个分层。确保一个分层只能与位于其下紧邻的分层进行通信,可以避免无序引入的代码问题。
5 . 低耦合渲染
Flux 设计的一个亮点在于架构不用关心UI 元素如何被渲染,也就是说,视图层与架构的其他部分是低耦合的。这样设计是有原因的。
Flux 首先是一个信息架构,其次才是一个软件架构。我们将首先学习其作为信息架构的思想,最后会学习其作为软件架构的思想。我们知道,视图技术的缺点是它会对架构的其他部分产生副作用,例如一个和DOM 进行特殊交互的视图就会产生这样的影响,所以,一旦我们决定使用这项技术,它势必会对信息架构的组织方式产生影响。这并不一定是件坏事,但它将导致我们对最终呈现给用户的信息做出妥协。
实际上,我们真正应该思考的是信息本身,以及信息是如何变化的。哪些相关行为会导致这些变化?数据之间是如何依赖的?Flux 不受当下浏览器技术的限制,这使得我们可以优先关注信息本身。因此,在信息架构中插入视图使其变为软件产品是很容易的。
本文选自《Flux架构》,点此链接可在博文视点官网查看此书。
想及时获得更多精彩文章,可在微信中搜索“博文视点”或者扫描下方二维码并关注。