Redux Middleware 的设计实现

387 阅读8分钟

这篇文章主要根据官方对于Middleware介绍,来分享一下个人对于 Redux Middleware 的理解。

因为初看 Middleware 的设计会感觉很奇怪:({ getState, dispatch }) => next => action => {}

那这篇文档的目标就是尽可能的解答这些疑惑。

我们不光会了解 Middleware 功能以及它存在意义,还会从一些真实的需求开始分析,一步一步了解它是怎么演变成最终这个形式的

阅读本文需要读者对 Redux 有一定的了解。

Redux Middleware 功能

这里有一段关于 Middleware 的官方介绍:

Redux middleware provides a third-part extension point between dispating the action, and the moment it reaches the reducer.

Redux Middleware 是提供了一种在 发出 action 之后到达 reducer 之前 进行第三方扩展的能力。

其实说白了就是,Middleware 是对 store 上去的 dispatch 方法的包装,以此来达到扩张能力的目的。

同时,Middleware 的接口设计具有很高的可组合性,以此来达到使用任意中间来扩展 Redux 的目的。

The best feature of middleware is that it's composable in a chain.

Redux Middleware 接口设计的演化

举个例子:在你的应用中希望能够有个中间件提供记录每一个 action 以及 action 之后的 state

没有中间件时我们是这样用的:

store.dispatch(addTodo('xx'));

第一步: 实现这个能力,不考虑复用性等因素

const action = addTodo('xx');

log('dispatch', action);

store.dispatch(action);

log('state', store.getState());

第二步: 复用这个能力

function dispatchAddLog(store, action) {
    log('dispatch', action);
    
    store.dispatch(action);
    
    log('state', store.getState());  
}

第三步: 我们不希望有新的学习记忆负担

为了不用每一种能力的扩展都是一种新的方法,从能增加沟通记忆的成本,我们将方案调整为对原来的 store.dispatch 方法进行重写

function logger(store) {
    const next = store.dispatch;
    store.dispatch = function (action) {
        log('dispatch', action);
    
        const result = next(action);
        
        log('state', store.getState());
        
        return resut;
    }
}

logger(store);

这样一来,就非常接近我们预期了:我们可以像原来一样直接调用 store.dispatch 就可以使用我们扩展

但是问题又来了,如果我们想要扩展的不止一种能力呢??

function loggerAction(store) {
    const next = store.dispatch;
    store.dispatch = function(action) {
        // ... do something
    }
}

function catchError(store) {
    const next = store.dispatch;
    store.dispatch = function(action) {
        // ... do something
    }
}

loggerAction(store);
catchError(store);

像这样在多个函数中,对 store.dispatch 进行了包装,就可以扩展多种能力。

目前,在提供能力扩展方面已经没有任何问题了;

我们开始考虑这种扩展的形式,是否足够好??能否提供更好的扩展方式??

首先:

  • 一个应用中,如果是各处分散的地方应用了这些扩展函数,作为一个开发者,无法掌控了哪些能力(因为这个函数可以在各个地方执行即可)
  • 为这些扩展函数注入参数 store ,无法保证其他开发者没有对 store 做其他更改

第四步: 更改扩展的形式

首先,针对第一个问题,我们提供一个 applyMiddleware 接口,接受其他扩展方法作为参数,那么这个参数列表,就是我们所应用的全部能力扩展

applyMiddleware(store, middlewares) {
    // ...
    middlewares.forEach(middleware => middleware(store));
    // ...
}

放在同一个地方统一管理会使整个项目的扩展变的容易得多

接着,我们解决向扩展函数中注入 store 的问题,我们重新定义扩展函数的形式:

我们不再向这些函数注入 store 了,而是注入当前的 dispatch , 并且规定需要返回新包装好的 dispatch 方法

function loggerAction(dispatch) {
    return function(action) {
        // ... do something
    }
}

function catchError(dispatch) {
    return function(action) {
        // ... do something
    }
}

applyMiddleware(store, middlewares) {
    // ...
    middlewares.forEach(middleware => {
        store.dispatch = middleware(store.dispatch);
    });
    // ...
}

好的,这样一来,我们把 Middleware 的能力限制在仅能对 dispatch 进行包装了。

但是,这样就OK了吗?

当然不是,试想一下这样的场景,如果在一个 middleware 中我们需要让当前的 action 继续往下执行,并且需要发起一个新的 action(这个新的 action 是希望重新经历全部 middlewares 的)。

第五步: 解决 dispatch 与 next 的问题

我们该怎么办?因为当前的 dispatch 只包含了还未执行的 middleware 。

很明显,在每一个 middleware 中,我们需要拿到两个 dispatch 。

第一个 dispatch 是完整的。只要触发 dispatch(action) ,这个 action 就会再次完整经历整个 middleware 链。

第二个 dispatch 是执行本次还未执行的 middlewares, 我们把它命名为 next。上面代码中的 dispatch 本质就是这个 next。

function logger(next) {
    return function(action) {
       // ...
       
       next(action);
       
       // ...
    }
}

那么我们还需要注入完整的 dispatch

function logger(next, dispatch) {
    return function(action) {
        // ...
        // 下面这两种执行效果是不一样的
        next(action);
        
        if (xxx) {
            dispatch(newAction);
        }
    }
}

// OK 再改造一下形式

function logger(dispatch) {
    return function(next) {
        return function(action) {
            // ...
        }
    }
}

这样我们就把形式确定为了

const middleware = dispatch => next => action => { ... };

当然很多时候,我们在 middleware 里拿到当前的 state 。于是,我们把 getState 方法也注入到第一个方法中去

const middleware = ({ getState, dispatch }) => next => action => { ... };

#6 总结 Middleware

好了,到了这一步,可以最终确定一个 Middleware 的最终形态了

function logger({ getState, dispatch }) {
    return function (next) {
        return function (action) {
            // do something
        }
    }
}

function catchError => ({ getState, dispatch }) => next => action => {
    // do something
}

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

OK, 现在所看到的就是 Redux Middleware 的最终形态。

总结

Middleware 只是包装了 storedispatch 方法。

技术上讲,任何 middleware 能做的事情,都可能通过手动包装 dispatch 调用来实现,但是放在同一个地方统一管理会使整个项目的扩展变的容易得多。

applyMiddleware 的设计实现

在上面 Middleware 的演化过程中,我们也展示了 applyMiddleware 的几处实现,主要为了说明 Middleware 的工作机制。

接下来,在确定了 Redux Middleware 的最终形式下,该如何来实现一个 applyMiddleware 的接口。

首先,先看下 Redux applyMiddleware 的接口定义:

applyMiddleware(...middlewares)

来看一下,实际中 applyMiddleware 是怎么用的:

// ...

const store = createStore(
    reducer,
    initailState,
    applyMiddleware(logger, catchErr)
);

// ...

可以发现,applyMiddleware 只接受任意中间件函数做参数传入。

但是,从分析 middleware 的演化过程中,我们知道 applyMiddleware 执行中一定是需要拿到 store ,然后对其 dispatch 方法进行包装的。

第一步: 确定 applyMiddleware 的形式

如何在 applyMiddleware 中获取到当前的 store ??

为了实现这个目标,我们假设 applyMiddleware 的形式如下:

function applyMiddleware(...middlewares) {

    return (store) => {
    
        if (middlewares.length === 0) {
            return store;
        }
    
        // ... 包装 store 上的 dispatch 方法
        
        return store;
    }
}

第二步: 包装 dispatch 方法

好了,在确定了 applyMiddleware 的形式后,我们来实现 middlewares 对 dispatch 方法的包装

function applyMiddleware(...middlewares) {

    return (store) => {
        // ...
        
        const param = {
            getState: store.getState,
            dispatch: store.dispatch,
        };
        
        let dispatch = store.dispatch;
        
        middlewares.forEach(middleware => {
            dispatch = middleware(param)(dispatch); 
        });
        
        store.dispatch = dispatch;
        
        // ...
    }
}

不知道大家这样实现的问题了没有

如果我们使用 applyMiddleware(logger, catchErr) 时,一定是希望限制性 logger 中间件,再执行 catchErr 中间件的。

而上的实现恰恰相反,所以我们需要对 middlewares 数反转一下。

function applyMiddleware(...middlewares) {

    return (store) => {
        // ...
        
        middlewares.reverse();
        
        middlewares.forEach(middleware => {
            dispatch = middleware(param)(dispatch); 
        });
        
        // ...
    }
}

第三步: 参考 Redux 的真实实现

就功能而言,下面 Redux 的实现确实只是上面代码的改写

function applyMiddleware(...middlewares) {

    return (store) => {
        // ...
        
        const param = {
            getState: store.getState,
            dispatch: store.dispatch,
        };
        
        // 此时 chain 的定义为:next => action => { ... } 的数组
        const chain = middlewares.map(middleware => middleware(param));
        
        store.dispatch = compose(...chain)(store.dispatch);
        
        // ...
    }
}

/**
  *  compose 函数将 next => action => { ... } 的数组,组合成了一个 next => action => { ... } 方法
  *
  * 并且实现 chain 的逆序
  **/
function compose(...func) {
    if (func.length === 1) {
        return func[0];
    }

    // from right to left
    return func.reduce((a, b) => (dispach: Dispatch) => a(b(dispach)));
}

大家可能觉得这个只是一些写法上的不同

那么我们接着看下一个问题

第四步: 避免在创建 middlewares 时使用 dispatch

截止到目前为止,我们的功能已经全部实现了,但是还存在一个小小的问题。

因为我们为每个中间件注入了 dispatchgetState 方法。

用户很可能会误用在创建中间件的时候去触发 dispatch ,就像这样:

function logger({ getState, dispatch }) {
    return function(next) {
        // 这是一种误用,这时拿到的是 store 上原始的 dispatch 方法,此时的 dispatching 过程将不会经过任何中间件
        dispatch(xxx);
        
        return function(action) {
            // ...
            next(action);
        }
    }
}

这种使用拿到的是 store 上原始的 dispatch 方法,此时的 dispatching 过程将不会经过任何中间件。

在不希望存在这种使用的情况下,我们希望给出用户一个 Error 提示

我们通过这样的实现来解决:

function applyMiddleware(...middlewares) {

    return (store) => {
        // ...
        
        let dispatch = function(action) {
            throw new Error('Dispatching while constructing your middleware is not allowed. ') ; 
        }
        
        const param = {
            getState: store.getState,
            dispatch: (action) => dispatch(action),
        };
        
        // 此时 chain 的定义为:next => action => { ... } 的数组
        const chain = middlewares.map(middleware => middleware(param));
        
        dispatch = compose(...chain)(store.dispatch);
        
        store.dispatch = dispatch;
        
        // ...
    }
}

OK,因为在创建了所有的中间件后,我们最后才更改 dispatch 方法,所以,在此之前,创建中间件的过程中,拿到的 dispatch 方法都是会呢个抛出错误的 dispatch 方法。

第五步: 最后,不要重复应用 middlewares

最后一点,为了不会对同一个 store 进行重复应用 middlewares , applyMiddleware 的函数实际设计为:

function applyMiddleware(...middlewares) {

    return (createStore) => (reducer, ...args: any[]) => {
        const store = createStore(reducer, ...args);
    
        // ... 包装 store 上的 dispatch 方法
        
        return store;
    }
}

对于这一点我到不是很认同,applyMiddleware 和 createStore 作为内部实现,实际可以避免这种问题。

参考: 官方文档 中文文档