这篇文章主要根据官方对于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 只是包装了 store 的 dispatch 方法。
技术上讲,任何 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
截止到目前为止,我们的功能已经全部实现了,但是还存在一个小小的问题。
因为我们为每个中间件注入了 dispatch 和 getState 方法。
用户很可能会误用在创建中间件的时候去触发 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 作为内部实现,实际可以避免这种问题。