mrdulin / blog

Personal Blog - 博客 | 编程技术,软件,生活

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

React & Redux应用架构

mrdulin opened this issue · comments

Cover image

架构基于React Hooks和React FC设计:

React&Redux application architecture

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。作用范围越大,越通用,文件向更外层提升,越靠近根目录。软件层级划分,划分好职责,每层做好自己的事情,调用下层接口,对上层提供接口,就可以搭积木了。


Flag Counter