Redux介绍之中间件

17 May

Redux的基础知识前几篇该介绍的都介绍完了,现在介绍点高阶的内容。本篇介绍一下中间件。我最早接触到中间件是在学习Express框架时,可见中间件Middleware并不是个什么新事物。中间件要满足两个特性:一是扩展功能,二是可以被链式组合。

我们来看个例子,例如Redux里dispatch一个Action,Reducer收到Action后更新state。我们希望能在这个过程中自动打印出Action对象和更新后的state,便于调试和追踪数据变化流。现在我们来写一个logger的中间件。源码已上传Github,请参照src/reactReduxMiddleware文件夹。

首先考虑在什么时候打印log?回顾前几篇介绍的Redux基础知识,Action是个plan object没地方写console.log。Action creator里可以打印出Action对象,但此时还没有执行Reducer,因此无法打印更新后的state。Reducer里可以取到Action对象和更新后的state,但它应该是个纯函数,不应该把console.log代码写进去。最后只剩一个选择,写到调用dispatch的地方:

console.log('current state: ', store.getState());
console.log('dispatching', action);
store.dispatch(action);
console.log('next state', store.getState());

这样能顺利打印出log,功能实现了,目的达到了。但显然我们可以封装一下,在所有dispatch前后都自动打印一下log。写一个增强版Store.dispatch:

const preDispatch = store.dispatch;

store.dispatch = (action) => {
    console.log('current state: ', store.getState());
    console.log('action: ', action);
    preDispatch(action);
    console.log('next state: ', store.getState());
};

上面都是自解释代码,函数签名与返回值和原生Store.dispatch一模一样,函数内部仍旧调用原生的Store.dispatch,最后将Store.dispatch指向这个增强版。在程序入口处加上这段代码,程序员写业务代码时根本意识不到这是个增强版dispatch方法,仿佛dispatch本就应该打印出log一样。

这就是中间件的雏形,Redux里的中间件都是改写dispatch方法(原因上面说了,Redux里只有dispatch适合写中间件)。但如果有多个中间件,都是改了dispatch方法,该怎么处理呢?

以上述打印log为例,简单起见,我们将其拆成两个。一个中间件只打印出Action,另一个中间件只打印出更新后的state:

// 只打印出 Action
const loggerAction = (store) => {
    const preDispatch = store.dispatch;
    store.dispatch = (action) => {
        console.log('action: ', action);
        preDispatch(action);
    };
};

// 只打印出 更新后的state
const loggerState = (store) => {
    const preDispatch = store.dispatch;
    store.dispatch = (action) => {
        console.log('current state: ', store.getState());
        preDispatch(action);
        console.log('next state', store.getState());
    };
};

const store = createStore(reducer);
loggerAction(store);
loggerState(store);

顺利打出log,效果如图:

redux

打上两个补丁的最终Store.dispatch如图,像个洋葱圈:

redux

至此我们已经知道中间件的用途了,但调用起来比较麻烦,如果有5个中间件要调用5次太麻烦了。可以设计的更智能点,我们自定义一个applyMiddleware方法(applyMiddleware其实是Redux为中间件提供的官方方法,现在我们自己来实现这个方法),允许将所有中间件以数组形式传递进去,参照lib/middleware.js的Step3:

// 只打印出 Action
export const loggerAction = (store) => (dispatch) => (action) => {
    console.log('action: ', action);
    dispatch(action);
};

// 只打印出 更新前后的state
export const loggerState = (store) => (dispatch) => (action) => {
    console.log('current state: ', store.getState());
    dispatch(action);
    console.log('next state', store.getState());
};

export const applyMiddleware = (store, middlewares) => {
    let dispatch = store.dispatch;
    middlewares.forEach((middleware) => {
        dispatch = middleware(store)(dispatch);
    });

    return {
        ...store,
        dispatch,
    };
};

entries/reactReduxMiddleware.js:

let store = createStore(reducer);
store = applyMiddleware(store, [loggerAction, loggerState]);

分析一下上面的代码,如果你熟悉ES6的箭头函数,其实也是自解释代码。原理就是将每个中间件设计成接受一个dispatch参数,并返回加工过的dispatch作为下一个中间件的参数,以方便链式调用。在applyMiddleware中返回的是原生store的一个副本,副本里的dispatch被最终生成的洋葱圈式的dispatch替换了。

这个applyMiddleware已经很接近Redux官方提供的同名方法了,只是官方版更加优化,将第一个参数store也封装掉,返回的是一个createStore方法。参照lib/middleware.js的final Step:

export const applyMiddleware = (...middlewares) => {
    return (createStore) => (reducer, preloadedState, enhancer) => {
        const store = createStore(reducer, preloadedState, enhancer);

        let dispatch = store.dispatch;
        middlewares.forEach((middleware) => {
            dispatch = middleware(store)(dispatch);
        });

        return {
            ...store,
            dispatch,
        };
    };
};

entries/reactReduxMiddleware.js:

const createStoreWithMiddleware = applyMiddleware(loggerAction, loggerState)(createStore);
const store = createStoreWithMiddleware(reducer);

上面这个版本的applyMiddleware和官方版的源码相似度已经达到90%,只是写法细节上稍有不同。

最后总结一下,中间件是一个强化源码功能,支持多个中间件链式调用的概念。在Redux里中间件等同于修改Store.dispatch方法。多个中间件用applyMiddleware方法组合成一个洋葱圈式的强化版Store.dispatch。常用的中间件有redux-logger,及下一篇异步Action中会介绍到的redux-thunk。

Leave a Reply

Your email address will not be published. Required fields are marked *