Giter Club home page Giter Club logo

rc-redux-model's Introduction

rc-redux-model 👋

English | 中文文档

Refer to dva's data flow solution and redux-thunk, internally implement middleware; provide default behavior action, call this action can directly modify any value in state, development is more convenient and concise, support Immutable ~

⛏ install

npm install rc-redux-model --save-dev

✨ feature

  • Lightweight and concise, writing data management is as comfortable as writing dva
  • Refer to redux-thunk, implement your own middleware internally to handle asynchronous actions
  • Abandon redux-saga, the asynchronous request can be processed by the user, or the provided method can be called to send, the return is a Promise
  • Provide the default action Action, call this Action, you can directly modify any value in the state
  • Support Immutable, just config it to make your data immutable

⏳ how did it come from ?

🚀 usage

Before using, please read this description again, and then read the complete example to get started quickly . 👉 If you want to know how it came, you can check here

  1. Create a new model folder, add a new userModel.js under the folder
import adapter from '@common/adapter';

const userModel = {
  namespace: 'userModel',
  openSeamlessImmutable: true,
  state: {
    classId: '',
    studentList: [],
    userInfo: {
      name: 'PDK',
    },
  },
  action: {
    // demo: dispatch an asynchronous request, change `globalModel` loading status
    // after the asynchronous request is over, modify reducers
    fetchUserInfo: async ({ dispatch, call }) => {
      // before the asynchronous request
      dispatch({
        type: 'globalModel/changeLoadingStatus',
        payload: true,
      });
      let res = await call(adapter.callAPI, params);
      // after the asynchronous request
      if (res.code === 0) {
        dispatch({
          type: 'userModel/setStore',
          payload: {
            key: 'userInfo',
            values: res.data,
          },
        });
        dispatch({
          type: 'globalModel/changeLoadingStatus',
          payload: false,
        });
      }
      return res;
    },
  },
};

export default userModel;
  1. Gather all models, please note that what is exported here is an array
// model/index.js
import userModel from './userModel';

export default [userModel];
  1. Process models, register middleware
// 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. Use in the page

Please note that the actions here are all asynchronous actions. Refer to redux-thunk for the implementation of internal middleware. That is to say, one of our dispatch and one action is a corresponding method. Look at the code:

class MyComponents extends React.PureComponent {
  componentDidMount() {
    // demo1 : dispatch an asynchronous action and modify the value of reducers after the request is completed
    // The request is written by yourself in model.action, which supports Promise.
    // Before we need to callback the data after the request, now you can get it directly by Promise.then()
    this.props
      .dispatch({
        type: 'userModel/fetchUserInfo',
      })
      .then((res) => {
        console.log(res);
      })
      .catch((err) => {
        console.log(err);
      });

    // demo2: call the `automatically generated default action` and directly modify the value of state.userInfo (this method is recommended)
    this.props.dispatch({
      type: 'userModel/setStore',
      payload: {
        key: 'userInfo',
        values: {
          name: 'sugarTeam',
        },
      },
    });
    // demo3: call the `automatically generated default action` to modify the state in the form of an array (this method is recommended)
    this.props.dispatch({
      type: 'userModel/setStoreList',
      payload: [
        {
          key: 'userInfo',
          values: {
            name: 'sugarTeam',
          },
        },
        {
          key: 'classId',
          values: 'sugarTurboS-666',
        },
      ],
    });
  }
}

hooks ?

// Usage with React Redux: Typing the useSelector hook & Typing the useDispatch hook
// https://redux.js.org/recipes/usage-with-typescript#usage-with-react-redux
import { useDispatch } from 'react-redux';

export function useFetchUserInfo() {
  const dispatch = useDispatch();
  return async (userId: string) => {
    // Here I choose to handle the asynchronous by myself.
    // After the asynchronous request is completed, I will pass the data to the reducer.
    const res = await MyAdapterAPI(userId);
    if (res.code === 200) {
      dispatch({
        type: 'userModel/setStore',
        payload: {
          key: 'userInfo',
          values: res.data,
        },
      });
    }
  };
}

Related description

For more information about rc-redux-model, please move to here : rc-redux-model design related instructions

API

Each model receives 5 attributes, as follows

parameter description type defaultValue
namespace the model's namespace, Must, and only string -
state the model's state,Must, and only object {}
action action,not necessary object -
reducers reducer,not necessary object -

default action to change state

  • modify single data
// Class Component  Writing
this.props.dispatch({
  type: '[model.namespace]/setStore',
  payload: {
    key: `${model.state.key}`,
    values: `${your values}`
  }
})

// Hooks Writing
import { useDispatch } from 'react-redux';
const dispatch = useDispatch();
dispatch({
  type: '[model.namespace]/setStore',
  payload: {
    key: `${model.state.key}`,
    values: `${your values}`
  }
})
  • modify multiple data
// Class Component  Writing
this.props.dispatch({
  type: '[model.namespace]/setStoreList',
  payload: [
    {
      key: `${model.state.key}`,
      values: `${your values}`
    },
    {
      key: `${model.state.key}`,
      values: `${your values}`
    }
  ]
})


// Hooks Writing
import { useDispatch } from 'react-redux';
const dispatch = useDispatch();
dispatch({
  type: '[model.namespace]/setStoreList',
  payload: [
    {
      key: `${model.state.key}`,
      values: `${your values}`
    },
    {
      key: `${model.state.key}`,
      values: `${your values}`
    }
  ]
})

Trends

You can check the downloads of the package by https://npmtrends.com/rc-redux-model

Maintainers

@PDKSophia

@SugarTurboS

Contributing

PRs accepted.

License

MIT © 2020 PDKSophia/SugarTurboS


This README was generated with ❤️ by readme-md-generator

rc-redux-model's People

Contributors

ajycc20 avatar foreverpx avatar pdksophia avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

rc-redux-model's Issues

rc-redux-model 介绍

Why rc-redux-model ?

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

以我为例,每次起一个项目,我都需要 :

  • 脚手架 create-react-app 快速生成一个应用框架,进行开发
  • 安装 redux 进行数据状态管理
  • 安装 react-redux ,调用 Provider 提供者模式,使得子组件都能取到 store 值
  • 如果想要解决异步请求,我也许还需要安装一个 redux-saga / redux-thunk
  • 如果想看到日志,那么我还会安装 redux-logger
  • ...

看似一顿操作猛如虎,其实心中已经 MMP,我会想,这么多前置工作,是不是让我的开发成本更高了呢?

为了解决异步 Action,我需要按照 redux-saga 或者 redux-thunk,从而处理异步问题,以 redux-saga 为例 :

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

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

分文件应该是一种默认的规范吧?反正我实习的时候,是分文件的;现在组里,是分文件的;看一些优秀库(star 多),也是分文件的;包括看 dva 的介绍,它也提到了问题

// 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 的出现,肯定是解决了一些问题。我们看看 dva 官网怎么说的 ~~

dva 首先是一个基于 redux 和 redux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-router 和 fetch,所以也可以理解为一个轻量级的应用框架。

有意思,但是因为 dva 身负重任,对我而言,使用它太过于“笨重”,我只是想取其精华,去其内置,我就只想用它写状态管理的方式: 在 model 里边,写完 reducer, state, action

再一次与 JPL 同学交流的过程中,发现他也有这种想法,同时他已经写了一个简单的中间件,在他们组里用了起来,出于学习以及如何写一个中间件,在参考它的代码之后,我也开始尝试写一个 redux 的中间件,并且内部支持默认的 action,让开发更加简洁,释放键盘上的 C 与 V ,同时对于 Immutable 的支持以及错误类型的检测,让你的 state 更加“合法”

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

What's rc-redux-model ?

rc-redux-model 是一个中间件,提供一种更为简洁和方便的数据状态管理[书写方式]。参考了 dva 的数据流方案,在一个 model 文件中写所有的 actionreducerstate,解读了 redux-thunk 的源码,内部实现了一个中间价,同时提供默认行为 action,调用此 action 可以直接修改任意值的 state,例如 :

只需要定义一个 model

export default {
  namespace: 'reduxModel',
  state: {
    testA: '',
    testB: [],
  },
}

那么 rc-redux-model 会自动帮你注册 action 及 reducers,等价于 action 和 reducers 不用你自己写了,如下 :

export default {
  namespace: 'reduxModel',
  state: {
    testA: '',
    testB: [],
  },
  action: {
    settestA: ({ commit, currentAction }) => {
      commit({
        type: 'SET_REDUXMODEL_TESTA',
        payload: currentAction.payload,
      })
    },
    settestB: ({ commit, currentAction }) => {
      commit({
        type: 'SET_REDUXMODEL_TESTB',
        payload: currentAction.payload,
      })
    },
    // 推荐使用此action进行修改reducers值
    setStore: ({ dispatch, currentAction }) => {
      dispatch({
        type: `reduxModel/change${currentAction.payload.key}`,
        payload: currentAction.payload.values,
      })
    },
  },
  reducers: {
    ['SET_REDUXMODEL_TESTA'](state, payload) {
      return {
        ...state,
        ...payload,
      }
    },
    ['SET_REDUXMODEL_TESTB'](state, payload) {
      return {
        ...state,
        ...payload,
      }
    },
  },
}

那么你只需要在组件中,调用的默认 Action 即可

class MyComponent extends React.Component {
  componentDidMount() {
    this.props.dispatch({
      type: 'reduxModel/setStore',
      payload: {
        key: 'testA',
        values: '666',
      },
    })
  }
}

hooks ?

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

// Usage with React Redux: Typing the useSelector hook & Typing the useDispatch hook
// https://redux.js.org/recipes/usage-with-typescript#usage-with-react-redux
import { useDispatch } from 'react-redux'

export function useFetchUserInfo() {
  const dispatch = useDispatch()
  return async (userId: string) => {
    // 这里我选择自己处理异步,异步请求完后,再把数据传到 reducer 中
    const res = await callAPI(userId)
    if (res.code === 200) {
      dispatch({
        type: 'userModel/setStore',
        payload: {
          key: 'userInfo',
          values: res.data,
        },
      })
    }
  }
}

强调说明

rc-redux-model 出发点在于解决我繁琐重复的工作,store 文件分散,state 类型和赋值错误的问题,为此,对于跟我一样的用户,提供了一个写状态管理较为[舒服]的书写方式,大部分情况下兼容原先项目~

  • 为了解决[store 文件分散],参考借鉴了 dva 写状态管理的方式,一个 model 中写所有的 action、state、reducers
  • 为了解决[繁琐重复的工作],提供默认的 action,用户不需要自己写修改 state 的 action,只需要调用默认提供的 [model.namespace/setStore] 即可,从而将一些重复的代码从 model 文件中剔除
  • 为了解决[state 类型和赋值错误],在每次修改 state 值时候,都会进行检测,如果不通过则报错提示

How to use

FAQ

可在现有的项目中兼容使用,具体使用方式,可参考完整例子

其他文章

如何使用 rc-redux-model 去实现数据管理

Code

以下是在项目中的真实使用,帮助小伙伴们更加直观的了解,以用户模块为主

原先流程

  1. 在 action 文件中定义
// 获取用户列表
export const fetchUserList = (params, callback) => {
  return {
    type: 'FETCH_USER_LIST',
    params,
    callback,
  }
}
  1. 在 saga 中监听此 action,并发起请求,请求完成,再 put 至 reducer 中
import adapter from '@/common/adapter'
import { call, put, takeLatest } from 'redux-saga/effects'

// 获取用户列表
function* fetchUserList({ params, callback }) {
  const res = yield call(adapter.callAPI, params)
  if (res.code === 0) {
    yield put({
      type: 'STORE_USER_LIST',
      data: res.data.userList,
    })
    if (callback) callback(res.data.userList)
  }
}

function* userSaga() {
  yield takeLatest('FETCH_USER_LIST', fetchUserList)
}

export default userSaga
  1. 切换至 reducers 文件中,自己写 set、merge 等逻辑
import Immutable from 'seamless-immutable'

const initialState = Immutable({
  userList: [],
})

function userReducer(state = initialState, action) {
  switch (action.type) {
    case 'STORE_USER_LIST':
      return Immutable.merge(state, 'userList', action.data)
    default:
      return state
  }
}

export default userReducer
  1. 回到组件中,import 这个 action 文件,然后去发起请求,并且通过 callback 回调,拿到请求后的数据
import * as userActions from './userAction'

class userComponent extends React.Component {
  componentDidMount() {
    this.props.dispatch(
      userActions.fetchUserList({ userStatus: 'online' }, (userList) => {
        console.log('userList: ', userList)
      })
    )
  }
}

使用了 rc-redux-model 之后的流程

  1. 在 model 文件中写所有的 action,默认给用户提供了修改 state 的 action,所以大部分情况下,你可以不用写 reducers
import Immutable from 'seamless-immutable'
import adapter from '@common/adapter'

export default {
  namespace: 'userModel',
  state: Immutable({
    userList: [],
  }),
  action: {
    // 获取用户列表
    fetchUserList: async ({ call, commit, currentAction }) => {
      const res = await call(adapter.callAPI, {
        actionName: 'FETCH_USER_LIST',
        params: currentAction.payload.params,
      })
      if (res.code === 0) {
        commit({
          type: 'userModel/setStoreLib',
          payload: {
            key: 'userList',
            values: {
              userList: res.data.userList,
            },
          },
        })
      }
      return res.data.userList || []
    },
  },
  openSeamlessImmutable: true,
}
  1. 页面中不需要引入 action 文件,直接 dispatch 这个 action 即可
class userComponent extends React.Component {
  componentDidMount() {
    this.props
      .dispatch({
        type: 'userModel/fetchUserList',
        payload: {
          params: {
            userStatus: 'online',
          },
        },
      })
      .then((userList) => {
        console.log('userList: ', userList)
      })
      .catch((err) => {
        console.log(err)
      })
  }
}

注意,记得把在引入store.createStore 的入口文件中,将 model.reducers 和 model.thunk 加上

// createStore.js
import { createStore, applyMiddleware, combineReducers } from 'redux'
import models from './models'
import RcReduxModel from 'rc-redux-model'

const reduxModel = new RcReduxModel(models)
const _rootThunk = reduxModel.thunk
const _rootReducers = reduxModel.reducers

const reducerList = combineReducers(_rootReducers)
return createStore(reducerList, applyMiddleware(_rootThunk))

rc-redux-model 的从0到1

前言

大家应该知道,react 是单向数据流的形式,它不存在数据向上回溯的技能,你要么就是向下分发,要么就是自己内部管理。

react 中,有 props 和 state,当我想从父组件给子组件传递数据的时候,可通过 props 进行数据传递,如果我想在组件内部自行管理状态,那可以选择使用 state。

很快,我遇到了一个问题,那就是兄弟组件之间如何进行通信?答案就是在父组件中管理 state,通过 props 下发给各子组件,子组件通过回调方式,进行通信

这会存在什么问题?你会发现如果你想共享数据,你得把所有需要共享的 state 集中放到所有组件顶层,然后分发给所有组件。

为此,需要一个库,来作为更加牛逼、专业的顶层 state 发给各组件,于是,我引入了 redux。

redux 体验

redux 可以说是较成熟,生态圈较完善的一个库了,搭配 redux-devtools-extension 这个 chrome 插件,让你开发更加快乐。然,世间万物,皆有利弊。

本身我使用 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 出发点在于解决繁琐重复的工作,store 文件分散,state 类型和赋值错误的问题,为此,对于跟我一样的用户,提供了一个写状态管理较为[舒服]的书写方式,大部分情况下兼容原先项目~

  • 为了解决[store 文件分散],参考借鉴了 dva 写状态管理的方式,一个 model 中写所有的 action、state、reducers
  • 为了解决[繁琐重复的工作],提供默认的 action,用户不需要自己写修改 state 的 action,只需要调用默认提供的 [model.namespace/setStore] 即可,从而将一些重复的代码从 model 文件中剔除
  • 为了解决[state 类型和赋值错误],在每次修改 state 值时候,都会进行检测,如果不通过则报错提示

初建雏形

由于之前看过 redux 源码,同时也看了一下 redux-thunk 的源码,并且查阅了一些相关文章,有了一些知识储备,说干就干~

参考了 dva 中对 model 的参数说明,因为我没有了 redux-saga ,所以是没有 effect 这个属性的,于是初步得到我的 model 参数

按照我的设想,我会存在多个 model 文件,聚集在一起之后,得到的是一个数组 :

import aModel from './aModel'
import bModel from './bModel'
import cModel from './cModel'

export default [aModel, bModel, cModel]

我所希望的是 : 传入一个 Array<IModelProps>,得到一个 RcReduxModel 对象,该对象拥有得给我导出 :

  • reducers: 所有 model.reducers 集合,这样我可以无障碍的用在 store.combineReducers中了,同时可以兼容你现有的项目,因为只要你用了 redux, 那么你肯定得通过 combineReducers API 去集合所有的 reducers
// createStore.js
import models from './models'
import RcReduxModel from 'rc-redux-model'

const reduxModel = new RcReduxModel(models)

const reducerList = combineReducers(reduxModel.reducers)
return createStore(reducerList)

因为我想像写 model 那样,所有东西都在一个文件中,自然而然,这个 action 集到 model 里边之后,如何处理异步就成了我需要解决的一个问题

异步处理

这里我可以将 redux-thunk 或者 redux-saga 集成进去,但是没必要。出于对这两个库的学习,以及在使用上带给我的[体验],我在想,能不能自行处理?然后给其添加自己的特色和功能?

于是,我去将 redux-thunk 的源码看了一遍,最后得出了一个解决方案 : 对比 redux-thunk ,其内部在于判断你的 action 是 function 还是 object,从而判断你的 action 是同步还是异步;而在 rc-redux-model 中,甭管三七二十一,我规定的每一个 action 都是异步的,也就是你发起的每一个 action,都是函数 :

aModel = {
  action: {
    // 这两个 action 都是 function
    firstAction: ({ getState, dispatch }) => {},
    secondAction: ({ getState, dispatch }) => {},
  },
}

即使你想要发起一个同步 action,去修改 state 的值,我也会将其作为异步进行处理,也就是你修改 state 值,你需要这么写 :

// 组件
this.props.dispatch({
  type: 'aModel/setStateA',
  payload: '666',
})
aModel = {
  namespace: 'aModel',
  state: {
    a: '111',
  },
  action: {
    // 这里是异步action,这里需要用户自己手动 dispatch 去修改 state 值
    setStateA: ({ currentAction, dispatch, commit }) => {
      dispatch({
        type: 'aModel/CHANGE_STATE_A',
        payload: currentAction.payload,
      })
      // 或者是使用 commit
      //   commit({
      //     type: 'CHANGE_STATE_A',
      //     payload: currentAction.payload,
      //   })
    },
  },
  reducers: {
    ['CHANGE_STATE_A'](state, payload) {
      return {
        ...state,
        a: payload,
      }
    },
  },
}

明确了这两点,接下来就只需要开发即可。如果前边看过我写 redux 源码分析到话,可以知道 reducer 是一个纯函数,所以我注册 reducer 中时,一定要明确这点: (以下代码摘抄 rc-redux-model 源码)

public registerReducers(model: IModelProps) {
    const { namespace, state, reducers } = model
    // 1检查 reducers
    invariant(reducers, `model's reducers must be defined, but got undefined`)

    // 1.1 得到所有 reducers 中的 action
    const reducersActionTypes = Object.keys(reducers)

    // 1.2 reducers 是一个纯函数,function(state, action) {}
    return (storeState: any, storeAction: any) => {
      const newState = storeState || state
      // 1.3 对 action 进行处理,规定 action.type 都是 namespace/actionName 的格式
      const reducersActionKeys = storeAction.type.split('/')

      const reducersActionModelName = reducersActionKeys[0]
      const reducersActionSelfName = reducersActionKeys[1]

      // 1.3.1 如果不是当前的 model
      if (reducersActionModelName !== namespace) return newState
      // 1.3.2 如果在 reducers 中存在这个 action
      if (reducersActionTypes.includes(reducersActionSelfName)) {
        return reducers[reducersActionSelfName](newState, storeAction.payload)
      }
      return newState
    }
  }

其次是对于中间件的开发,每一个中间件都是 store => next => action 的形式(不太了解中间件的可以自行去了解一波),所以我很简单就可以写出这段代码 :

const registerMiddleWare = (models: any) => {
  return ({ dispatch, getState }) => (next: any) => (action: any) => {
    // 这个 action 是我 this.props.dispatch 发起的action
    // 所以我需要找到它具体对应的是哪个 model.namespace 的
    // 前边已经对 model.namespace 做了判断,确保每个 model.namespace 必须唯一,不能重复
    // 找到该 model,然后再找到这个 model.action 中对应我发起的 action
    // 因为每一个 action 都是以 [model.namespace/actionName] 的形式,所以我可以 split 之后得到 namespace
    const actionKeyTypes = action.type.split('/')
    const actionModelName = actionKeyTypes[0]
    const actionSelfName = actionKeyTypes[1]

    const currentModel = getCurrentModel(actionModelName, models)

    if (currentModel) {
      const currentModelAction = currentModel.action
        ? currentModel.action[actionSelfName]
        : null
      // 参考redux-thunk的写法,判断是不是function,如果是,说明是个thunk
      if (currentModelAction && typeof currentModelAction === 'function') {
        return currentModelAction({
          dispatch,
          getState,
          currentAction: action,
        })
      }
      // 因为这里的action,可能是一个发到reducer,修改state的值
      // 但是在 model.action 中是直接写的是 commit reducerAction
      // 而我的每一个action都要[model.namespace/actionName]格式
      // 所以这里需要处理,并且判断这个action是不是在reducers中存在
      // 这里就不贴代码了,感兴趣的直接去看源码~
    }
  }
  return next(action)
}

上边是摘抄了部分源码,感兴趣的小伙伴可以去看看源码,并不多,并且源码中我都写了注释。经过不断调试,并且通过 jest 写了单元测试,并没有啥毛病,于是我兴致勃勃得给身边的同事安利了一波,没想到被 👊 打击了

提供默认行为,自动注册 action 及 reducers

“只有被怼过,才能知道你做的是什么破玩意”,在我给小伙伴安利的时候,他问 : “那你这东西,有什么用?”,我说写状态数据像写 dva 一样舒服,于是他又说,那我为什么不用 dva 呢?

解释一波后,他接着说: “不可否认的是,你这个库,写状态数据起来确实舒服,但我作为一个使用者,要在组里推广使用,仅靠此功能,是无法说服我组里的人都用你这个东西,除非你还能提供一些功能。听完你的介绍,你说你的 action 都是异步的,等价于修改 state 的 action,都需要我自己去写,假设我有 20 个 state,意味着我得在 model.action 中,写对应的 20 个修改 state 的 action,然后在 model.reducers 中同样写 20 个相对应的 reducer,作为使用者,我的工作量是不是很大,如果你能提供一个默认的 action 行为给我,那么我还可能会用”

仔细一想,确实如此,那我就提供一个默认的 action,用于用户修改 state 的值吧,当我提供了此 action 之后,我又发现,所有修改 state 的 action,都走同一个 action.type,那么在 redux-devtools-extension 中,是很难发现这个 action 触发,具体是为了修改哪个 state 值。

但是正如使用者说的,如果有 20 个 state 值,那么我为用户自动注册 20 个 action,用户在使用上是否需要记住每一个 state 对应的 action 呢?这肯定是极其不合理的,所以最终解决方案为 : 为每一个 state ,自动注册对应的 action 和 reducer, 同时再提供了一个默认的 action(setStore)

✨ 例 : state 有 n 个值,那么最终会自动注册 n+1 个 action,用户只需要记住并调用默认的这个 action(setStore) 即可

用户只需要调用默认提供的 setStore 即可,然后根据 key 进行判断,从而转发到对应到 action 上 ~ 使用起来极其简单

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

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

数据不可变

在函数式编程语言中,数据是不可变的,所有的数据一旦产生,就不能改变其中的值,如果要改变,那就只能生成一个新的数据。在我的项目中,我使用了 seamless-immutable,那么在 model.state 中,我使用了 Immutable 包裹了 state,然后调用默认提供的 action,最后会报错,懂的都懂 !

那么该怎么办呢?于是...我又在内部支持了 Immutable ,提供一个配置参数 openSeamlessImmutable,默认为 false,请注意,如果你的 state 是 Immutable,而在 model 中不设置此配置,那么会报错 !!!

// 使用 seamless-immutable

import Immutable from 'seamless-immutable'

export default {
  namespace: 'appModel',
  state: Immutable({}),
  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] 做一些类型检测处理,如 👍

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

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

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

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

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

结尾

到此,终于将一套流程走完,同时在组里的项目拉了个分支,实践使用了一波,完美兼容,未出问题。于是交付了第一个可使用的版本,这次一个中间件的开发,让我对 redux 的了解更近异步,最后,👏 欢迎大家留言一起交流

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.