如何更优雅地使用 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 条评论
登录 后参与评论

相关文章

来自专栏阮一峰的网络日志

四种免费英汉电子词典软件简评

老牌的词典软件,口碑一向很好,多年来我一直使用。唯一感觉有欠缺的有以下几点:1)可配置性差,外部词典资源不足,难以扩展;2)例句还是太少;3)安装文件太臃肿,容...

5442
来自专栏华章科技

【一文打尽】SQL 数据分析常用语句.....收藏

• 1 基础查询 • 2 字符串\数字\日期时间 • 3 聚合数据查询 • 4 子查询 • 5 联接\组合查询 • 6 高级查询 • 7 更新数据

601
来自专栏IT派

520特别版Python实战:教你用微信每天给TA说晚安

导读:今天就是520,不知你是否已经准备好要表白的话语。为了助力你撩妹成功,大数据今天也提前备了点干货——教你用Python每天给妹纸发晚安。

1151
来自专栏SDNLAB

SDNLAB技术分享(十五):容器网络大观

一、容器网络概述 容器这一两年火的不行,可以说是独领IT风骚,一时风光无二。相比于虚拟机来说,容器更轻,一台服务器上可以运行成百上千的容器,这意味着更为密集的计...

3548
来自专栏北京马哥教育

深入浅出Docker(一):Docker核心技术预览

【编者按】Docker是PaaS供应商dotCloud开源的一个基于LXC 的高级容器引擎,源代码托管在 GitHub 上, 基于Go语言开发并遵从Apach...

3284
来自专栏一“技”之长

iOS音频播放器锁屏歌词显示与性能优化 原

    前边有博客探讨了有关iOS开发中音频播放的技术与进行后台音频播放并在后台与用户进行交互的方法,本篇将探讨一种在锁屏界面同步显示歌词歌词的方法,并在应用性...

1002
来自专栏CodeSheep的技术分享

SpringBoot应用Docker化

22613

容器技术,还处在起点阶段

容器,Docker,Kubernetes,这些技术已经被使用了四年左右。有些人甚至开始认为这项技术已经成熟了!但我强烈认为,容器基础设施的实现还处于一个成长的阶...

2337
来自专栏酷玩时刻

QQ轻游戏入门到精通OR放弃?

注册很简单,使用已有Q号登录「厘米游戏」开放平台按照流程提交资料审核即可 。开发者接入官方说明文档

5844
来自专栏SDNLAB

SDDC新生力量:F5 BIG-IP & VMware NSX

过去10年中,企业为了享受软件计算和存储资源的灵活性迅速地接纳了网络虚拟化。SDN和虚拟化新用例软件定义数据中心(SDDC)越来越受欢迎。随着F5和VMware...

3479

扫码关注云+社区