业务背景介绍:腾讯云数据库产品中心 & 大数据及人工智能产品中心 前端从2016年初开始尝试 React + Redux 全家桶,期间经历了很多波折,到目前为止总共28个项目,其中有15个项目使用了该方案
Redux 在我看来除了提供统一的状态管理,最大好处就是实现 视图、业务逻辑 与 数据处理的分离,这样可以最大程度地去复用三个模块。
图:Redux 对项目的模块拆分
从这种意义上来说,它是成功的,但是实际的开发过程中,却遇到很多问题,导致开发体验非常不友好。
1、丑陋的switch case
做过 Redux 开发的一定对 Reducer 不陌生,里面主要靠 switch case 来处理 action。对于一个状态复杂的应用,一般使用 combineReducers来进行模块拆分,进而减少switch case的长度,使得模块化的 Reducer 可维护。实际应用中,往往比较考验开发者的模块划分能力,一些比较复杂的模块,不进行很好的拆分和重构,伴随着业务的变化 switch case 任然会增长很长。但如果你拆分得过细,Reducer与应用的状态树就会变得复杂。
下面是一个典型的表格 reducer:
var initailState = {
columns,
isLoading: true,
list: [],
result: undefined,
selectedSet: new Set(),
searchKey: ''
};
export default function (state = initailState, action) {
switch (action.type) {
case actions.ON_SEARCH:
return {
...state,
searchKey: action.searchKey
};
case actions.ON_TABLE_RELOAD://重新加载
return {
...state,
isLoading: true,
selectedSet: new Set()
};
case actions.ON_TABLE_LOADED://加载完成
return {
...state,
isLoading: false,
list: action.result && action.result.data && action.result.data.mixIpList || [],
result: action.result
};
case actions.ON_TABLE_ALL_CHECK_CHANGE://全选
{
const {isCheck} = action,
{list} = state;
let selectedSet = new Set();
if (isCheck) {
list.map((item)=>selectedSet.add(item.id));
}
return {...state, selectedSet};
}
case actions.ON_TABLE_ITEM_SELECT_CHANGE://选中一项
{
const {id, isCheck} = action;
let {selectedSet, list} = state;
if (isCheck) {
selectedSet.add(id);
} else {
selectedSet.delete(id);
}
return {...state, selectedSet};
}
case actions.ON_PROJECT_LOADED:
{
if(state.list.length){
var plist = action.list;
var li = state.list.concat();
li.forEach(function (vip) {
plist.forEach(function (proj) {
if(vip.projectId == proj.value){
vip.projectName = proj.text;
}
});
});
return {...state, list: li};
}else{
return state;
}
}
default:
return state;
}
}
2、令人头晕的开发体验
Redux 要实现 视图、业务逻辑 与 数据处理的分离,其实默认要求开发者开发过程是纵向的,但实际的开发过程中,大多数人的开发过程是横向的,如下图:
图:开发过程
这就导致一个问题,开发者会在 Reducer、ActionCreator、View 三者来回切换开发,在阅读一个项目源码的时候,也需要来回切换查阅,才能清晰地知道某个模块的逻辑。当模块的 Action 足够多,足够复杂,并且你显示器又不够大的时候,上面的过程往往就会把你绕晕了。
经历了很多项目,我观察到 Reducer 的一个代码特点,大量的 switch case 下都是简单的数据加工合成新的状态子树,这里可以通过统一的扩展覆盖方式来实现这个目标。
首先,我将 Dispatch 的方法设计为:
dispatch({
type: actions.ON_REPORT_LOAD_COMPLETED,
report:{
isLoadingError: false,
original: result.data[0],
compare: result.data.length > 1 ? result.data[1] : null,
}
})
这样,依靠关键字 report 可以用来做 Reducer 匹配,对应 report里面的内容可以直接在原有状态子树的基础上扩展覆盖生成新的状态子树。
对应 report的 Reducer 设计如下:
function reportReducer(
state = {
isLoadingError: false,
original:{},
compare:{}
},
action) {
let newState = {...state, ...action['report']};
return newState;
}
按照上述的方法,我们就解决了switch case的问题,action.type在这里的作用就只有 Redux DevTools 的回溯才会用到。
还可以近一步地优化,可以写一个方法来返回 Reducer 方法,这样就不用再重复写相同 Reducer 的扩展逻辑,如下:
function autoReducerCreator(initializeState, id) {
return (state = initializeState, action) => {
let updateState = action[id];
//没有更新的状态,不进行处理
if(typeof updateState == 'undefined'){
return state;
}
//只更新原始状态子树有的属性
//let newState = {...state, ...action[id]};
let newState = {};
for(let key in state){
if(updateState.hasOwnProperty(key)){
newState = updateState[key];
} else{
newState = state[key];
}
}
return newState;
}
}
//生成 reportReducer
let reportReducer = autoReducerCreator({
isLoadingError: false,
original:{},
compare:{}
}, 'report');
let mainReducer = autoReducerCreator({
isLoadingError: false,
title: '-',
content: '-'
}, 'main');
//组合 reducer
let reducers = combineReducers({
report: reportReducer,
main: mainReducer
});
最后还可以近一步创建一个函数分析状态对象,自动生成 Reducer,使得接口调用更简单,对整个 Redux 状态树更直观,如下:
function combineAutoReducers(initializeState){
let reducers = {};
for(let key in initializeState){
let subState = initializeState[key];
reducers[key] = autoReducerCreator(subState, key);
}
return combineReducers(reducers);
}
combineAutoReducers({
report:{
isLoadingError: false,
original:{},
compare:{}
},
main:{
isLoadingError: false,
title: '-',
content: '-'
}
})
回到第一张图 Redux 的本意应该是数据与业务分离,数据处理的代码被分割到 Reducer 里,而业务逻辑放到 ActionCreator 里,而上述的优雅方案从某种程度上来会打破这种设定。但我想说的是这是一种折中,将 Reducer 90%代码压缩掉,剩余10%的数据处理代码不可避免的分散到 ActionCreator里,经过实际项目经历,其他同事均反馈开发效率与代码阅读体验得到很大提升。
当然最后的这个工具也保留了对原生 Reducer 的兼容方法。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。