
主要总结几个话题:
- 现在 APP 的结构是怎样的
- 每个部分都承担什么作用
- 不足的地方
- 18 年旧文 《Redux 推行讲稿: React 做什么,Redux 做什么》
东西是 18 年项目开始时定的, 初始目标是 React + Redux,一个管内部关系,一个管外部关系。 随着做的过程中逐渐完善,抽象出脱离页面的业务封装,发展成 Services
这里只提到整体结构部分,模块内部如何分层,在这篇文章中模块设计通用范式:基础设施 -> 通用功能 -> 业务封装 三层结构
我早期有一篇讨论程序有什么“元素”的文章 如何描述一个程序 — 程序有什么“元素”,元素之间的关系,如何描述
在接触到 ”响应式编程“ ”React“ 后解开了我对 MVC 中状态和界面关系的别扭之处, “界面描述就应该是某个状态下,长什么样” 而以前是 “执行什么行为,改成什么样”(执行行为的本质,其实是在改状态)
APP 的结构

整体数据流如上图,看着一堆线条,实则找到任意一个节点,就会发现: 数据修改都 **只会指向 store**,**不会发生任何节点与节点之间的状态操作**(除了 Store)。 监听者监听 store,在状态变更时改变界面或执行一些行为
-
React Component: 负责 视图内部状态与界面绑定 -
Container+Action+Reducer负责 跨组件数据流向控制, -
Services是脱离页面的独立业务模块(内部也是通过 redux 绑定和修改全局状态)
ps.Container 是 react-redux 提供的一个概念,被用于 “绑定了当前业务状态,行为修改 store 的 Component”
层级关系上:
- 用户直接操作,也是直接对外的业务层是 Page(Container), Page 可能囊括很多的 Component, Container 而这些 Component\Container 内部也可能还有 Sub Component\Container (注意:一个没绑定业务的 Component 不会拥有一个绑定了业务的 Container,但反过来是成立了。 Component 与 Container 没有层次区分,只在于是不是专用于当前环境)。
- 另外 Services 部分依托于 Container,由 Container 触发执行某些行为(内部会自己封装了状态判断);部分在某个时机启动后,就一直在后方监听外部数据,修改内部状态。
-
整个结构最底层还有 基础功能支撑,这篇不详述。
- 上图 Test 是执行一年后失败的单元测试 ,我放在这文里单元测试
Type和Model的区别在于,一个是 ts 的 interface,只声明类型有什么字段(类比struct)。Model 是类,有函数有属性 简单看一下每个部分的角色, 然后下一节着重React,Redux,Services分别承担什么
业务模块
这部分与数据\层次无关,是业务上的归类(比如: “设置页面模块”, “登录注册模块”, “相册模块”)
目的在于把同属于一块业务的内容,归纳在一起, 当做 “一个小的 APP”
所以麻雀虽小五脏俱全,APP 有的他都有,一个业务模块内部有自己的 Type ,自己的 Utils ,自己的 State ,自己的 Action 等等。
当做一个 “子 APP ” 看待即可
React
我们抛开 HTML,Flex 那些便利, 单看数据与视图的关系 React 所做的是, 为 “界面“ 这个静态概念, 增加了 ”状态“ 这个动态描述 白话描述既 ”写界面时, 只需要描述每个状态下的界面应该渲染层什么样,不需要手动修改界面“
例: 未登录时,页面要显示什么内容。 登录后页面要显示什么内容
React 描述了 界面元素内部, 界面在不同状态下的不同渲染 这部分会在下一节详细说明
Redux
Redux 提供了一套 action + reducer + store 的数据流向管理机制
- Store: 存放了我们 APP 大部分状态 (部分只属于某个元素的内部状态没管)
- Container: 是
props绑定了全局的状态 (mapStateToProps),props回调行为绑定了 Dispatch(action) 的Component(绑定了当前业务状态和业务逻辑,只适用于现在业务场景的Component) - Action: 被抽象出来的事件
- Reducer: 是将事件转为状态变更的纯函数
这整一套机制描述了 元素与元素之间, 数据流向的约束 , 他强制约束了数据修改仅能通过 action -> reducer 修改 Store。 Reducer 在我们 APP 中每个业务模块都有一到多个(state+reducer+action),视具体业务划分而定
例: 设置页可以修改一些全局选项(是否加水印), 这些选项会通过 Aciton 到 reducer 然后进到 Store 变成新的状态 比如飞机会实时更新自己的状态, APP 在收到飞机状态更新后,dispatch(action),最终进到全局状态供其他地方使用
ps.redux 在执行彻底的情况下,应该是所有状态, 执行函数都被这套机制约束起来。 但在实际实现中,这对开发很不友好,所以我们允许只属于元素内部的状态,内部管理。 允许跨元素的控制函数(但一般不允许一个元素直接改另一个元素状态)
这部分会在下一节详细说明
Services
上面提到,React 主要描述的 ”界面“, 一个程序除了 ”界面“外,还有很多别的不以页面为基础在实现业务逻辑的业务封装 这些业务封装也有自己的状态, 逻辑, 业务。 但是又不是界面, 就被我们归到了 Services 一层。
内部可能监听着外部环境变化(比如 GPS,比如飞机实时状态变更),会在外部发生变化时同步修改内部状态。 并且封装了一些业务功能,用简单易用的函数接口暴露出去, 内部会自行根据当前的状态,做对应的行为。
我们 APP 中有 35 个 Services
例:
- GPS 服务: 监听 GPS,实时的刷新 APP 内部状态
- Ability 服务: 监听飞机的能力, 异常, 对飞机能否执行某些行为作出警示
- Camera 服务: 根据当前状态,处理拍照(取当前的摄像参数), 判断模式处理模式切换,监听飞机的 Camera 状态
Type: 全局/当前模块的类型定义
使用中常会遇到,一个类型在很多地方都用到,为了明确层级关系,规避关系混乱的 import,就会将类型往上提一层。 需要注意的点就是, 类型定义位置不恰当,容易引起引用关系混乱 其他就没啥了
比如,定义一个常用的 Key-Value 结构
export type ZMap<K extends string | number, V> = {[key in K]: V}
比如,定义一个数学概念
export interface Range {min: number, max: number}
比如,定义常用的 UI 概念
export interface Area {x0: number, y0: number,x1: number,y1: number}
export interface Size {width: number, height: number}
Utils:纯函数,封装一些便利操作
目的是封装一些常用操作,与当前环境,状态全部无关,单纯的函数式操作 需要特别注意的是, Utils 是纯函数(意味根据入参,通过逻辑计算返回值,内部不会有任何别的引用,入参确定返回值就确定) 常见的:数学计算(几何计算,进制转换,时间转换)
比如,数组工具: 防止取数据越界,查找元素序号等等
export function getValidValueWithIndex<T>(index: number, array: T[]): T | undefined{
if(index < 0 || index >= array.length) return
return array[index]
}
export function getValueIndexFromArray<T>(value: T, array:T[], defaultIndex: number = -1) {
let index = array.findIndex((item) => item === value)
return index == -1 ? defaultIndex : index
}
比如,GPS 工具: 用于计算两个 gps 的距离(代码太长) 比如,数学工具: 勾股计算两点距离, 计算一个值的百分比
export function getDistanceBetween2Points(x1: number, y1: number, x2: number, y2: number) {
return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2))
}
export function getPercent(value: number, minValue: number, maxValue: number) {
let range = Math.max(maxValue - minValue, 0)
return (value - minValue) / range
}
Config/Constant:配置项,常量
APP 的默认配置项 和 定义好的一些常量 通常我们要求所有代码里写的常量,都要提取出来,做成可配置项。 业务强相关的放在独立文件中,方便需要的人做配置。不需要维护的,放在文件中
也是能说的不多,直接放代码 比如,视频分辨率具体值
export const HoverVideoResolution = {
resolution4K: {width: 3840, height: 2160},
resolution2_7k: {width: 2720, height: 1530},
resolution1080P: {width: 1920, height: 1080},
resolution720P: {width: 1280, height: 720}
}
比如,业务控制,提示显示多久, 倒计时结束后连拍多少张照,飞机最大速度控制
// 提示显示的时长
const tipsDuration = 5
// 倒计时结束拍多少张
const CountDownShootPicsCount = {
[CountDownOptions.Disabled]: 1,
[CountDownOptions.ThreeSeconds]: 3,
[CountDownOptions.TenSeconds]: 3,
}
// 飞控控制
export namespace FlightControlConst {
/** 飞机最大移动速度(即 x、y、z三个方向) */
export const MaxSpeed = 2
/** 飞机最大旋转速度(即yaw) */
export const MaxYaw = 2
/** Gimbal 速度控制的最大取值范围 单位: 弧度/秒
export const GimbalMaxSpeed = 1 / 15 * Math.PI
}
规定一个流程的步骤顺序
const WiFiDownloadAndWritePhotoSteps = [
MediaPipelineStepType.FetchMediaInfo,
MediaPipelineStepType.DownloadMediaThumbnailByHttp,
MediaPipelineStepType.SaveMediaThumbnail,
MediaPipelineStepType.InsertMediaToDataBase,
MediaPipelineStepType.DownloadMediaOriginByHttp,
MediaPipelineStepType.AddWaterMark,
MediaPipelineStepType.SaveMediaToAlbum,
MediaPipelineStepType.UpdateDataBaseMedia,
]
程序中每个部分都承担什么职能
React
我在初写代码不久时曾经想过一个问题,一个程序里都有什么东西, 这些东西都是什么关系 如何描述一个程序 — 程序有什么“元素”,元素之间的关系,如何描述
我当时想明白的一个问题是,程序的逻辑有三个东西:“状态” “执行行为” “当前程序表现”(不止 View),描述程序流其实是在描述这三个事物之间的关系
而受 MVC 影响, 我即使意思到 APP 界面,在不同状态下,会呈现不同的形式,却一直采用的是某个因素,触发执行某个函数,
然后在函数中修改程序变量, 修改当前界面的方式 (下图上部分)
没“悟”出来的那部分,在看到 “响应式编程” 后才反应过来。“对,就应该是这样的”:
- 一个程序的表现是静态的,不同状态下会有不同表现
- 所有状态下的表现形式形成一个集合,这个集合就是所有的可能, 是有限的,可罗列的全部描述出来的
- 程序的执行过程, 是一个动态切换“状态”的过程。 而每个状态下的程序表现,已经在一开约定好了
它与前者的不同在于, 前者的界面样式是执行时决定,由函数动态修改出来。 而后者是一开始就在那,事件只是引起的状态切换。 前者意味着, 一个 View 不同的表现形式散落在各个函数中,而各个函数本身没有关系。所以很难清晰的复原出一个 view 的可能情况。由各个不同函数处理的同一个 View,就像被多个线程同时访问的数组一样。 很容易出现修改不再意料之中情况。
后者, 一个界面长什么样,天然已经描述完了。 逻辑执行过程中,完全不关心视图修改,只专注于状态 ps.要注意的是,这是逻辑上的描述。 实际在实现是,哈哈哈很遗憾, React 内部还是要执行函数改的
Redux
想象一个没有约束的状态修改和函数调用, 元素与元素之间,通过回调,通过直接修改,通过间接修改。回调函数层层传递,并且没有明确限制的场景。
每个 “元素” 都有很多状态,这个状态可以被内,外修改。 最后整块逻辑就像揉在一起的线团
(当然,程序员都很聪明, 会用各种方式规避类似的情况。而有趣的是,优化方式 ”不统一“(个人的,团体的)本身也是一种乱)
(下图表示一个 “元素” 可能让另一个 “元素”,发生修改的行为。 比如回调,比如直接改属性)

redux 是一种写法极其繁琐,可能导致直接的函数修改,变成 3 倍代码量的实现方式(定义 Action,定义 ActionCreator,定义 Reducer)。 而其优势也在于建立的这个约束。
官方虽然没有明说,但其设计理念的理想情况, 会变成程序中所有状态都在一个全局的 Store 中描述; 程序的所有行为都要定义对应的 Action, 程序的所有状态修改行为执行只能通过dispatch(action) 实现;
最终得到的结果是,”元素“ 与 ”元素“ 之间完全独立(理想情况), 数据的改变只会由某个“元素”发起, 然后修改 Store。 没有其他可能。对每个 “元素” 而言都没有依赖关系。
代码实现:
State: 是以树结构存储的状态,一个纯对象,按不同的业务模块分了不同的子字段
Contianer: 前面提过,是绑定了状态的 Component, 实现上就是调用react-redux 提供的 connect函数,映射上全局状态,将所有操作改为 dispatch(action)
class CameraPage extends Component<Props, States> { render(){...}}
const mapStateToProps = (state: AppState) => ({
connectMode: state.camera.pageState.connectMode,
} as StoreProps)
const mapDispatchToProps = (dispatch: Dispatch) => ({
setupConnectMode: (connectMode: ConnectMode) => {
dispatch(CameraPageActionCreator.setupConnectMode(connectMode))
},
} as DispatchProps)
export default connect(mapStateToProps, mapDispatchToProps)(CameraPage)
reducer: rootReducer 本质其实是个巨大的 switch-case, 实现时会按模块放在不同文件中。为了书写形象,我又封装了一个函数,将 switch-case,转为 Key-value 形式. 需要注意的是 reducer 内部必须为纯函数
✏️[file:9F6F1AC0-F397-4893-9E02-5A45086398D3-75609-00020CE3EFE3E34C/reducer.ts]
export default function createReducer<S>(initState: S, handlers: {[key: string]: (state: S, action: any)=>S}): (state: S, action: any) => S {
return (state = initState, action: any) => {
const handler = handlers[action.type]
return handler ? handler(state, action) : state
}
}
action: 一个约定带 type 字段的数据结构,可能会附带一些信息
Services
从 APP 用户的角度看而言, 所有的功能都是 依附 在页面上的, 习惯了 MVC 的情况下,很容易把页面当 C 用。
初期开发时, 我们的 Page,Container 层绑定了很多的 dispatch 事件,由页面承接了逻辑实现的主体。
而随着开发的逐渐进行,渐渐发现(其实原先也知道,但没有这么做的动力): 有很多逻辑行为可能重复出现在几个页面中,究其原因,这些行为只是在之前由某个页面执行,但本身并不隶属于这个页面。
比方说, 我们 APP 中有一个实时显示飞机图传的拍摄界面(就像手机的相机)。 里面有停止录像行为,切换模式行为,拍照行为 早期的实现中,这些行为全部都绑定在页面的 停止按钮, 切换模式按钮,拍照按钮上 而不久后,出现一个需求飞机与 APP 断线后要根据情况 停止录像, 切换模式。 这时就体会到,一个不隶属于页面的行为,缺绑定在页面,但外部条件不再依赖页面时,就面临的修改困难
所以之后,所有发生在某个页面的事情,我们都会找到其实际的主体。脱离页面归纳成 services 单例 比如: 控制相机模块的 CameraServices, 控制智能模式的 CaptainServices, 处理遥控器的 RemoteControlServices
代码实现
Services 一般分成两个部分, 一个是对全局状态的监听, 另一个就是使用全局状态封装出来的业务函数
export default class UserService {
private handleStateChange = () => {
store.subscribe(()=>{ ... })
}
private updateLimitedInfo = () =>{
let {sn, version} = store.getState().drone
let {id, name, token} = store.getState().user
if(!sn || !token) return
requestDroneLimitedInfo(token, sn).then(info=>{}
...
}
不足的地方
- 现在的 Service 包含了很多的建立在基础库之上,与当前 APP 业务上强绑定,专用于当前 APP,但是又不包含状态的类 比如文件管理系统,里面配置了这个 APP 特有的文件类型,规定了这个 APP 的文件夹路径。提供与 APP 业务相关的函数。 但是不操作任何状态 这部分不确定放哪
- Redux 的使用十分繁琐
- Redux 的维护从某种角度说很容易, 比如逻辑走向极其清晰,状态新增,行为新增都容易。 另一方面,Redux 的维护又很难, 一个使用很广的状态, 很多地方都触发修改。 排查时就不想以前,直接定位到页面,定位到函数即可, 而是需要在 store 的角度,排查很多动了状态的地方。
附录:18 年举例描述 React 做什么, Redux 做什么
一个有多个状态影响界面的例子
界面内容:
- 外部大容器包含 顶部导航栏, 底部工具栏, 中间列表
- 中间横向图片, 视频列表,列表中
- 图片视频不同的编辑内容
- 图片视频的控制容器,
界面的不同状态
当刚进入页面没有开始编辑时, 编辑内容为滤镜时:

-
显示拍摄时间标题
- 页面中显示滤镜编辑 (在滤镜和裁剪模式下各不相同)
- 底部显示编辑切换选项,并且选中滤镜 交互上:
- 可以左右滑动,可以放大缩小
当页面开始编辑时:

- 顶部显示编辑
- 中间显示编辑后的图片(在滤镜和裁剪模式下各不相同)
- 底部显示确认与取消
交互上:
- 无法左右滑动,无法放大缩小
当没有编辑时页面被放大或者点击:

- 隐藏顶部
- 隐藏编辑
- 隐藏底部
交互上:
- 可以左右滑动,可以放大缩小视频(需要 redux 的地方)
React,Redux 所处理的业务逻辑分析
- 没有 React 和没有绑定状态的情况下
- 点击了某一个按钮
- onPress 中判断当前的页面状态,
- 根据当前的状态做 switch
- 根据不同的情况做不同的页面变更
- 点击了某一个按钮
- 没有 React 但是实现状态管理的情况下
- 不同的 ui 归纳为几个状态
- 实现每个不同的状态下不同的 ui (未编辑时底部什么样, 编辑时底部什么样, 预览时底部什么样)
- 实现状态变更监听
- 此时每个页面的状态还是相互之间独立的
- 类内部需要专门的状态管理用于识别不同情况下当前的状态
- 有 React 的情况下
- React 框架默认以状态划分页面, 内部已经实现了 render 监听状态变更
-
state => react => component
- 每个模块之间的状态还是相互独立的,依靠外部传值实现
- 每个模块都可能各自触发引起其他模块状态改变的行为
- 有 Redux 的情况下
- 不同模块对同一个状态的改变全部被归纳为同一个
-
不同模块的状态改变全部由 store 控制
- 很难做异步处理
- thunk 和 Promise 的做法是改造 action, 使其可以接受 function promise
- 会导致 action 开始承担业务逻辑
- 有 rxjs 的情况下
- combine
- 若没有 React , 会承担 状态改变 -> render 的职责, 所以在有 react 的情况下这个可以不考虑
- 异步处理能力强大
- 可以将状态的变化归纳为数据流
-
可以很方便的处理状态之间的关系(可以把所有事件封装一个个 Observable)
- mediaPipeline
参考: 对 React 状态管理的理解及方案对比 · Issue #36 · sunyongjian/blog · GitHub