如何更优雅地使用 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 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏从零开始学自动化测试

python接口自动化21-规范的API接口文档示例

前言 接口文档到底长啥样?做接口测试最大的障碍在于没有接口文档,很多公司不注重接口文档的编写,导致测试小伙伴没见过接口文档。 运气好一点的测试小伙伴可能厚着脸皮...

7868
来自专栏腾讯Bugly的专栏

Android 内存优化总结&实践

导语 智能手机发展到今天已经有十几个年头,手机的软硬件都已经发生了翻天覆地的变化,特别是Android阵营,从一开始的一两百M到今天动辄4G,6G内存。然而大部...

3937
来自专栏Google Dart

为Flutter应用程序添加交互性 顶

你如何修改你的应用程序,使其对用户输入做出反应? 在本教程中,您将为仅包含非交互式小部件的应用添加交互性。 具体来说,您将通过创建一个管理两个无状态小部件的自定...

1212
来自专栏腾讯IVWEB团队的专栏

浏览器渲染原理

本文为大家分享了浏览器的工作原理和渲染过程,如计算CSS样式,构建Render Tree, Layout等过程。

1.3K2
来自专栏一名合格java开发的自我修养

Storm同步调用之DRPC模型探讨

摘要:Storm的编程模型是一个有向无环图,决定了storm的spout接收到外部系统的请求后,spout并不能得到bolt的处理结果并将结果返回给外部请求。...

901
来自专栏云计算

服务集成时需避免的两个错误

随着面向服务架构(下文简称 SOA,Service Oriented Architecture)的出现,企业通过将业务功能分解为多重服务 [1],它们迅速地从整...

2685
来自专栏服务端思维

Java面试通关要点 汇总集【最终版】

首先,声明下,以下知识点并非阿里的面试题。这里,笔者结合自己过往的面试经验,整理了一些核心的知识清单,帮助读者更好地回顾与复习 Java 服务端核心技术。本文会...

742
来自专栏谈补锅

深入浅出-iOS程序性能优化 (转载)

iOS应用是非常注重用户体验的,不光是要求界面设计合理美观,也要求各种UI的反应灵敏,我相信大家对那种一拖就卡卡卡的 TableView 应用没什么好印象。

862
来自专栏AndroidTv

属性动画 ValueAnimator 运行原理全解析

好,废话不多说,之前我们已经分析过 View 动画 Animation 运行原理解析,那么这次就来学习下属性动画的运行原理。

3638
来自专栏前端小吉米

无 Flash 时代,让直播拥抱 H5(完整篇)

2234

扫码关注云+社区