V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
SophiaPeng
V2EX  ›  开源软件

第一次写开源项目, redux 中间件,望老铁们给点建议

  •  
  •   SophiaPeng · 2020-08-27 16:11:45 +08:00 · 1544 次点击
    这是一个创建于 1337 天前的主题,其中的信息可能已经有所发展或是发生改变。

    前言

    今天给大家带来一款 redux 中间件 : 👏 rc-redux-model,✍️ 提供一种较为舒适的数据状态管理书写方式,让你简洁优雅的去开发;内部自动生成 action, 只需记住一个 action,可以修改任意的 state 值,方便简洁,从而释放你的 CV 键~

    源码仓库地址 : rc-redux-model ,如果你觉得不错,求个 ✨

    背景

    相信大家都了解 redux,并且也认同这种数据流的方式(毕竟不认同,你也不会用嘛~),然,世间万物,皆有利弊。

    本身我使用 redux 并不会有什么所谓的“痛点”,因为 redux 默认只支持同步操作,让使用者自行选择处理异步,对于异步请求 redux 是无能为力的。可以这么说,它保证自己是纯粹的,脏活累活都丢给别人去干。

    于是我的痛点在于 : 如何处理异步请求,为此我使用了 redux-saga 去解决异步的问题

    但是在使用 redux + redux-saga 中,我发现,这会让我的 [重复性] 工作变多(逐步晋升 CV 工程师),因为它在我们项目中,会存在啰嗦的样板代码。

    举个 🌰 : 异步请求,获取用户信息,我需要创建 sagas/user.jsreducers/user.jsactions/user.js,为了统一管理 const,我还会有一个 const/user.js,然后在这些文件之间来回切换。

    分文件应该是一种默认的规范吧?

    // const/user.js
    const FETCH_USER_INFO = 'FETCH_USER_INFO'
    const FETCH_USER_INFO_SUCCESS = 'FETCH_USER_INFO_SUCCESS'
    
    // actions/user.js
    export function fetchUserInfo(params, callback) {
      return {
        type: FETCH_USER_INFO,
        params,
        callback,
      }
    }
    
    // sagas/user.js
    function* fetchUserInfoSaga({ params, callback }) {
      const res = yield call(fetch.callAPI, {
        actionName: FETCH_USER_INFO,
        params,
      })
      if (res.code === 0) {
        yield put({
          type: FETCH_USER_INFO_SUCCESS,
          data: res.data,
        })
        callback && callback()
      } else {
        throw res.msg
      }
    }
    
    // reducers/user.js
    function userReducer(state, action) {
      switch (action.type) {
        case FETCH_USER_INFO_SUCCESS:
          return Immutable.set(state, 'userInfo', action.data)
      }
    }
    

    没错, 这种样板代码,简直就是 CV 操作,只需要 copy 一份,修改一下名称,对我个人而言,这会让我不够专注,分散管理 const 、action 、saga 、reducer 一套流程,需要不断的跳跃思路。

    而且文件数量会变多,我是真的不喜欢如此繁琐的流程,有没有好的框架能帮我把这些事都做完呢?

    dva

    以下引用 dva 官网的介绍 :

    基于 redux 和 redux-saga 的数据流方案,让你在一个 model 文件中写所有的 action 、state 、effect 、reducers 等,然后为了简化开发体验,内置了 react-router 和 fetch.

    聊聊我对 dva 的看法,官方说了,基于 redux + redux-saga 的方案,只是在你写的时候,都写在一个 model 文件,然后它帮你做一些处理;其次它是一个框架,而不是一个库,是否意味着: 我在项目开始之前,我就需要确定项目的架构是不是用 dva,如果开发一半,我想换成 dva 这种状态管理的写法,而去引入 dva,是否不合理?

    再或者,我只是做一些 demo 、写点小型的个人项目,但我又想像写 dva 的数据状态管理 model 那种方式,引入 dva 是不是反而变得笨重呢?

    回过头来看,我的出发点是 : 在于解决繁琐重复的工作,store 文件分散,state 类型和赋值错误的问题,为此,对于跟我一样的用户,提供了一个写状态管理较为[舒服]的书写方式,大部分情况下兼容原先项目,只需要安装这个包,就能引入一套数据管理方案,写起来又舒服简洁,开心开心的撸代码,不香吗?

    于是 rc-redux-model 就这样出现了~

    rc-redux-model

    需要明确的是 : rc-redux-model 是一个中间件,提供一种更为简洁和方便的数据状态管理[书写方式]

    参考了 dva 的数据流方案,在一个 model 文件中写所有的 actionreducerstate,解读了 redux-thunk 的源码,内部实现了一个中间件,同时提供默认行为 action,调用此 action 可以直接修改任意值的 state,方便简洁,让你忍不住说 WC

    特性

    • 轻巧简洁,写数据管理就跟写 dva 一样舒服
    • 异步请求由用户自行处理,内部支持 call 方法,可调用提供的方法进行转发,该方法返回的是一个 Promise
    • 参考 redux-thunk,内部实现独立的中间件,所有的 action 都是异步 action
    • 提供默认行为 action,调用此 action,可以修改任意的 state 值,解决你重复性写 action 、reducers 问题
    • 内置 seamless-immutable ,只需开启配置,让你的数据不可变
    • 默认检测不规范的赋值与类型错误,让你的数据更加健壮

    安装

    npm install --save rc-redux-model
    

    相关说明

    如何定义一个 model 并自动注册 action 及 reducers ?

    每一个 model 必须带有 namespace 、state,action 与 reducers 可不写,如需开启 immutable,需配置 openSeamlessImmutable = true,一个完整的 model 结构如下

    export default {
      namespace: '[your model.namespace]',
      state: {
        testA: '',
        testB: false,
        testC: [],
        testD: {},
      },
    }
    

    rc-redux-model 会根据你的 state,每一个 state 的字段都会自动注册一个修改此 state 的 action,从而释放你键盘上的 ⌨️ CV 键, 例如 :

    state: {
      userName: 'oldValue'
    }
    

    那么会自动为你注册一个 action,action 名以 set${stateName} 格式,如你的 stateName 为 : userName,那么会自动注册的 action 为 : setuserName

    action: {
      setuserName: ({ dispatch, getState, commit, call, currentAction }) => {}
    }
    

    你只要在组件中调用此 action 即可修改 state 值 (📢 不推荐使用这种 action 进行修改 state 值,推荐使用 setStore

    this.props.dispatch({
      type: 'userModel/setuserName',
      payload: {
        userName: 'newValue',
      },
    })
    

    问题来了,当 state 中的值很多(比如有几十个),那么为用户自动注册几十个 action,用户在使用上是否需要记住每一个 state 对应的 action 呢?这肯定是极其不合理的,所以一开始是提供一个默认的 action,用于修改所有的 state 值 ...

    随之而来的问题是,如果只提供一个 action,那么所有修改 State 的值都走的这个 action.type,在 redux-devtools-extension 中,会看不到具体的相对信息记录(因为都是同一个 action),最终,还是提供一个默认的 action,此 action 会根据用户提供的 payload.key,从而转发至对应的 action 中。

    ✨ 对外提供统一默认 action,方面用户使用;对内根据 key,进行真实 action 的转发

    this.props.dispatch({
      type: '[model.namespace]/setStore',
      payload: {
        key: [model.state.key]  // 你要修改的 state key
        values: [your values] // 你要修改的值
      }
    })
    

    🌟 所有修改 state 的 action,都通过 setStore 来发,不必担心在 redux devtools 中找不到,此 action 只是会根据你的 key,转发对应的 action 而已

    如何发送一个 action ?

    一个 action 由 type 、payload 组成,type 的命名规则为 : [model.namespace / actionName]

    // 下边是 namespace = appModel,actionName = fetchUserList 的例子
    const action = {
      type: 'appModel/fetchUserList',
    }
    // 发起这个 action
    this.props.dispatch(action)
    

    请注意,这里的每一个 action 都是 function, 也就是说,处理 同步 action 的思路跟处理 异步 action是一样的,如果你不明白,👉 请移步这里

    异步请求由谁处理 ?

    model.action 中,每一个 action 都是 function,它的回调参数为 :

    • dispatch : store 提供的 API,你可以调用 dispatch 继续分发 action
    • getState : store 提供的 API,通过该 API 你可以得到最新的 state
    • currentAction : 当前你 this.props.dispatch 的 action,你可以从这里拿到 typepayload
    • call : 替你转发请求,同时会使用 Promise 包裹,当然你可以自己写异步逻辑
    • commit : 接收一个 action,该方法用于 dispatch action 到 reducers,从而修改 state 值

    可以自己处理异步,再通过调用默认提供的 [model.namespace/setStore] 这个 action 进行修改 state 值

    如何在组件中获取 state 值?

    请注意,rc-redux-model 是一个中间件,并且大部分情况下,能够在你现有的项目中兼容,所以获取 state 的方式,还是跟你原来在组件中如何获取 state 一样

    一般来讲,我们的项目都会安装 react-redux 库,然后通过 connect 获取 state 上的值(没什么变化,你之前怎么写,现在就怎么写)

    class appComponent extends React.Component {
      componentDidMount() {
        // 发起 action,将 loading 状态改为 true
        this.props.dispatch({
          type: 'appModel/fetchLoadingStatus',
          payload: {
            loadingStatus: true,
          },
        })
      }
    
      render() {
        const { loadingStatus } = this.props.appModel
        console.log(loadingStatus) // true
      }
    }
    
    const mapStateToProps = (state) => {
      return {
        appModel: state.appModel,
        reportTaskInfo: state.reportModel.taskInfo, // 其他 model 的值
      }
    }
    
    export default connect(mapStateToProps)(appComponent)
    

    如果很不幸,你项目中没安装 react-redux,那么你只能在每一个组件中,引入这个 store,然后通过 store.getState() 拿到 state 值了

    但是这种方式的缺陷就是,你要确保你的 state 是最新的,也就是你改完 state 值之后,需要重新 store.getState() 拿一下最新的值,这是比较麻烦的

    import store from '@your_folder/store' // 这个 store 就是你使用 Redux.createStore API 生成的 store
    
    class appComponent extends React.Component {
      constructor() {
        this.appState = store.getState()['appModel']
      }
    }
    

    数据不可变的(Immutable) ?

    在函数式编程语言中,数据是不可变的,所有的数据一旦产生,就不能改变其中的值,如果要改变,那就只能生成一个新的数据。如果有看过 redux 源码的小伙伴一定会知道,为什么每次都要返回一个新的 state,如果没听过,👉 可以看下这篇文章

    目前 rc-redux-model 内部集成了 seamless-immutable,提供一个 model 配置参数 openSeamlessImmutable,默认为 false,请注意,如果你的 state 是 Immutable,而在 model 中不设置此配置,那么会报错 !!!

    // 使用 seamless-immutable
    
    import Immutable from 'seamless-immutable'
    
    export default {
      namespace: 'appModel',
      state: Immutable({
        username: '',
      }),
      openSeamlessImmutable: true, // 必须开启此配置
    }
    

    类型正确性 ?

    不可避免,有时在 model.state 中定义好某个值的类型,但在改的时候却将其改为另一个类型,例如 :

    export default {
      namespace: 'userModel',
      state: {
        name: '', // 这里定义 name 为 string 类型
      },
    }
    

    但在修改此 state value 时,传递的确是一个非 string 类型的值

    this.props.dispatch({
      type: 'userModel/setStore',
      payload: {
        key: 'name',
        values: {}, // 这里 name 变成了 object
      },
    })
    

    这其实是不合理的,在 rc-redux-model 中,会判断 state[key] 中的类型与 payload 传入的类型进行比较,如果类型不相等,报错提示

    所有修改 state 的值,前提是 : 该值已经在 state 中定义,以下情况也会报错提示

    export default {
      namespace: 'userModel',
      state: {
        name: '', // 这里只定义 state 中存在 name
      },
    }
    

    此时想修改 state 中的另一属性值

    this.props.dispatch({
      type: 'userModel/setStore',
      payload: {
        key: 'age',
        values: 18, // 这里想修改 age 属性的值
      },
    })
    

    极度不合理,因为你在 state 中并没有声明此属性,rc-redux-model 会默认帮你做检测

    使用

    如有疑问,看下边的相关说明~ 同时对于如何在项目中使用,👉 可以点这里

    提供默认 action,无需额外多写 action/reducers

    原先,我们想要修改 state 值,需要在 reducers 中定义好 action,但现在, rc-redux-model 提供默认的 action 用于修改,所以在 model 中,只需要定义 state 值即可

    export default {
      namespace: 'appModel',
      state: {
        value1: '',
      },
    }
    

    在页面中,只需要调用默认的 [model.namespace/setStore] 就可以修改 state 里的值了,美滋滋,不需要你自己在 action 、reducers 去写很多重复的修改 state 代码

    this.props.dispatch({
      type: 'appModel/setStore',
      payload: {
        key: 'value1',
        values: 'appModel_state_value1',
      },
    })
    

    复杂且真实的例子

    1. 新建一个 model 文件夹,该文件夹下新增一个 userModel.js
    // model/userModel.js
    import adapter from '@common/adapter'
    
    const userModel = {
      namespace: 'userModel',
      openSeamlessImmutable: false,
      state: {
        classId: '',
        studentList: [],
        userInfo: {
          name: 'PDK',
        },
      },
      action: {
        // demo: 发起一个异步请求,修改 global.model 的 loading 状态,异步请求结束之后,修改 reducers
        // 此异步逻辑,可自行处理,如果采用 call,那么会通过 Promise 包裹一层帮你转发
        fetchUserInfo: async ({ dispatch, call }) => {
          // 请求前,将 globalModel 中的 loading 置为 true
          dispatch({
            type: 'globalModel/changeLoadingStatus',
            payload: true,
          })
          let res = await call(adapter.callAPI, params)
          if (res.code === 0) {
            dispatch({
              type: 'userModel/setStore',
              payload: {
                key: 'userInfo',
                values: res.data,
              },
            })
            // 请求结束,将 globalModel 中的 loading 置为 false
            dispatch({
              type: 'globalModel/changeLoadingStatus',
              payload: false,
            })
          }
          return res
        },
      },
    }
    
    export default userModel
    
    1. 聚集所有的 models,请注意,这里导出的是一个数组
    // model/index.js
    import userModel from './userModel'
    
    export default [userModel]
    
    1. 处理 models,注册中间件
    // createStore.js
    import { createStore, applyMiddleware, combineReducers } from 'redux'
    import models from './models'
    import RcReduxModel from 'rc-redux-model'
    
    const reduxModel = new RcReduxModel(models)
    
    const reducerList = combineReducers(reduxModel.reducers)
    return createStore(reducerList, applyMiddleware(reduxModel.thunk))
    
    1. 在组件中调用
    class MyComponents extends React.PureComponent {
      componentDidMount() {
        // demo: 发起一个异步请求,修改 global.model 的 loading 状态,异步请求结束之后,修改 reducers
        // 具体的请求,在 model.action 中自己写,支持 Promise,之前需要 callback 回调请求后的数据,现在直接 then 获取
        this.props
          .dispatch({
            type: 'userModel/fetchUserInfo',
          })
          .then((res) => {
            console.log(res)
          })
          .catch((err) => {
            console.log(err)
          })
    
        // demo1: 调用自动生成的默认 action,直接修改 state.userInfo 的值 (推荐此方法)
        this.props.dispatch({
          type: 'userModel/setStore',
          payload: {
            key: 'userInfo',
            values: {
              name: 'setStore_name',
            },
          },
        })
        // demo2: 调用自动生成的默认 action,直接修改 state.classId 的值 (推荐此方法)
        this.props.dispatch({
          type: 'userModel/setStore',
          payload: {
            key: 'classId',
            values: 'sugarTeam2020',
          },
        })
      }
    }
    

    hooks ?

    hooks 的出现,让我们看到了处理复杂且重复逻辑的曙光,那么问题来了,在 hooks 中能不能用 rc-redux-model,我想说 : “想啥呢,一个是 react 的特性,一个是 redux 的中间件, 冲突吗?”

    建议在 hooks 中把所有的业务逻辑,包括异步请求等都做了,调用 dispatch 只是为了修改 state 的值,这样你的 model 文件就极其干净,只需要写 namespacestate,action 和 reducers 都不需要写了,使用默认提供的 [model.namespace/setStore] 即可

    源码仓库地址 : rc-redux-model ,如果你觉得不错,求个 ✨ 感谢老哥们

    目前尚无回复
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   1078 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 19:08 · PVG 03:08 · LAX 12:08 · JFK 15:08
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.