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

rc-redux-model 让你使用redux更简单 #43

Open
ForeverPx opened this issue Aug 29, 2020 · 0 comments
Open

rc-redux-model 让你使用redux更简单 #43

ForeverPx opened this issue Aug 29, 2020 · 0 comments
Assignees
Labels

Comments

@ForeverPx
Copy link

前言

今天给大家带来一款 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] 即可

API

每一个 model 接收 5 个属性,具体如下

参数 说明 类型 默认值
namespace 必须,且唯一 string -
state 数据状态,必须 object {}
action action,非必须 object -
reducers reducer,非必须 object -
openSeamlessImmutable 是否开启 Immutable,非必须 boolean false

提供的默认 Action

 @desc 注册生成默认的action
 @summary 使用方式

 this.props.dispatch({
   type: '[model.namespace]/setStore',
   payload: {
     key: [model.state.key]
     values: [your values]
   }
 })

相关文章

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants