Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React & Redux应用架构 #95

Open
mrdulin opened this issue Aug 5, 2021 · 0 comments
Open

React & Redux应用架构 #95

mrdulin opened this issue Aug 5, 2021 · 0 comments

Comments

@mrdulin
Copy link
Owner

mrdulin commented Aug 5, 2021

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

@mrdulin mrdulin changed the title React&Redux应用架构 React & Redux应用架构 Aug 5, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant