Fish Redux 设计原则
status
category
date
summary
slug
icon
tags
password
fish-redux 由阿里巴巴闲鱼团队开源,是基于 Redux 数据管理的 Flutter 应用框架,特点是配置式组装,将页面(Page)和组件(Component)进行拆分,上层负责组装,下层负责实现,方便维护,且上下文无关。自己的 Flutter 项目也使用了 Fish Redux,从典型的 MVC 模式转换过来需要在思考模式上有所转变,为了更好地使用框架,应该先理解框架的设计理念和原则,而 Fish Redux 正是借鉴了 Redux 的设计灵感,因此可以从 Redux 中文文档 入手学习。
动机
虽然 Redux 的设计初衷是为了解决 JavaScript 开发单页应用中的问题,但包括 Flutter 在内的移动应用开发同样也面临着这些问题。随着应用越来越复杂,应用需要管理越来越多的状态(state),它们可能包括服务器响应、缓存数据、UI 状态、数组中被选中的数据等等。
管理不断变化的 state 非常困难,例如这个 state 表示某个 model 中的数据,model 的变化会引起另一个 model 的变化,继而影响了 view 应该显示的状态,在复杂的应用场景中,这个影响链可以沿着 model 和 view 一直传下去。因此 state 的变化可能导致我们意想不到的结果,对于开发者来说,根据 state 的变化编写上下文相关的函数也非常痛苦,添加新功能、测试和调试重现都比较麻烦。
理想的情况应该是 state 的变化完全可以预测,并且 state 变化时,model 和 view 做出的反应也应该是除 state 外上下文无关的,即只关心变化前后的 state。
三大原则
Redux 就是为了解决上述麻烦而设计的,它有三大原则。
单一数据源
整个应用的 state,会储存在一棵对象树中,对象树存在于唯一一个 store 中。
Redux 使用唯一的对象树来保存整个应用需要的数据,也就是 state。因为只有一个 store,当需要储存不同的 state,自然形成了树状结构。因此 store 可以被理解为是“应用的状态”,类似于 MVC 模式中 model 的概念,这种设计来自于 Flux,原本的 Flux 支持多个 store,但 Redux 简化为只有一个。
Redux 的单一数据源设计对开发者来说比较友好,因为可以方便地调试和监控状态的变化,状态储存在树状结构中,也很容易实现 Redo/Undo 操作。
State 是只读的
唯一能改变 state 的方法是触发 action,action 是一个描述发生事件的对象。
这项原则规定不能直接更改 state 的值,而应该间接地通过 action 来通知 state 更新。这个设计非常关键,它来自于 Flux 中的单项数据流,对于每一个动作的发生,最终的影响都是可预测的,并且按照一个接一个的顺序来影响 state,进而进行视图或数据上的更新。
“只读”的意思是不能更改,换句话说就是没有 setter 方法,所以状态对象的更改并不是在原有对象上更改,而是重新创建了一个全新的状态对象,并代替了原有对象。
使用纯函数进行修改
为了描述 action 如何改变 state,应该编写 reducer 函数。
Reducer 只是纯函数,它接受 action 和之前的 state,并返回新的 state。纯函数指的是它除了产生 state 之外,不应该有其他的副作用。纯函数的特性使得我们可以方便地控制它们的调用顺序,或者进行 reducer 的拆分,甚至编写可复用的 reducer。
核心概念
Fish Redux 的核心概念在 Redux 的基础上略有更改,不过大体相似。
Page
page 代表一个页面,继承自 Component,包含并整合了 state、view、reducer、effect 等等。
State
state 用来储存 page/component 的状态,即保存数据。
Action
Action 通过枚举定义一种行为,可以携带信息参数,并发往 store,Action 本身并不包括对 state 的修改。Action 包含两个字段 type 和 payload;推荐写法是在 action.dart 里定义一个 type 的字符串枚举类和一个 ActionCreator 类,这样有利于约束 payload 的类型。
Reducer/Effect
两者都是用来处理数据或行为的,但略有区别:
- reducer 作为纯函数,仅仅响应 action 对 store 的改变
- effect 处理 state 改变的副作用,例如点击事件、异步请求等等
有关全局 Store
单一的状态树有利于数据的统一管理,并且生命周期和应用一致,也有利于组件之间进行数据共享。个人认为原本 Redux 的这种设计思想,是建立在网页本身只有一页的特性之上的,并不完全适用于多个页面的移动应用。在 Fish Redux 中也有这个思想,例如同一个 page 下的 component 共享同一个 store。然而 component 之于 page,并不完全类似 page 之于 App。构建一个 page 时,会构建它包含的所有 component,但一个 App 启动并不代表它会打开所有的 page。因此所有 component 共享 page store,比所有 page 共享 App store 更加合理。因此 store 的粒度,体现了原有项目逐渐庞大的过程中,集中和分治的思想斗争。
于是在最初的 Fish Redux 中,store 是以 page 为粒度划分的,多个 page 则存在多个 store,在页面之间跳转时则通过 route 传递参数。这样的设计模式更像 Flux,而没有完全照搬 Redux 的唯一 store,但按照我的个人理解,这仍然没有打破单一数据源的原则。虽然有多个 store,但每个 page 仍然对应着唯一的 store,并不存在同一个 page 从多个 store 获取数据的情况,Fish Redux 只是把原本聚合在一起的唯一 store,按照 page 为粒度切割开来,它们之间依然是不互通的。
但是作为一个完整的大型移动应用来说,有些场景是需要多个 page 共享数据源的,并且确实存在某些数据会影响多个 page 的情况,如果仍然按照界面传参的方式进行沟通,会十分繁琐且效率低下。例如在 这个 issue 中提到的,开发者希望有 App 级别的 store 支持某些界面共享某一数据,而其他界面仍然拥有独立的 store,讨论中有人提出需要在构建界面时的 buildPage 方法中传入一个 store 参数,以指明 page 的数据来源。
支持 App Store 之后,虽然看似存在多个数据源,但只要坚持单向数据流的原则,仍然可以保证数据驱动和结构清晰。例如在 这个 issue 中提到的,解决方案的选择往往是结构清晰和使用灵活之间的博弈:
- 通过 page.connectExtraStore api,建立 App Store 和 Page Store 之间的单向驱动,层层传递
好处是数据驱动结构清晰、UI刷新精准控制。
- 通过 context. addObservable 可以给组件增加额外的外部驱动源
好处是使用灵活,但是可能会产生额外无效的组件刷新。比如组件只是关心了 AppState.a, 当 AppState.b 发生变化,组件同样会被刷新,浪费了很多资源。另外,滥用这种方法也会大幅增加维护成本。
以上是自己关于单一数据源和全局 store 的思考,框架的发展进程也提示我们,优秀的设计原则应该兼顾项目的愈发庞大和独特的应用场景,适当的时候做一些变通。
Loading...