阅读 525

【KT】你还在redux中写重复啰嗦的样板代码吗

前言

📢 博客首发 : 阿宽的博客

📢 团队博客: SugarTurboS

🌈 仓库源码 : rc-redux-model ,求个 ✨ star

最近受 JPL 同学的启发,写了一个中间件 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)
复制代码

异步处理

其实我更加希望的是,由用户自行处理异步,然后再发起同步Action,将数据塞到 redux 中,但是异步处理还是得提供的,所以就得想,该如何处理异步问题?

相对比 redux-thunkredux-saga ,在看了 thunk 的源码之后,觉得其极简单,并且更倾向于我的出发点,出于学习,以及在使用上带给我的[体验],我在想,能否参考一波源码,然后实现一个?

issues区有人提问为什么使用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 即可,然后根据 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 会默认帮你做检测

安利一波

更多相关信息可前往 : 🌈 rc-redux-model 中查看,同时有问题可以在 issues 中交流

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 ,只需开启配置,让你的数据不可变
  • 默认检测不规范的赋值与类型错误,让你的数据更加健壮

使用

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

提供默认 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',
  },
})
复制代码

结尾

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

相关链接