如何更优雅地使用 Redux

业务背景介绍:腾讯云数据库产品中心 & 大数据及人工智能产品中心 前端从2016年初开始尝试 React + Redux 全家桶,期间经历了很多波折,到目前为止总共28个项目,其中有15个项目使用了该方案

一、Redux开发噩梦

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 的兼容方法。

原创声明,本文系作者授权云+社区-专栏发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

2 条评论
登录 后参与评论

相关文章

来自专栏编程微刊

Jquery前端分页插件pagination同步加载和异步加载

1333
来自专栏彭湖湾的编程世界

【redux】详解react/redux的服务端渲染:页面性能与SEO

亟待解决的疑问 为什么服务端渲染首屏渲染快?(对比客户端首屏渲染) react客户端渲染的一大痛点就是首屏渲染速度慢问题,因为react是一个单页面应用,大多数...

2457
来自专栏李成熙heyli

性能优化三部曲之三——Node直出让你的网页秒开

项目: 手Q群成员分布直出 原因: 为家校群业务直出做准备 群成员分布业务是小型业务,而且逻辑相当简单,方便做直出试验田 基本概念: 直出其实并不算是新概念。只...

4537
来自专栏包子铺里聊IT

DAG、Workflow 系统设计、Airflow 与开源的那些事儿

DAG (Directed Acyclic Graph) 是一个非常有用、也有很有意思的数据结构。如果说数组、链表、二叉树这类数据结构是学习中的基础,那么 DA...

3154
来自专栏c#开发者

web开发web form,mvc,Silverlight比较优缺点

最近一段时间比较闲,所以顺便尝试去了解一些新东西,虽然不做开发好多年,但最始终还是觉得做开发(coding)来的最轻松,也最拿手,做项目经理真的很烦,看来我还...

2584
来自专栏ThoughtWorks

React全家桶与前端单元测试艺术|洞见

TL;DR——什么是好的单元测试? 其实我是个标题党,单元测试根本没有“艺术”可言。 好的单元测试来自于好的代码,如果说有艺术,那也是代码的艺术。 注:以下“...

3006
来自专栏前沿技墅

漫谈前端性能本质 突破React应用瓶颈

侯策:硕士毕业于法国国立高等电信学校。曾任职于BePATIENT集团,负责互联网+医疗平台的研发。曾任职于法国能源和苏伊士集团,参与欧洲天然气运输和费用系统的研...

521
来自专栏携程技术中心

携程2015 Open House获奖项目:响应式的蜕变

响应式的蜕变 Ctrip Tech 本文不再从最基本的语法开始行文,而在列举一些最基本的信息之后,开始探讨传统响应式设计的问题,与在实践当中思考出来的改进方法,...

1849
来自专栏司想君

手拉手,用Vue开发动态刷新Echarts组件

2788
来自专栏前端架构

重谈react优势——react技术栈回顾

现在,react已经慢慢退火,该用用react技术栈的已经使用上,填过多少坑,加过多少班,血泪控诉也不下千文。

1053

扫码关注云+社区