这篇文章是正在进行的Angular
架构系列的一部分,我们将在视图层和服务层一级介绍常见的设计问题和解决方案。 这是完整系列:
- 视图层架构-智能组件与展示组件
- 视图层架构-容器与展示组件常见的设计缺陷
- 服务层架构-如何使用
Observable
数据服务构建Angular
应用程序 - 服务层架构-
Redux
和Ngrx Store
-何时使用Store
?为什么? - 服务层架构-
Ngrx Store
-架构指南
Angular
服务层 - store
架构
当前版本的Angular
是对以前版本(AngularJs
)的改进。 视图层比以往更容易学习和使用。
但是服务层(也称为数据层)实际上是应用程序的功能核心提供了许多选项:
- 我们应该如何构建服务层?
- 我们应该使用
store
吗? - 我们应该使用
Redux
吗? - 我们应该使用普通的
RxJs
吗? - 那么
NgRx Store
呢?
在Angular
世界中非常受欢迎的一件事是store
解决方案。
它们起源于React
世界并经历了通常的技术采用曲线:大规模采用,认识到它不是一切的最终解决方案,然后在某些情况下使用它而不是其他情况。
为什么store
在React
中如此受欢迎?
为什么store
在React
世界变得如此受欢迎,是否有特定原因或是由于多种原因? 这些原因是否也适用于Angular
世界,还是有替代解决方案? store
解决了哪些问题?
您是否注意到有很多关于store
解决方案的信息,但关于我们何时应该使用它们以及为什么使用它们的信息有限? 让我们回顾一下这些问题。
目录
在这篇文章中,我们将讨论以下主题:
- 何时使用
Redux
或store
? - 我们经常需要
store
吗? - 为什么
Redux
在React
世界中如此受欢迎? Redux
解决的问题是否也出现在Angular
世界中?store
解决了什么问题?- 什么类型的应用程序受益于
store
解决方案? - 什么类型的工具与
store
解决方案相关联? React
和Angular
中的单向数据流store
和可测试性store
和表现store
和工具Redux
vsMobx
- 与
Mobx
和CycleJs
进行工具比较 - 关于方法的提案
- 结论和建议
注意:下面有一个带有Ngrx DevTools
演示的视频。 此外,您可能希望在此另一篇文章中介绍集中存储模式和Ngrx Store
- Angular Ngrx
速成课程第1部分:Ngrx Store - Learn It By Understanding The Original Facebook Counter Bug。
何时使用Redux
或store
?
store
起源于Redux
世界,所以这将是最先看的知识,然后从那里学习。
让我们来看看React
生态系统的react-howto
指南,有什么建议? 这是一个重要的引用:
你可能听说过
Flux
。 那里有关于Flux
的大量错误信息。 许多人坐下来构建一个应用程序,并希望定义他们的数据模型,他们认为他们需要使用Flux
来完成它。 这是采用Flux
的错误方法。
Redux
的创建者也有这个著名的文章——You Might Not Need Redux,它可以应用于任何store
解决方案。
然后在React How-To
中还有另一个声明,它似乎同样适用于原始的Flux
,Redux
,Ngrx Store
或任何store
解决方案:
你会知道什么时候需要
Flux
。 如果您不确定是否需要它,则不需要它。
基于此,看起来store
不建议由一些原创作者系统使用。 在这些文章中,我们得到的印象是创作者似乎担心store
被视为一种适合所有解决方案的store
。
但后来我们遇到了像这样的文章——I Always Seem to Need Redux
不知何故,即使store
被他们自己的创作者谨慎推荐,他们仍然在React
世界中大规模采用。
为什么会这样? 我们试着回答这个问题。
什么时候建议使用Flux
或Redux
?
如果我们深入研究React How-To
的文档,我们会得到一些关于何时从Flux
中受益的迹象:
React
组件按层次结构排列。 大多数情况下,您的数据模型也遵循层次结构。 在这些情况下,Flux
不会给你带来太多帮助。 但是,有时您的数据模型不是分层的。 当您的React
组件开始接收感觉无关的道具,或者您有少量组件开始变得非常复杂时,您可能想要查看Flux
。
此外,如果我们深入研究问题,我们也会得到这些建议。 如果符合以下情况,建议使用类似于store
:
您有一段数据需要在您的应用中的多个位置使用,并通过属性传递它会使您的组件违反单一责任原则(即使他们的界面变得不那么有意义)
但也有这种情况:
有多个独立的参与者(通常是服务器和最终用户)可能会改变这些数据
因此,有几种情况建议将store
解决方案与React
一起使用。 那么让我们看看它如何适合Angular
。
具有并发更新的store
和应用程序
如果我们仅基于最后一部分,只有少数应用程序即具有服务器推送要求的应用程序将受益于Flux
。 因为通常这时候我们有多个角色更新相同的数据,这就是源自Flux
的原始Facebook
计数问题。
有关原始计数器问题的更多详细信息,请查看最初的Flux
演讲。
请注意,我们不需要让服务器推送到这种情况,使用setInterval
进行长轮询或修改setTimeout
中的数据会导致我们遇到相同的情况:多个角色同时编辑相同的数据。
我们可以肯定地说很多应用都没有这个问题,对吧? 这是一个我们需要设计的重要问题,如果存在,但大多数应用程序都有它吗? 可能不是,只有某类应用程序。
但是为什么Redux
在React
世界中被普遍采用呢? 这留下了另一个原因。
Redux
解决的最常见问题是什么?
Redux
还解决了extraneous props
问题。 这一直是Redux
在React
世界中如此受欢迎的主要原因之一。
在Angular
中,props feels extraneous
是什么意思? Props
相当于Angular
组件的@Input()
成员变量。
因此,这意味着Redux
可以帮助我们应对使用@Input()
将输入传递到组件树上的组件的情况,但这些输入感觉无关紧要,因为此时不是应用程序的一部分。
例如,我们在组件树上传递了5或10个级别。 树的叶子知道如何处理它,但对于中间的所有组件,这个输入是无关紧要的,并使该组件可重用性降低,而且更加依赖于应用程序。 但这只是一个例子。
extraneous props
,还有什么意思?
extraneous props
问题似乎是一个普遍问题。
在某些情况下,组件在组件树中的完全不同的点上彼此依赖,并且将输入10级向上传递到树上,并且回调函数10级向下传递到树上,然后5级到另一个分支,这在复杂性上不可缩放。
发生这种情况时,还有其他情况:
- 在树的深处传递数据,并对组件树上的几个级别的事件做出反应
- 另一个问题是,我们在树中具有相互依赖的兄弟组件,它们代表屏幕上相同数据的不同视图,例如具有未读消息的文件夹列表,以及页眉上的未读消息计数器。
还有更多的例子。 如果我们只有props
或@Input()
作为组件通信机制,我们会很快遇到麻烦。 仅传递组件的输入不会复杂化。
这些场景实际上很常见,所以有我们的答案。
为什么Redux
在React
世界中如此受欢迎?
可能是因为它也解决了extraneous props
问题:这意味着它为更复杂的组件交互场景提供了解决方案。
这是一个基本问题,没有它我们就无法构建更大的应用程序,而Redux
解决了它。
几乎所有非平凡的应用程序都有这些场景,它确实不占用大型应用程序,大多数典型的企业应用程序都会有某种复杂的组件互通场景。
为什么Redux
在这些情况下运行良好?
如果我们尝试使用像AngularJs
的$scope.broadcast()
这样的事件总线解决这些场景,我们将很容易地结束event soup
场景,其中事件以意想不到的方式链接自己,并且很难推断应用程序。
这是因为事件可以很容易地转换为命令,使emitter
知道接收器的内部。 此外,还有可能意外地将事件链接在一起。
Redux
看起来像一个事件总线,但事实并非如此。 实际上,Redux
存储是Command
和Observable
模式的组合。 我们对store
做的是,我们发送一个称为action
的命令对象:
|
|
我们将一个action
发送到store
,store
将对store
内的数据进行操作。 但是action
的emitter
不知道store
会用它做什么。
我们还可以从应用程序的完全不同的部分发送另一个操作:
|
|
store
将处理它并更新消息列表。 然后将消息发送到需要它的应用程序的任何部分。 但接收端不知道是什么触发了新数据的生成:
- 从后端到达的新消息
- 刷新请求
- 消息被标记为已读
那么这与解耦和扩展复杂性有什么关系呢?
store
如何允许分离的组件交互
使用新版本数据的组件(可能是消息列表和计数器)不知道是什么导致数据发生变化,就像我们订阅RxJs Observable
时我们不知道什么触发了值的发出,我们只知道我们有新的值。
消费组件已经订阅了store
,就像他们订阅了RxJs Observable
一样。 这种模式效果很好,因为我们必须尽力将发出的数据转换为命令,而事件总线则非常容易。
服务器推送怎么样?
我们现在说服务器也在不断地推送新数据,新消息。 数据也通过调度操作推送:
|
|
在所有情况下,都会收到一个新的消息列表并将其呈现为消息列表或未读消息的计数器。 渲染的结果将是一致的:我们将不会有一个全部读取的消息列表和一个表示有3条未读消息的计数器。
这种情况是store
闪耀的时候
store
是解决可编辑数据和多个参与者问题的理想解决方案,但我们可以想象数据不会从服务器推送出来。 在这种情况下,我们只有组件交互和协调问题,但我们没有竞争条件的可能性。
在这种情况下,我们试图解决的问题只是在组件树的多个断开连接的位置进行组件交互,对吗?
我们不再需要由多个并发角色编辑相同数据的解决方案。 这导致了Redux
和store
的一个重要特征。
store
是解决多个问题的复合解决方案,而不仅仅是一个问题
我们可以看到这个例子看到store
是一个多责任的解决方案:
- 他们通过
Observable
模式解决了组件交互的问题 - 如果需要,它们提供客户端缓存,以避免重复的
Ajax
请求 - 它们提供了放置临时
UI
状态的位置,因为我们填写大型表单或者想要在路由器视图之间导航时在搜索表单中存储搜索条件 - 它们解决了允许多个参与者修改客户端瞬态数据的问题
store
不仅仅解决其中一个问题,而是解决所有这些问题。
多责任解决方案有什么问题?
一个潜在的问题是那些问题并不总是在一起:你可能想要解决一个而不是另一个。 并非每个应用程序都具有与Facebook
相同的限制:它是全球最大的Web
应用程序,拥有18亿用户。
假设您的应用程序是典型的企业应用程序,用户少于100个:您对客户端缓存的使用有限,并且可能没有服务器推送要求。您可能有服务器推送但数据通常是只读的。
在这种情况下,您可能无法从store
架构中受益(稍后会详细介绍)。
此外,您可能需要涵盖复杂的组件交互方案,而无需将数据存储在内存中。 这里的重要部分是这些问题并不总是在一起:它们聚集在一起,用于非常特殊的应用程序而不是其他应用程序。
Redux
是否避免状态相关的问题?
重要的是要注意它不会,并且一般都没有其他全局存储解决方案,因为使用Redux
我们正在创建一个大的全局应用程序级状态:存储是一个应用程序范围的单例服务。
全局应用程序状态的问题不是它创建的方式,而是它存在的事实。 由于我们忘记清理它,因此很容易产生细微的错误。 它实际上并没有改变我们仅使用纯reducer
函数创建状态,或者全局状态是不可变的这一事实。
所有这些都有所帮助,但我们仍然创建了全局应用程序状态,主要问题仍然存在:它存在并且我们需要在所有正确的位置清理它,并且在复杂性方面不能很好地扩展。
但是如果需要的话,全局状态没有任何问题:到处都需要一些用户数据,为什么不加载一次并将其放入单例服务?
处理全局状态的最佳方式是什么?
避免全局应用程序状态的最佳方法是不创建它,除非它是必要的。 现代应用程序确实需要比以前更多的状态:例如,当我们浏览应用程序时,我们在哪里保留给定搜索表单的最后搜索结果?
每当我们从详细信息返回到主表时,我们都不想重复搜索,即使我们触发了路由器导航。
我们可以使用临时的本地状态吗?
这些情况的理想情况是能够创建仅与该特定掌握细节设置的交互本地化的状态,并使其在使用后自动清理自身。
这就是Angular
允许我们做的事情,我们将在稍后看到。
除了store
之外,Angular
世界还有替代解决方案吗?
在Angular
中,我们有一整套内置的解决方案来处理复杂的组件交互场景。 所有这些解决方案的核心是Angular
依赖注入系统:
- 如果我们愿意,我们可以在组件树中深入注入服务
- 如果我们觉得它们本身紧密耦合,我们甚至可以将组件或服务注入彼此
- 我们可以创建可能存储或不存储数据的共享数据服务
但那只是开始。 让我们回到主要细节场景: 我们可以创建一个非全局服务,并使用分层注入器将其关联到页面的一部分。 这意味着服务及其最终状态将透明地清理自己。
创建自我清理的本地状态
假设我们已经浏览了包含消息列表的应用程序的一部分,并且我们单击列表并转到消息的详细信息。
这是该路由的顶级组件:
|
|
请注意providers
属性中的MessagesService
。 这是什么意思? 这意味着该服务不是应用程序范围的单例。 因此,如果我们想要在打开和关闭多个详细信息时将主服务器的搜索结果保留在内存中,则MessagesService
将是放置它而不是全局存储的理想位置。 为什么?
因为MessagesService
的这个实例是MessagesContainerComponent
及其兄弟的本地实例。 它只能在那里注入,而不是应用程序中的任何其他地方。
您还可以创建一个MessagesTableService
并将其注入表的级别,使用它来加载和分页数据并且并排放置多个表,每个表都有自己的MessagesTableService
实例。
关于这些仅由组件树的子集可见的本地服务的好处在于,当我们离开其路由时,它们与相关组件一起清理自己。
本地有状态服务可以实现为例如observable
数据服务。
Angular
和store
——频繁的选择?
我们可以看到,在Angular
中,我们可以使用许多组件间通信机制,而不仅仅是@Input()
,我们还有一种自动创建和处理本地状态的机制。
在Angular
,我们不一定受益于store
来解决这些问题,还有许多其他内置解决方案。
很多时候,store
被添加到应用程序中以获得类似于可观察的API
以允许某些组件交互:为什么不简单地使用Observable
?
添加存储是对应用程序整体架构的重要约束,它意味着创建大量的全局应用程序状态。 如果内置更好的替代方案并不意味着这一点,为什么不考虑它们呢?
使用store
有成本
该store
确实解决了组件交互的问题,但它也创建了在应用程序中管理状态的需求,而在使用其他解决方案时可能不存在。
这可能意味着在Angular
中,store
解决方案比React
更不常用? 事实上,在最初的一段时间之后,React
也会寻求其他解决方案,我们将会看到。
通常还提到了支持store
解决方案选择的其他参数:性能,可测试性,工具以及保持应用程序可预测且易于推理的能力。 让我们逐一介绍这些,从最后一个开始。
单向数据流
单向数据流是我们在React
和Angular
中都听到的一个重要属性:它被称为在应用程序中查找的属性,它确保它们是可预测且易于推理的。
React
中的单向数据流
在最初的Flux
会话中,单向数据流描述如下:用户触发action
,将其分派到生成新模型的store
并将其发送到视图。
但是视图在呈现时不能自己发送进一步的action
,如果action
的发送已经在进行,也不能发送另一个action
。
避免这种情况看起来像基于原始演示文稿的Flux
的主要目标之一,请看这里。 这里是另一个参考。
另外,请查看最初的Flux
调度程序代码,其中在会谈中提到了检查。
React
和Flux
中的UI
可预测性似乎主要通过在数据层上设置有益约束来实现:防止链式调度。
Redux
和单向数据流
重要的是要注意Redux
不能防止最初的Flux
会谈中提到的链式调度场景。 使用Redux
,我们可以在原始Flux
中触发订阅方法的另一个调度,如果已经调度了一个action
,我们就无法触发另一个。
因此,强制执行单向数据流的需求似乎并不是Redux
如此广泛采用的主要原因之一,因为通过设计并且至少根据原始Flux
会谈中提供的定义,它不会阻止链式调度问题。
也许是因为它太过限制而且在实践中它并没有发生太多?
Angular
中的单向数据流
在Angular
中,我们还看到单向数据流被提及为允许我们以可预测的方式推断应用程序的属性。
但是关注点似乎有点不同:它不是关于对数据层施加约束,数据层可以是任何形式。
Angular
中的单向数据流被描述为确保视图无法自行更新。 那是什么意思?
开发模式下的单向数据流和渲染
渲染开始时,我们在一次扫描中浏览组件树,而在渲染期间组件不能在第二次传递时给出不同的结果或修改父组件。
基本上,评估模板中的表达式或触发某些组件生命周期方法的行为本身不会触发视图中的进一步更改,从而创建类似于AngularJs
的多步藏检测的情况,这有时会导致不可预测的结果。
打破Angular
单向数据流
想象一下,你正在向屏幕上打印一个随机数:如果你试图通过组件getter
方法计算它并将其传递给模板表达式,我们将在开发模式中破坏应用程序,因为你第二次从上到下扫描没有得到相同的结果 :
|
|
试试吧,你应该得到:
|
|
因此,看起来确保UI
中可预测的呈现行为并阻止视图自身更新,我们不一定需要采用类似于store
的体系结构。
让我们回顾一下使用store
的另一个常见原因:提高性能,接下来让我们回顾可测试性和工具。
store
和性能
有时候store
被认为是使应用程序更高效的一种方式,因为我们可以使用ImmutableJs
或Deep Freeze
这样的状态使状态不可变,然后我们可以在任何地方使用OnPush
变更检测。
Angular
变化检测机制开箱即用,并且行为非常直观。 默认情况下,我们仅使用模板中的表达式作为表达式来检测更改,其余部分将被忽略(请查看此文章)。
OnPush
实际上是一种优化,只有少数应用程序可能会从中受益,例如加载大量数据的应用程序(以及我们可以加载多少对用户仍然有用的数据),或者在非常有限的设备中运行的应用程序。
可以肯定地说,大多数应用程序都不属于这些类别(目前的智能手机)。 但是如果我们仍然需要OnPush
,我们可以在没有store
的情况下使用它,特别是如果我们的数据大部分都是只读的话。
如果应用程序是某种类型的实时仪表板,如图表仪表板,则可能更好地限制数据或其他解决方案。 我们甚至可以从更改检测中分离UI
的一个分支并限制其渲染。
这里的要点是添加store
确实意味着我们将使应用程序更高效或更容易优化,因为我们可以以完全独立的方式优化变更检测系统 - 这两个东西可以一起使用但是没有固有的联系。
采用store
架构的另一个共同点是可测试性,让我们看一下,这是进入工具演示之前的最后一点。
store
和可测试性
引入store
经常提出的主要好处之一是它将提高应用程序的可测试性。
确实,reducer
函数很容易测试,但是应用程序本身并没有通过引入store
来测试,因为我们通过依赖注入系统注入依赖项而不是直接在组件内部创建它们。
假设一个应用程序没有很多数据修改或服务器和用户对数据的并发修改:该应用程序可能不需要存储,并且引入它不会使它更易于测试。
但最后也是最重要的是,我们获得了巨大的利益 - 工具。
store
和工具
使用store
的最大原因之一是它提供的工具生态系统。 工具是惊人的,time traveling
调试,能够将store
状态附加到错误报告和热重新加载,这些是巨大的功能。
通过查看这个简短的视频来演示Ngrx DevTools
。 如果你从未见过它,它真的值得。
这些工具是惊人的,但看起来现在Redux
不是必须在新的React
应用程序中使用,那么它在工具方面如何工作?
Redux
的常用替代方案
在最初采用Redux
的一段时间之后,许多React
应用程序正在使用MobX
构建,MobX
是Observable
模式的变体。 我们可以在文档中看到这个描述:
MobX
为现有的数据结构(如对象,数组和类实例)添加了可观察的功能。
这可以通过使用@observable
装饰器(ES.Next
)注释您的类属性来完成。 这是一个小代码示例:
|
|
如果您在NgRx Dev Tools
上看到上面的视频,这看起来很熟悉吗? 看一下,还有一些开发者工具就像Mobx
的redux dev
工具。
实际上,Mobx
开发工具也使用相同的浏览器插件。 基于这个例子,似乎拥有这种高级类型的工具我们不一定需要采用store
架构。 我们需要做的就是使用具有良好或不断发展的工具生态系统的Observable
库编写我们的应用程序。
其他相关生态系统中的工具
另一个与流的概念和可观察模式及其变化相关的生态系统是CycleJs
生态系统。 以下是Flux
,Redux
和CycleJs
生态系统中一些开发人员工具演示的视图。 就在此之前,我们讨论了添加工具而不是使用预先定义的架构(通过@andaltaltz)。
整个演讲很棒,但如果你选择一个适合你的应用程序的架构,那么在13:03的5分钟讨论会产生一些非常有趣的观点。
请注意,许多这种高级工具通常是跨生态系统进行的工作。
这里要记住的最重要的事情是,看起来还有其他方法可以在不采用store
架构的情况下获得出色的工具。
结论
很可能Store
架构最初在React
世界中变得流行,因为它们解决了一些基本问题,React
只是View
没有(按设计)提供开箱即用的解决方案:
- 为解耦组件交互提供类似
Observable
的模式 - 为临时
UI
状态提供客户端容器 - 提供缓存以避免过多的
HTTP
请求 - 为多个参与者提供并发数据修改的解决方案
- 为工具提供一个钩子
然后半年到一年后,生态系统演变为仅在某些应用类型中采用store
而不是其他应用类型。 现在Angular
世界可能会发生同样的事情,结果可能是一样的。 让我们关注工具,RxJs 5
的主要特性之一就是提高可调试性。