导论
什么是 Redux
Redux 是 Flux 的一个变种, 是一种非常流行的单向数据流管理框架. 在 Flux 的基础上, Redux 将数据流进行统一的管理, 让数据流的变化, 变得可预测, 这也是 Redux 名字的由来.
我眼中的 Redux
Flux 早期的官方实现中, 并没有 Reducer 的概念, Store 是一种服务的聚合体, 包括一些数据和操作, 而 Redux 中的 Store 已经是另一种概念了, 如果说 Flux 的早期实现是一个 OOP 的版本, 那 Redux 其实算是 Flux 的 FP 实现, 所以当我们将 OOP 的各种设计工具应用到基于 Redux 构建的应用中去时, 你会发现如此的格格不入, 一切都是离散的, 一个对象被变成了三部分, Action, Reducer, Store 中的一部分, 由于 OOP 在软件开发领域的巨大影响, 为了能够融入 OOP 社区做出了很多努力和尝试, 其中典型的代表是基于 Duck-Module 的流派, 如阿里的 dva 等, 另外一些则属于原生派, 通过各种 Util 来减少 Redux 的样板代码. 不过本文不讨论这些实现, 本文将讨论的是如何在 Redux 上实现这两种风格的前端应用架构
基于 Redux 的 OOP 前端应用架构
OOP 的核心概念是类和对象, 我们开发类来封装数据和操作, 提供对象来输出它们, 通过组合和继承来描述复杂的对象体系. 用各种工具来确保操作的边界, 让对象高内聚, 同时隔离彼此之间的风险.
所以如果没有对象, 一切就都是徒劳的.
而 Redux 是基于 FP 的, 就像两种不同的世界观, 无论怎么拧巴, Reducer 也不可能成为 OOP 里的一员, Action 只是消息的传递者, 在 OOP 里我们通过给对象发送消息来实现方法的调用, 从这个角度看, Bind 了 Dispatch 的 ActionCreator 看起来更像是一个方法, 但是数据又都在 Store 里, 但仔细思考下, 结合以上这几点, 我们大致上应该能理出一条通向 OOP 的道路
- 隐藏 Reducer
- 隐藏 Dispatch, ActionCreator 默认绑定 Dispatch
- 将对象挂载到 Store 上, 隐藏 getState() API
有了思路, 那就开始搞吧
搞吧....其实本文只讨论编程思想, 所以如何实现, 就得另起一篇了:-D
基于 Redux 的 FP 前端应用架构
讨论完了 OOP, 然我们来看看 FP, FP 的核心不用说自然是函数了, 一切皆函数, 但要开发一个应用, 仅仅光靠函数这一个概念, 我们的工作量未免过于巨大, 因此需要引入一些有效的工具帮助我们, 比如 "管道" 和 "高阶函数", 另外为了保持数据不可变, 我们还需要引入不可变的数据类型系统 "Immutable", 有了这些我们可以尝试开始着手去搭建一个前端应用架构了.
如果说 OOP 是用一堆对象描述世界, 对象彼此通过消息来传递数据, 那在 FP 的角度来看, 世界就像一条条管道, 数据在管道中流动, 管道和管道之间可以并行, 也可以串联, 甚至可以彼此包含. 在对象的世界里, 对象负责存储和处理数据, 数据是动态的, 可变的, 而在管道的世界里, 数据是不可变的, 因为管道不负责存储数据, 如果数据没有任何消费方, 那数据本身就没有意义了. 所以存储数据是不必要的. 因为数据处理本身并不绑定数据, 这也使得数据处理的过程变得非常灵活. 总结下, FP 的前端应用架构基于以下四点
- 用函数定义操作, 并解决数据并行的问题
- 用高阶函数来组合函数, 构建复杂的操作
- 用管道解决数据的串行操作
- 引入"Immutable" 保证数据不可变
基于这四点, 我们可以尝试对前端应用架构使用 FP 的方式来进行抽象和描述, 比如对于一个包含界面的 Web应用, 从用户输入数据到传递给服务端的过程, 可以想象成一条管道
用户输入 -> UI响应并处理 -> 数据层响应并处理 -> 发送数据的管道响应并处理 -> 数据源响应并处理 -> 持久化
//反过来
用户请求 -> UI响应并处理 -> 数据层响应并处理 -> 发送数据的管道请求并处理 -> 数据源响应并发送
在过去要保证这样的串行操作, 可能存在诸多问题, 比如异步的处理, 所幸 ES7的 async/await 给我们带来了便利, 异步和同步编程模型的统一使得降低了前端架构的复杂性. 整个前端应用架构看上去就像两条流水线, 组合在一起就是一个环, 我叫它"环形架构"或者"流水线架构"
以其中的数据管道为例, 我们可以想象请求数据的引擎就是一个并行节点, 因为对不同的数据源, 请求引擎可能不同, 但是我们会统一成一个概念, 比如 fetch, 在 node 和微信小程序下就是完全不同的, 但是通过概念统一, 我们可以利用高阶函数组合出一个能够并行处理多种数据源的引擎, 利用管道对传递的参数和数据源响应的数据分别处理.
事实上, 前端架构中的很多部分都无法用对象描述, 包括层次, 垂直的和纵向的, 流程图, 状态图, 时序图等等. 但这些东西却可以用数据管道来统一描述, 因为前端架构图都是平面图, 当我们描述前端架构的时候, 都是在同一平面上描述数据是如何流动和处理的, 无论我们如何划分, 数据在某一平面上一定是某一种串行或者并行操作.
当你习惯这种思维模式, 我想你会上瘾, 因为一切都是一个环上的某个操作, 无论多复杂的系统, 都可以被分解成一个简单的操作, 粒度的控制完全取决于你.
然后我们把 Redux 放到这个应用的环中, 你会发现, Redux 代表的并不是整个环, 而是其中的一部分, 更像是一个DB, Store 提供 Dispatch 和 来触发数据的写入, Action.Type 如果用来表示数据类型显然比用来描述操作更合理, 这样 Action 本身就是可枚举的了, 相对应的 Reducer 也变得可枚举了, 然后整个前端应用的架构应该是
UI 数据管道 -> Redux 管道 -> 数据源处理管道 -> 数据源
我们将 UI 上的 local 数据放在 UI 数据管道处理, 同时 Redux 管道负责接管外部数据源的读写, 而数据源处理管道则负责通信中前后的数据处理, 如果应用很庞大, 你可以加入更多的平面, 去切分整个系统, 例如加入数据模型的重组, 比如使用 Reselect 库重组数据模型
UI 数据管道 -> 数据模型重组管道 -> Redux 管道 -> 数据源处理管道 -> 数据源
只要你愿意, 你可以合理的切分更多的平面出来, 当然过多的管道会带来通信和数据校验的成本, 这些都是需要在架构设计中被考虑到的部分.
但当你在你的应用中使用这种架构基于 Redux 去开发, 你的应用应该会非常容易测试和维护, 但是在开发模式上, 采用 TDD 应该会更合适, 因为确保每个管道的可用性, 是将他们拼起来的唯一前提.
某野生前端架构师
写于2017年夏末