从Vue换到React+Redux进行开发已经有半年多的时间,总的来讲体验是很好的,对于各类逻辑和业务组件的抽象实在是方便的不行,高阶组件,洋葱模型等等给我带来了不少编程思想上的提高。可是在使用Redux开发的过程当中仍是感受不太顺手,本文将阐述我是如何对Redux进行一步步“改造”以适应我的和团队开发需求的。
过程当中的示例和结果放在了easy-redux,欢迎star。
原文连接前端
在使用Redux开发的过程当中逐渐发现,虽然咱们已经将UI组件和业务组件尽量的进行抽离,尽量的保证reduceractions的复用性,
可是咱们仍是会花费大量的时间来书写近乎相同的代码。尤为是咱们组内但愿秉承一个原则:尽可能将全部的操做及状态修改都交由action来执行,方便咱们对问题进行定位。当我在某大型前端交流群里还看到“不用Redux,不想加班”的说法时,不得不感叹,须要作些努力来解决我目前的问题了。react
是的,Redux对我来讲,太复杂了git
针对一个简单的操做,咱们须要进行如下步骤来完成:github
1.定义action编程
export const CHANGE_CONDITION = 'CHANGE_CONDITION'
2.定义一个对应的action建立函数redux
export const changeCondition = condition => ({ type: CHANGE_CONDITION, condition })
3.引入action, 定义reducer, 在复杂的switch语句中,对对象进行更改api
import { CHANGE_CONDITION } from '@actions' const condition = (state = initCondition, action) => { switch(action.type) { case CHANGE_CONDITION: return ... default: return state } }
4.在须要时,引入action建立函数, 并将对应的state进行链接app
import { changeCondition } from 'actions' @connect(...)
我只是想作一个简单的状态修改呀!异步
可能咱们会说,这样拆分可以保证咱们整个项目的规范化,加强业务的可预测性与错误定位能力。
可是随着项目的不断扩大,每一个页面都有一堆action须要我加的时候,实在是让人头痛啊。函数
并且,针对请求的修改,咱们每每要把action拆分红START,SUCCESS,FAILED三种状态,reducer里须要进行三次修改。并且每每
针对这些修改,咱们进行的处理都是大体相同的:更新loading状态,更新数据,更新错误等等。
因此说,咱们如何在保证redux的设计原则以及项目规范性上,对其进行“简化改造”,是我这里须要解决的问题。
针对请求的处理,我以前也写过一篇文章优雅地减小redux请求样板代码, 经过封装了一个redux中间件react-fetch-middleware
来对请求代码进行优化。
大体思路以下:
1.action建立函数返回的内容为一个包含请求信息的对象,并包含须要分发的三个action,这三个action能够经过actionCreator进行建立
import { actionCreator } from 'redux-data-fetch-middleware' // create action types export const actionTypes = actionCreator('GET_USER_LIST') export const getUserList = params => ({ url: '/api/userList', params: params, types: actionTypes, // handle result handleResult: res => res.data.list, // handle error handleError: ... })
2.在redux中间件中,针对以上格式的action进行处理,首先进行请求,并分发请求开始的action,
在请求成功和失败时,分别分发对应的action
const applyFetchMiddleware = ( fetchMethod = fetch, handleResponse = val => val, handleErrorTotal = error => error ) => store => next => action => { // 判断action的格式 if (!action.url || !Array.isArray(action.types)) { return next(action) } // 获取传入的三个action const [ START, SUCCESS, FAILED ] = action.types // 在不一样状态分发action, 并传入loading,error状态 next({ type: START, loading: true, ...action }) return fetchMethod(url, params) .then(ret => { next({ type: SUCCESS, loading: false, payload: handleResult(ret) }) }) .catch(error => { next({ type: FAILED, loading: false, error: handleError(error) }) }) }
3.将reducer进行对应的默认处理,使用reducerCreator建立的函数中自动进行对应处理,而且提供二次处理的机制
const [ GET, GET_SUCCESS, GET_FAILED ] = actionTypes // 会在这里自动处理分发的三个action const fetchedUserList = reducerCreator(actionTypes) const userList = (state = { list: [] }, action => { // 二次处理 switch(action.type) { case GET_SUCCESS: return { ...state, action.payload } } }) export default combineReducers({ userList: fetchedUserList(userList) })
通过前一步对请求的简化,咱们已经能够在保证不改变redux原则和书写习惯的基础上,极大的简化请求样板代码。
针对普通的数据处理,咱们是否是能够更进一步?
很高兴看到这个库: Rematch
, 对Redux Api进行了极大的简化。
可是有些功能和改进并非咱们想要的,所以我仅对我须要的功能和改进点进行说明,并用本身的方式进行实现。咱们来一步步看看
咱们须要解决的问题以及如何解决的。
针对reducer,咱们不但愿重复的引用定义的各个action, 而且去掉冗长的switch判断。其实咱们能够将其进行反转拆分,将每个action定义为标准化的reducer, 在其中对state进行处理.
const counter = { state: 1, reducers: { add: (state, payload) => state + payload, sub: (state, payload) => state - payload } }
去掉以前的action和action建立函数,直接在actions中进行数据处理,并与对应的reducer进行match
export const addNum = num => dispatch => dispatch('/counter/add', num)
咱们会看到,与reducer进行match时,咱们使用了'/counter/add'这种命名空间的方式,
目的是在保证其直观性的同时,保证action与其reducer是一一对应的。
咱们能够经过加强的combinceReducer进行命名空间的设定:
const counter1 = { ... } const counter2 = { ... } const counters = combinceReducer({ counter1, counter2 }) const list = { ... } // 设置大reducer的根命名空间 export default combinceReducer({ counters, list }, '/test') // 咱们能够经过这样来访问 dispatch('/test/counters/counter1/add')
针对请求这些异步action,咱们能够参考咱们以前的修改, dispatch一个对象
export const getList = params => dispatch => { return dispatch({ //对应到咱们想要dispatch的命名空间 action: '/list/getList', url: '/api/getList', params, handleResponse: res => res.data.list, handleError: error => error }) }
同时,咱们在reducer中进行简单的处理便可,依旧能够进行默认的三个状态处理
const list = { // 定义reducer头,会自动变为getList(开始请求),getListSuccess,getListFailed // 并进行loading等默认处理 fetch: 'getList' state: { list: [] }, reducers: { // 二次处理 getListSuccess: (state, payload) => ({ ...state, list: payload }) } }
咱们会看到,咱们已经将redux的api进行了极大的简化,可是依旧保持了原有的结构。目的有如下几点:
原有的数据流变成了这样:
所以,咱们是在redux的基础上进行二次封装的,咱们依然保证了原有的Redux数据流,保证数据的可回溯性,加强业务的可预测性与错误定位能力。这样能极大的保证与老项目的兼容性,因此咱们须要作的,只是对action和reducer的转化工做
咱们经过新的combinceReducer,将新的格式,转化为以前的reducer格式,并保存各个reducer其和对应的action的命名空间。
代码简单示意:
//获取各reducers里的方法 const actionNames = Object.keys(reducers) const resultActions = actionNames.map(action => { const childNamespace = `${namespace}/${action}` // 将action存入namespace Namespace.setActionByNamespace(childNamespace) return { name: Namespace.toAction(childNamespace), fn: reducers[action] } }) // 返回默认格式 return (state = inititalState, action) => { // 查询action对应的新的reducer里的方法 const actionFn = resultActions.find(cur => cur.name === action.type) if (actionFn) { return actionFn.fn && actionFn.fn(state, action.payload) } return state }
咱们须要把这样格式的函数,转化成这样
count => dispatch => dispatch('/count/add', count) //or params => dispatch => { dispatch('/count/add', 1), dispatch('/count/sub', 2) } //结果 count => ({ type: 'count_add', payload: count })
这里的处理比较复杂,其实就是改造咱们的dispatch函数
action => params => (dispatch, getstate) => { const retDispatch = (namespace, payload) => { return dispatch({ type: Namespace.get(namespace), payload }) } return action(params)(retDispatch, getstate) }
经过对Redux Api的改造,至关于二次封装,已经很大的简化了目前在项目中的样板代码,而且在项目中很顺畅的使用。
针对整个过程,其实还有几个能够改进的地方:
有兴趣的话,欢迎探讨~ 附上github easy-redux