网易有数项目中Redux的应用

最近在看项目中报表编辑的代码,在这里写写自己对于代码的理解,整理一下思路,本文主要是写波神在报表编辑模块中应用的Redux,当然这个是简化版的,有数在这个流程中还嵌入了其他的流程,比较复杂,如果这里说法有错误的,多多包涵。

先上一张有数报表编辑的界面:

下面进入正题:

从界面中我们可以看到,在右上角有一些icon用于操作,明显整个编辑过程是支持撤销和恢复的,所以今天要说的是支持撤销和恢复的Redux,会从下面几个方面来进行整理:

  • state的结构
  • middleware
  • reducer的扩展
  • 结合regular使用

state的结构

通过打印store.getState()我们可以得到state的结构,这里不关心数据对象是怎样的,但是需要强调的一点是,每个state对象都应该是全新的对象,修改state不能直接state.key = value,应该类似于state = Object.assign({}, state, {key: value}, true)

{
    current: Object,          //当前的数据
    index: Number,            //当前数据在timeline数组中的索引
    timeline: Array[Object],  //存储时间线上的数据的数组
    size: Number              //限定timeline的数量
}

添加一步操作时:

  • current = reducer(state.current, action)来将current变成新的state
  • 取得正确的timeline,这里有两种情况需要注意:(1)当timeline的个数已经是我们限定的size值了,那么我们应该将timeline中的第一个state抛弃掉。(2)当现在的state是在timeline中间的一环,应该将当前state后面的state清除掉。
  • timeline.push(current),current为新的state对象
  • index = timeline.length - 1

撤销一步操作时:

  • 判断是否在第一步,如果不是就可以将current指向timeline[index-1]
  • index = index - 1

恢复一步操作时:

  • 判断是否在最后一步,如果不是就可以将current指向timeline[index+1]
  • index = index + 1

middleware

Redux的middleware提供的是reducer前后的扩展点。你可以利用middleware来进行日志记录、创建崩溃报告、调用异步接口或者路由等等。

var createStore = Redux.applyMiddleware(middleware1, middleware2)( Redux.createStore )

一个middleware长这样:

function middleware() {
    return function(next) {
        return function(action) {
            //do before reducer
            var result = next(action)
            //do after reducer
            return result;
        }
    }
}

这个中间件中,有2层return,最终的效果是applyMiddleware函数会整合两个中间件,当你执行store.dispatch(action)的时候,相当于执行了下面的过程:

//do before reducer at middleware1
//do before reducer at middleware2
store.dispatch(action)
//do after reducer at middleware2
//do after reducer at middleware1

这样你就可以在dispatch之前做一些操作,像有数这边主要做的一些工作,比如在获取报表之前会先请求看这张报表是否锁定,在dispatch之后做一些错误操作的检查等等。

然后再创建store:

store = createStore(reducers, initState);

Reducer的扩展

一个reducer是这样的:

function reducer(state, action) {
    switch (action.type) {
        case 'someType':
            var newState = {};
            ...
            return newState;
            break;
        default: 
            return state;
            break;
    }
}

上面是一个普通的reducer,当你想要有前进后退功能时,要对齐进行扩展,在有数中,使用了track函数来进行扩展,下面的代码是简化版,如果还需要什么功能,都是可以加的。

track函数如下:

var handlers = {
    'undo': undo,
    'redo': redo
}
var track = function(reducer) {
    var initialState = {
        timeline: [],
        index: -1,
        size: 24
    };
    return function(state, action) {
        if (state === undefined) {
            state = initialState;
        }
        var actionType = action.type;
        var handler = handlers[actionType];
        if (handler) {
            //是回退或者恢复
            return handler(state, action.payload)
        }
        //其他action
        return add(state, reducer(state.current, action), action)
    }
}

function add(state, data, action) {
    var current = state.current,
        nextState = {current:data};
    var nextTimeline = nextState.timeline = state.timeline.slice(
        state.index + 1 >= state.size ? 1 : 0,
        state.index + 1
    );
    nextState.index = nextTimeline.push(data) - 1;
    return $util.extend(newState, state) //extend是对象扩展函数,像jQuery.extend
};

function undo(state) {
    return seek(state, state.index - 1)
}

function redo(sate){
    return seek(state, state.index + 1)
}

function seek(state, index){
    if (index < 0) index = 0;
    if (index > state.timeline.length -1) index = state.timeline.length - 1;
    return index === state.index ? state : $util.extend({
        index: index,
        current: state.timeline[index]
    }, state)
}

这样的reducer就支持回退和恢复了,当需要回退时:

store.dispatch({
    type: 'undo'
})

恢复时:

store.dispatch({
    type: 'redo'
})

和RegularJs结合使用

我们都知道,React结合Redux产生了react-reduxk,有数和Redux结合起来是这样的,整个报表是一个Regular组件:

 var Report = Regular.extend({
    name: 'report',
    template: tpl,
    data: {},
    config: function(){},
    init: function(){}
 })

我们希望每次state的变化能及时地在页面中得到反映,很自然而然地想到要有一个监听回调函数:store.subscribe。

因为Regular的模版解析是在config和init之间发生的,所以将state反射到data的过程就应该放在config的最后,考虑到Report组件的很多子组件都会需要将state的数据解析到data上,所以这个方法应该封装起来被复用。

这时就用到了$afterConfig,这是Regular在实例化组件过程中会触发的一个事件,所以封装在一个mixins就变成了下面代码:

mixins.Redux = {

    events: {
        $afterConfig: function() {

            var data = this.data;

            if (this.mapStateToData) {

                var unsub = store.subscribe( this.mapState.bind(this) );
                this.mapState()
                this.$on('$destroy', unsub);

            }
        }
    },

    mapState: function(){
        var state = store.getState();
        if( !state.current ) return;
        this.mapStateToData(state.current, this.data, state);
        setTimeout(function(){
            this.$update();
        }.bind(this), 0)
    }
}

当你需要在组件中(报表或者其子组件)实时监听state变化的话就这样引用即可:


var Report = Regular.extend({
    name: 'report',
    template: tpl,
    data: {},
    config: function(){},
    init: function(){},
    /*添加mapStateToData函数*/
    mapStateToData: function(currentState, data, state) {
        //TODO
    }
 }).implement(mixin.Redux)


网易有数:企业级大数据可视化分析平台,具有全面的安全保障、强大的大数据计算性能、先进的智能分析、便捷的协作分享等特性。点击免费试用

本文来自网易实践者社区,经作者康东扬授权发布。