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 的从0到1 #2

Open
PDKSophia opened this issue Aug 22, 2020 · 0 comments
Open

rc-redux-model 的从0到1 #2

PDKSophia opened this issue Aug 22, 2020 · 0 comments
Assignees
Labels
design 设计

Comments

@PDKSophia
Copy link
Collaborator

PDKSophia commented Aug 22, 2020

前言

大家应该知道,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 的了解更近异步,最后,👏 欢迎大家留言一起交流

@PDKSophia PDKSophia added the documentation Improvements or additions to documentation label Aug 22, 2020
@PDKSophia PDKSophia pinned this issue Aug 22, 2020
@PDKSophia PDKSophia added docs 文档 design 设计 and removed documentation Improvements or additions to documentation docs 文档 labels Aug 22, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
design 设计
Projects
None yet
Development

No branches or pull requests

2 participants