rc-redux-model
rc-redux-model copied to clipboard
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.js
、reducers/user.js
、actions/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 的了解更近异步,最后,👏 欢迎大家留言一起交流