React & Redux应用架构
mrdulin opened this issue · comments
架构基于React Hooks和React FC设计:
View层
React functional component构建视图,包含:
- ReactElement,JSX视图元素
- 视图的事件处理函数,例如onClick等
- 使用controller层提供的hooks,获取View Model
使用组件内部state的视图逻辑通过custom hook封装,导出state和操作该state的函数,事件处理函数直接去调用custom hook导出的函数来变更视图state。
Controller层
主要使用React hooks来实现,包含
- 业务custom hooks
- UI custom hooks
UI custom hooks封装组件内部状态(通过useState
定义)及其变更操作,组件内部状态可能依赖组件的props经过逻辑计算得出,都封装在hook里,这块代码逻辑不要放在组件里。一个好的实践是每一种类型的state及其相关操作都封装在一个hook里,单一职责,比如useSearchCondition()
hook用来封装搜索查询条件:
export function useSearchCondition() {
const dispatch = useDispatch();
const { pagination, ...searchCondition } = useSelector(searchConditionSelector);
const setPagiantion = (p: PaginationConfig) => {
if (p.current && p.pageSize) {
const payload: Pagination = {
currentPage: p.current,
pageSize: p.pageSize,
};
dispatch(actionCreators.activity.management.updatePagination(payload));
}
};
const setSearchCondition = (searchCondition: Omit<SearchConditionState, 'pagination'>) => {
dispatch(actionCreators.activity.management.updateSearchCondition(searchCondition));
};
const clearSearchCondition = () => dispatch(actionCreators.activity.management.clearSearchCondition());
return {
pagination: {
current: pagination.currentPage,
pageSize: pagination.pageSize,
} as PaginationConfig,
setPagiantion,
setSearchCondition,
clearSearchCondition,
...searchCondition,
} as const;
}
如果是class-based component,组件只能有一个state,如果需要划分子state用来保存不同的业务数据和UI交互的数据,非扁平化的方式是使用命名空间对象,但是后续更新state比扁平化方式稍微麻烦一点,多一层浅拷贝:
this.state = {
ui: {
showModal: false,
// 表单临时数据等等
// form: {}
},
business: {
DTOFromAPIX: {},
DTOFromAPIY: {},
DTOFromAPIZ: {},
}
}
扁平化方式是通过注释区分:
this.state = {
// UI state
showModal: false,
// Business state
DTOFromAPIX: {},
DTOFromAPIY: {},
DTOFromAPIZ: {}
}
react hooks提供了更好的state组织方式,每一个custom hook操作一个state slice,参考Call useSelector Multiple Times in Function Components,遵循单一职责原则。上述例子可以拆分出4个custom hooks: useShowModal()
, useDTOFromAPIX()
, useDTOFromAPIY()
, useDTOFromAPIZ()
,这点官方文档已经说明Tip: Using Multiple State Variables。此外,每个hook也可以使用组件的生命周期hook,依赖生命周期的逻辑彻底内聚在自己的hook里,而不是像class component中多个不相关的逻辑都在一个生命周期方法里处理,比如在componentDidUpdate()
既要处理showModal
state,也要处理DTOFromAPIX
state, DTOFromAPIY
state, DTOFromAPIZ
state。
业务custom hooks封装与业务逻辑相关的数据及其操作,数据源包含backend service API调用返回,web storage, cookie, constants, URL query parameter等。需要将数据持久化到redux store的数据获取方式使用dispatch+redux-thunk创建的异步action creator(redux-saga等),考虑到部分视图很独立,不需要持久化API数据到redux store,可以省略dispatch+async action creator,直接调用前端fetch封装的API service直接去调用backend service API。
用户与视图交互产生的数据可能会持久化在Redux Store里,典型的数据比如过滤条件,通过useSelector
+selector获取数据,与这个redux state对应redux action操作也封装在hook里,通过useDispatch
+action creator进行操作。
Data Access层
包含:
- Reselect库创建的Selector,用于从redux store中读数据和计算衍生数据
- Redux thunk(redux-saga)等中间件创建的thunk或saga,用于异步流程控制,action meta data处理,调用前端API service,入参校验与处理,保证传递给API service方法的参数是正确的。
使用reselect库提供的createSelector方法创建selector作为访问redux store的方法。selector既可以被useSelector
使用,也可以在redux-thunk里通过xxxSelector(getState())
这样的形式使用,用来获取redux store上的某一个state slice,复用state slice访问逻辑。此外,selector还可以为数据访问创建一个接口,不管reducer和selector中的逻辑怎么变化,只要selector返回的数据接口满足组件即可,我们不用去修改组件,做到redux state和组件视图数据隔离。
selector的另一个目的是为衍生数据的计算提供了优化,selector可以基于组件的props和state进行计算衍生数据,Accessing React Props in Selectors,可以基于动态或非动态参数进行衍生数据计算How do I create a selector that takes an argument? ,selector提供的memozie功能可以使在输入不变的情况下,返回上一次计算结果(引用相等,值相等),配合React.memo
, useEffect
的dependency list跳过effect,使用useMemo
,如果dependency list中使用了selector返回的衍生数据,在返回结果引用和值不变的情况下,可以创建memorized结果,避免组件每次render重新执行昂贵的逻辑,完成对组件的渲染优化,减少不必要的re-render。
Service层
比较宽泛的一个类别,包含了helper, utils, 第三方库,通用的custom hooks,第三方hooks等,致力于完成某一个特定的任务。
通过使用fetch,axios, socket.io等库完成对API service的封装,主要功能是对接应用外部数据源,backend API service,第三方API,websocket等,通信协议主要是HTTP protocal。通过拦截器,中间件等AOP编程方式,或者收敛到一个函数,完成对请求的预处理,响应的预处理及网络错误,通用业务异常等错误处理。API service的每个方法获取到的是具体每个接口返回结果和业务异常。
不管调用什么外部数据源的接口,前端API service输出的数据结构应该是统一标准固定的(预先定义好接口),比如输出的对象包含三个字段: {error: null, result: null, message: null}
, error
表示业务异常code,result
表示业务正确处理的响应,一些DTO对象都会在result里,message
表示业务异常时的错误消息。
helper, utils存放通用方法,不关心也不应该包含业务逻辑,不再赘述。
API service的方法可以在controller层的hooks中被调用,也可以在redux thunk创建的async action creator中调用,不要在组件视图层中直接调用。
Data Persistence层
Redux store存储的数据不算严格意义上的持久化,由于是存储在应用程序内存中,属于Memory DB,生命周期为应用的生命周期,应用初始化(刷新浏览器,启动,重启服务),则之前存储的数据丢失。根据需求决定是否使用redux-presist等库将Redux store中的数据持久化到Web Storage中。
存储数据主要有以下几类:
- 外部数据源的业务数据,可以进行范式化,即Normalizing State Shape
,使用Redux-ORM或者RTK提供的createEntityAdapter 来管理范式化state - 用户与View层交互产生的数据,比如表单,过滤条件等
- 根据需求是否需要使用Web Storage和cookie里的数据来初始化redux store, 可使用redux-persist库对redux store进行持久化和水合。
应用程序依赖的其他数据源:浏览器环境主要有Web Storage, cookie, URL query parameter,应用程序定义的常量等。
具体架构根据需求做调整,通过分层,分治等实现关注点分离。结合组件化,模块化,高内聚,低耦合,TDD提升前端代码质量,提升可读性,可维护性,可扩展性,可复用性。
额外补充:组件一搬分为展示型组件和容器型组件,容器型组件还可细分为页面级,组件级,根据作用范围也可以分为页面级,组件级,习惯在组件文件所在的目录创建hooks.ts来存放该级别组件需要的custom hooks。作用范围越大,越通用,文件向更外层提升,越靠近根目录。软件层级划分,划分好职责,每层做好自己的事情,调用下层接口,对上层提供接口,就可以搭积木了。